This commit is contained in:
Gabrieal Jimmy
2026-10-01 15:15:20 -05:00
commit 97a27f9a42
327 changed files with 7075 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Python
__pycache__/
*.py[cod]
.venv/
venv/
.env/
# Secrets (OPENROUTER_API_KEY is read from the environment)
.env
.env.*
*.key
secrets*
# Generated sprites (generate_sprite.py --out, default sprites/)
sprites/
sample_out/
test_out/
test_out_colors/
out/
# Generated contact sheets (preview.py, $PREVIEW_OUT defaults to preview/)
preview/
# Scratch reports
size_changes.txt
# Real business logos used for local testing
test_logos/
logos/
# Editor / OS
.vscode/
.idea/
*.swp
.DS_Store
Thumbs.db

108
AI_PIXEL_ART_GUIDE.md Normal file
View File

@@ -0,0 +1,108 @@
# How to make good pixel art as an AI (no image model)
This guide is for AI coding agents. It describes the method used to build the Henry pixel-art loading animations (`~/Documents/henry_pixel_art_animations`). No diffusion or image generation was involved: every pixel is written as code, rendered, looked at, and fixed.
The reference implementation is in `henry_pixel_art_animations/src/lib/henry/`: `engine/` holds the drawing engine, `henry.ts` the mascot sprite, and `scenes/` one file per animation.
---
## 1. Constraints first
Hard limits are what make the result look like real pixel art instead of blurry clip art. Decide them before drawing anything.
- **Tiny canvas.** Everything is drawn at a low logical resolution (Henry uses 160×120, 4:3), then upscaled. Never draw at screen resolution.
- **Whole pixels only.** Round every coordinate. Sub-pixel positions smear when scaled.
- **One fixed palette.** Use about 16 colours plus a few named extras. Henry uses Sweetie 16 by GrafxKid. Every scene uses only these colours, which is why they all look like one set. Ban ad-hoc hex values, or give each extra colour a name.
- **1px dark outline** (`ink`, the palette's darkest colour) around characters and props, so they read against any background.
- **Upscale with nearest-neighbour.** In canvas, set `imageSmoothingEnabled = false`; in CSS, use `image-rendering: pixelated`. For fractional display scales, use a "sharp bilinear" upscale: scale with nearest-neighbour to the next whole multiple, then do one smooth downscale to the exact size. Pixels then stay evenly sized.
## 2. Sprites are text grids
Write each sprite as an array of equal-length strings. Each character maps to a palette colour, and `.` is transparent.
```ts
const PAL = { k: C.ink, w: C.down, h: C.downLight, s: C.downShade, e: C.ink, o: C.bill, O: C.billShade, f: C.bill };
const STAND = [
'....kkkkk.....',
'...kwwwwwk....',
'..khwwwwwwk...',
'..khwwwwwwwk..',
'..kwwwwwwewk..', // e = eye
'..kwwwwwwewkk.',
'..kwwwwwwwwook', // o = bill
'..kswwwwwwwOk.',
// ...
'.....f..f.....', // feet
'.....ff.ff....',
];
```
- **Validate.** Throw if any row has a different width. This catches most authoring mistakes.
- **Bake once.** Render each sprite once to a small offscreen canvas, plus a horizontally flipped copy, and `drawImage` those every frame.
- **Make new poses by editing rows, not redrawing.** A blink changes the eye cell. Breathing drops one row so the body squashes 1px. A crouch drops two rows. A stride swaps the feet rows. Keeping the head rows identical keeps the character on-model across every pose.
- **Reason row by row** about silhouette and symmetry while writing the grid. Get the outline right first, then add the fill, then one highlight colour and one shade colour.
- **Keep sprites small.** A 14×18 character is plenty; small sprites read more clearly and are easier to get right.
## 3. Draw scenery procedurally
Backgrounds are drawing commands, not grids.
- **Primitives:** `rect`, `px`, a scanline `disc` (a filled circle with no antialiasing), `pill` (a rounded rectangle with 1px corner notches), `clip`, and `sprite`.
- **Dithering:** at colour band edges, use a checkerboard. For example, at a sky band edge, paint the previous band's colour where `(x + y) % 2 === 0`, fading over 2–4 rows.
- **Seeded randomness** for skylines, asphalt specks and windows, with a small PRNG such as `mulberry32(seed)`, so the scenery is identical every run.
- **Pre-render static layers** (background, foreground props) once into offscreen canvases. Each frame, draw only the moving things between them.
- **Layer order:** background, then far movers (clouds, birds, distant cars), then big actors (bus), then foreground props, then the character, then speech bubbles and effects.
- **Ambient life** keeps loops from feeling static: drifting clouds, a passing bird, cars, blinking, flickering lights.
## 4. Animate with timers and state machines
- **Phases.** Each animation is a small state machine, e.g. `intro → waiting (loops forever) → resolving → finished`. Each phase has its own timer.
- **Idle scheduler.** While waiting, the character randomly picks short "acts" (look around, tap foot, hop, think bubble), never the same act twice in a row, with random rests between. This prevents a visibly repeating loop.
- **Motion:** use easing functions (`easeOutCubic`, `easeOutBack`) and a parabolic `hop(t, height)`. Step frame animations by time (`Math.floor(t / 0.16) % 2`) or by distance travelled (wheels, walk cycles).
- **Cartoon cues:** a 1-frame pop on bubbles, dust puffs on landing, a squash before a jump, sparkles on success, wobble or shake for impact.
- **Clamp `dt`** to a range like 0–0.05 s, so a backgrounded tab doesn't fast-forward, and a negative first-frame delta can't break array indexing.
- **Transitions:** the iris (circle) wipe paints everything outside a circle with the palette's darkest colour, one scanline at a time so the edges stay crisp. It closes on the character's position and opens the next scene on its character.
## 5. Render, look, fix — the step that matters most
You cannot see the art while writing the character grids, so you must render it and inspect it. Never ship unviewed art.
1. Run the app (e.g. a Vite dev server) and open it in a headless browser.
2. Grab frames from the canvas **inside one in-page script**, because headless tabs may freeze between tool calls:
```js
// in page.evaluate: sample the canvas every 250 ms, trigger UI via setTimeout in the same script
const out = []; const c = document.querySelector('canvas');
const id = setInterval(() => { out.push(c.toDataURL('image/png')); if (out.length > 40) { clearInterval(id); resolve(out); } }, 250);
```
For exact, repeatable checks, import the scene module directly in the page and step it with a fixed `dt = 1/60`, drawing to an offscreen canvas.
3. Write the frames as PNGs, then tile them into a contact sheet with ffmpeg. Always use nearest-neighbour scaling:
```sh
ffmpeg -framerate 1 -i f%03d.png -vf "scale=160:120:flags=neighbor,crop=W:H:X:Y,scale=320:-1:flags=neighbor,tile=6x4" -frames:v 1 sheet.png
```
4. **Look at the image.** Crop and zoom to 4–10× on details such as the character, icons and contact points. Check whether:
- the silhouette reads, and the icon is recognisable;
- the motion reads across frames;
- things line up (feet on the ground, a bill touching a button, nothing clipping through a wall);
- the character is still visible when the iris closes.
5. Fix and repeat. Expect several rounds on anything important. Real fixes from this project: the bill was one pixel too narrow; the "walk" icon was a blob until it got a gap between head and body; a button was 2px too low for the bill to touch; a peeking pose showed the tail instead of the face until it was flipped.
6. For small icons, preview the grid by itself on a plain background at about 14× before placing it in the scene.
## 6. Scaling up with multiple agents
If several agents each build a scene, keep the style consistent:
- **Write a shared guide first.** Cover the engine API, palette, layout constants, character sprite and helpers, the scene contract, style rules (outlines, dithering, ambient life), and the render-and-look verification loop.
- **Freeze the shared files** (engine, palette, character). Agents put scene-specific colours and poses in their own files, and build new poses from the shared character rows so the design never drifts.
- **Give each agent a way to open its scene directly,** e.g. `?scene=<id>`, and a unique temp directory and browser tab.
- **Review every finished scene yourself.** Step each one with fixed timesteps across early, middle and late "done" triggers to catch crashes and scenes that never finish, then look at the contact sheets.
## Checklist
- [ ] Low fixed resolution, whole pixels, nearest-neighbour upscale
- [ ] One named palette; no stray colours
- [ ] 1px dark outlines on characters and props
- [ ] Sprites as text grids with validated widths; poses made by editing rows
- [ ] Procedural backgrounds with checkerboard dithering and seeded randomness
- [ ] State-machine animation, varied idle acts, ambient motion, clamped `dt`
- [ ] Rendered, sampled, contact-sheeted, zoomed and **looked at** — then fixed and checked again

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
# sprite_gen
Procedural 96x96 creature sprites for businesses. You give it a business name and logo. It picks a type, builds a creature from parts and recolors it with the logo's brand colors.
## Setup
```bash
pip install -r requirements.txt
```
You only need an [OpenRouter](https://openrouter.ai) API key if you want the type picked automatically. Skip the key by passing `--type`.
## Generate a sprite
```bash
# Type picked from the business name
OPENROUTER_API_KEY=<API_KEY> python generate_sprite.py "Rosas Landscaping" logos/rosas.png
# Add a description to help pick the type
OPENROUTER_API_KEY=<API_KEY> python generate_sprite.py "Rosas Landscaping" logos/rosas.png --desc "Lawn care and garden design"
# Set the type yourself (no API key needed)
python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass
# Set the colors yourself instead of using a logo (main color first)
python generate_sprite.py "Rosas Landscaping" --type grass --colors "#2E7D32,#FBC02D"
# A different creature for the same business
python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass --reroll 1
```
Each run writes `sprites/<slug>.png` (96x96), `sprites/<slug>@4x.png` and `sprites/<slug>.json` (type, parts, colors). With `--reroll N`, the file names end in `-rN`.
| Option | Meaning |
|---|---|
| `--type T` | Skip the classifier. One of: normal fire water grass electric ice fighting poison ground flying psychic bug rock ghost dragon |
| `--desc TEXT` | Description sent to the classifier (default: the name) |
| `--colors "#main,#accent"` | Manual colors; overrides the logo |
| `--reroll N` | Alternate creature; the name alone always gives the same sprite |
| `--out DIR` | Output folder (default `sprites`) |
| `--scale N` | Size of the upscaled copy (default 4, `1` disables it) |
Logo colors: transparent pixels, soft edge pixels and a solid white/black/gray background are ignored, and colored brand colors are used before white, black or gray. A logo with only white, black or gray in it still uses those colors.
## Batch
Use a CSV with the columns `name,logo,type,description`. Only `name` is required, and rows with no `type` use the classifier.
```bash
OPENROUTER_API_KEY=<API_KEY> python generate_sprite.py --batch businesses.csv --out sprites
```
## Editing the art
The parts are text grids in `parts_src/<slot>/<name>.txt`, compiled to `parts/<slot>/<name>.png`. Weights, compatibility and deny lists live in `sprite_parts.py`. See `AI_PIXEL_ART_GUIDE.md` for the palette and anchor rules.
```bash
python build_parts.py # compile all grids (or pass part names)
python validate_parts.py --sample 3000 # check colors, anchors, canvas fit on random composites
python preview.py bases # contact sheets in preview/
python preview.py part back plant_bulb # one part on every base it fits
python preview.py zoom body=base_fox tail=tail_leaf
python generate_sprite.py --sprite-list SPRITES.md # regenerate the part list and weight report
```
Do not run `--make-placeholders --overwrite`, because it replaces the real art with test placeholders.

319
SPRITES.md Normal file
View File

@@ -0,0 +1,319 @@
# Sprite checklist
**104 sprites** across 5 slots. Save each as `parts/<slot>/<name>.png`.
## Art rules
- Final canvas is 96x96. Sizes below are suggested part canvases; crop tight with transparency.
- Characters face left in a 3/4 view, light from the top-left. 1px outline, no anti-aliasing, no semi-transparent pixels.
- Draw only with the palette keys below. Any other color (eye white, pupils) is left as-is.
| Key | Hex | Use |
|---|---|---|
| main_light | #FF8080 | body / most fur and skin |
| main_base | #FF0000 | body / most fur and skin |
| main_dark | #800000 | body / most fur and skin |
| accent_light | #80FF80 | horns, wings, beaks, crests, shells |
| accent_base | #00FF00 | horns, wings, beaks, crests, shells |
| accent_dark | #008000 | horns, wings, beaks, crests, shells |
| type_light | #8080FF | elemental bits: flames, leaves, sparks, ice, water |
| type_base | #0000FF | elemental bits: flames, leaves, sparks, ice, water |
| type_dark | #000080 | elemental bits: flames, leaves, sparks, ice, water |
| outline | #000000 | outline (recolored) |
| pupil | #141414 | not recolored |
| eye white | #FFFFFF | not recolored |
### Anchor marker pixels
One pixel each. `pivot` is where the part attaches to its parent; the others are where children attach.
| Marker | Hex | Needed in |
|---|---|---|
| pivot | #FF00FF | every part (bodies: bottom center between the feet) |
| head_top | #FA00FF | all bodies (top of the head) |
| tail | #FD00FF | all bodies |
| back | #FC00FF | all bodies |
| aura | #FB00FF | all bodies (center of mass; belly patch centre) |
## body (28)
| Done | File | Size | Palette | Plans | Description | Most likely for |
|---|---|---|---|---|---|---|
| [ ] | `body/base_fox.png` | 42x36 | main | quadruped | Chibi fox/cat critter with cheek tufts, big pointed ears and a fluffy bib | fire, electric, psychic |
| [ ] | `body/base_cub.png` | 40x34 | main | quadruped | Chunky bear-cub on all fours, round ears, sturdy paws | ground, fighting, rock |
| [ ] | `body/base_pup.png` | 42x34 | main | quadruped | Bouncy puppy with floppy ears and a happy open mouth | normal, fire, fighting |
| [ ] | `body/base_salamander.png` | 47x27 | main | quadruped | Low chibi salamander/newt with a wide smile and frilly cheeks | fire, water, poison |
| [ ] | `body/base_drake.png` | 42x38 | main | quadruped | Cool baby dragon on four legs, nub horns, confident grin | dragon, fire, rock |
| [ ] | `body/base_fawn.png` | 42x35 | main | quadruped | Dainty chibi fawn in 3/4 view: big leaf ears, gentle eyes, spotted back | grass, ice, psychic |
| [ ] | `body/base_imp.png` | 40x40 | main | biped | Upright mouse-imp with big round ears and a cheeky fanged grin | electric, normal, psychic |
| [ ] | `body/base_brawler.png` | 42x43 | main | biped | Stout little fighter with big fists and a determined face | fighting, ground, rock |
| [ ] | `body/base_bunny.png` | 36x45 | main | biped | Upright chibi bunny with long ears and a fluffy chest | normal, grass, ice |
| [ ] | `body/base_dino.png` | 36x44 | main | biped | Little upright dino with a big head, tiny arms and a toothy grin | dragon, fire, ground |
| [ ] | `body/base_pixie.png` | 42x39 | main | biped | Leaf-elf pixie with sweeping pointed ears, round torso, stubby legs and a mischievous grin | psychic, ghost, grass |
| [ ] | `body/base_chick.png` | 36x35 | main | avian | Egg-round chick with a feather tuft, wing nubs and a tiny beak | normal, ice, water |
| [ ] | `body/base_owl.png` | 36x36 | main | avian | Round owlet with huge eyes and ear tufts | psychic, flying, ghost |
| [ ] | `body/base_hawk.png` | 38x37 | main | avian | Cool young hawk, puffed chest, sharp brow and hooked beak | flying, fire, fighting |
| [ ] | `body/base_penguin.png` | 36x38 | main | avian | Chubby penguin chick with flipper wings and a bib | ice, water, normal |
| [ ] | `body/base_guppy.png` | 46x33 | main | aquatic | Round bubbly fish with big eyes and frilly fins | water, ice, poison |
| [ ] | `body/base_otter.png` | 49x30 | main | aquatic | Chubby seal/otter pup lying on its flippers | water, ice, normal |
| [ ] | `body/base_axolotl.png` | 51x30 | main | aquatic | Smiling axolotl with feathery gill fronds | water, poison, dragon |
| [ ] | `body/base_noodle.png` | 44x40 | main | serpentine | Cute coiled snake with a big round head and a sly smile | poison, ground, normal |
| [ ] | `body/base_eel.png` | 45x46 | main | serpentine | Rising sea-serpent with fin frills and big eyes | water, ice, electric |
| [ ] | `body/base_wyrm.png` | 43x44 | main | serpentine | Cool coiled young wyrm with whisker barbels and a proud look | dragon, ghost, ice |
| [ ] | `body/base_blob.png` | 40x33 | main | blob | Squishy mochi blob with a curl on top and a big smile | normal, grass, ice |
| [ ] | `body/base_slime.png` | 40x34 | main | blob | Drippy goo blob with a cheeky tongue-out face | poison, water, ghost |
| [ ] | `body/base_wisp.png` | 36x40 | main | blob | Floating ghost wisp with a curly tail-end and a spooky-cute grin | ghost, psychic, fire |
| [ ] | `body/base_pebble.png` | 38x32 | main | blob | Round rock lump creature with a sturdy little face and stubby feet | rock, ground, fighting |
| [ ] | `body/base_beetle.png` | 42x30 | main | insectoid | Round shiny beetle bug with a horn and stubby legs | bug, ground, rock |
| [ ] | `body/base_grub.png` | 44x27 | main | insectoid | Plump caterpillar with a big happy face and tufted segments | bug, grass, poison |
| [ ] | `body/base_bee.png` | 38x35 | main | insectoid | Fuzzy round bee/bug with tiny wings and antennae | bug, fighting, flying |
## head_top (23)
| Done | File | Size | Palette | Plans | Description | Most likely for |
|---|---|---|---|---|---|---|
| [ ] | `head_top/ears_pointed.png` | 16x14 | main | all | Pair of tall pointed ears | electric, normal, fire |
| [ ] | `head_top/ears_round.png` | 16x14 | main | all | Pair of round mouse/bear ears | normal, electric, ice |
| [ ] | `head_top/ears_long.png` | 18x20 | main | all | Long rabbit ears (drawn behind the creature) | normal, psychic, grass |
| [ ] | `head_top/ears_floppy.png` | 26x12 | main | all | Droopy hanging ears (drawn behind the creature) | normal, water, ground |
| [ ] | `head_top/ears_bolt.png` | 16x14 | main | all | Zigzag lightning-bolt ears, type-colored tips | electric, normal, fire |
| [ ] | `head_top/ears_fin.png` | 16x14 | accent | all | Fin-shaped ear frills | water, ice, dragon |
| [ ] | `head_top/horn_single.png` | 10x14 | accent | all | One forehead horn (rhino/beetle) | ground, bug, rock |
| [ ] | `head_top/horns_ram.png` | 26x14 | accent | all | Curled ram horns | ground, rock, fighting |
| [ ] | `head_top/horns_dragon.png` | 22x16 | accent | all | Swept-back dragon horns | dragon, fire, ghost |
| [ ] | `head_top/antennae.png` | 16x16 | main | all | Two thin antennae with tips | bug, electric, psychic |
| [ ] | `head_top/crest_feather.png` | 16x14 | accent | all | Swept feather crest | flying, fire, fighting |
| [ ] | `head_top/crest_flame.png` | 16x14 | type | all | Flickering flame on top of head | fire, normal, water |
| [ ] | `head_top/leaf_sprout.png` | 14x14 | type | all | Leaf/sprout growing from head | grass, normal, fire |
| [ ] | `head_top/flower_bloom.png` | 16x14 | accent | all | Small flower on head | grass, poison, psychic |
| [ ] | `head_top/mushroom_cap.png` | 20x12 | accent | all | Mushroom cap worn like a hat | poison, grass, bug |
| [ ] | `head_top/ice_crown.png` | 18x12 | type | all | Ring of small icicles | ice, normal, fire |
| [ ] | `head_top/crystal_spikes.png` | 16x14 | accent | all | Cluster of crystal shards | rock, ice, psychic |
| [ ] | `head_top/gem_forehead.png` | 8x8 | accent | all | Gem set in forehead | psychic, dragon, rock |
| [ ] | `head_top/spiky_hair.png` | 20x12 | main | all | Spiky tuft of fur/hair | electric, fighting, poison |
| [ ] | `head_top/headband.png` | 24x10 | accent | all | Tied headband with trailing ends | fighting, normal, fire |
| [ ] | `head_top/wisp_flame.png` | 16x14 | type | all | Ghostly floating flame wisp | ghost, fire, psychic |
| [ ] | `head_top/water_spout.png` | 16x14 | type | all | Water droplet/fountain crest | water, ice, normal |
| [ ] | `head_top/cloud_tuft.png` | 20x12 | type | all | Puffy cloud tuft | flying, electric, ice |
## back (22)
| Done | File | Size | Palette | Plans | Description | Most likely for |
|---|---|---|---|---|---|---|
| [ ] | `back/wings_feather.png` | 44x30 | accent | avian, biped, quadruped | Full feathered wings, spread | flying, fire, normal |
| [ ] | `back/wings_feather_small.png` | 30x14 | accent | avian, biped, quadruped, blob | Tiny stubby wings | flying, normal, ice |
| [ ] | `back/wings_bat.png` | 44x26 | accent | biped, quadruped, avian, blob, serpentine | Membrane bat wings | poison, ghost, flying |
| [ ] | `back/wings_bug.png` | 40x28 | accent | insectoid, biped, blob | Translucent veined insect wings | bug, grass, poison |
| [ ] | `back/wings_dragon.png` | 48x32 | accent | quadruped, biped, serpentine | Large clawed dragon wings | dragon, fire, flying |
| [ ] | `back/shell_round.png` | 40x26 | accent | quadruped, biped, aquatic | Domed turtle shell | water, ground, rock |
| [ ] | `back/shell_spiked.png` | 40x28 | accent | quadruped, biped, insectoid | Armored shell with spikes | rock, ground, water |
| [ ] | `back/spikes_back.png` | 36x14 | accent | quadruped, biped, serpentine, aquatic, blob | Row of spikes along the spine | ground, poison, rock |
| [ ] | `back/sail_fin.png` | 34x20 | accent | quadruped, biped, serpentine | Tall dimetrodon-style back sail | fire, dragon, water |
| [ ] | `back/dorsal_fin.png` | 20x16 | accent | aquatic, serpentine, quadruped | Shark-style dorsal fin | water, ice, dragon |
| [ ] | `back/plant_bulb.png` | 30x26 | type | quadruped, biped, blob | Large seed pod/bulb with leaves | grass, poison, normal |
| [ ] | `back/leaf_cape.png` | 38x20 | type | quadruped, biped, serpentine, insectoid | Overlapping leaves along the back | grass, bug, flying |
| [ ] | `back/flower_back.png` | 32x26 | accent | quadruped, biped, blob | Big blooming flower | grass, poison, psychic |
| [ ] | `back/mushroom_back.png` | 30x22 | accent | quadruped, insectoid, blob | Cluster of mushrooms growing on back | grass, poison, bug |
| [ ] | `back/flame_mane.png` | 36x28 | type | quadruped, biped | Mane/collar of fire | fire, dragon, normal |
| [ ] | `back/crystal_cluster.png` | 30x24 | accent | quadruped, biped, blob, serpentine | Crystals jutting from back | rock, ice, psychic |
| [ ] | `back/rock_plates.png` | 38x20 | accent | quadruped, biped, serpentine | Boulder armor plates | rock, ground, fighting |
| [ ] | `back/icicle_spikes.png` | 29x24 | type | quadruped, biped, aquatic, avian, blob | Tight fan of icicles rising from the back | ice, water, normal |
| [ ] | `back/mane_fluffy.png` | 34x26 | main | quadruped, biped | Big fluffy fur collar/mane | normal, fighting, electric |
| [ ] | `back/coil_spikes.png` | 26x24 | type | quadruped, biped, blob, insectoid | Chunky coil/battery prongs with a spark | electric, normal, fire |
| [ ] | `back/cloud_puffs.png` | 30x20 | type | blob, avian, quadruped, serpentine | Fluffy cloud hugging the back | flying, ice, psychic |
| [ ] | `back/ghost_tatters.png` | 26x24 | accent | biped, blob, serpentine, avian | Short ghostly shroud draped from the shoulders | ghost, poison, psychic |
## tail (17)
| Done | File | Size | Palette | Plans | Description | Most likely for |
|---|---|---|---|---|---|---|
| [ ] | `tail/tail_stub.png` | 10x10 | main | quadruped, biped, avian | Short round stub tail | normal, ice, ground |
| [ ] | `tail/tail_fluffy.png` | 26x20 | main | quadruped, biped | Big fluffy fox/squirrel tail | normal, ice, electric |
| [ ] | `tail/tail_long_thin.png` | 30x12 | main | quadruped, biped | Long thin tail with tuft | psychic, normal, electric |
| [ ] | `tail/tail_curly.png` | 16x16 | main | quadruped, biped | Spiral curly tail | normal, psychic, grass |
| [ ] | `tail/tail_flame_tip.png` | 28x20 | type | quadruped, biped, serpentine | Tail ending in a flame | fire, normal, water |
| [ ] | `tail/tail_lightning.png` | 26x22 | type | quadruped, biped | Zigzag lightning-bolt tail | electric, normal, fire |
| [ ] | `tail/tail_leaf.png` | 24x16 | type | quadruped, biped, insectoid, serpentine | Tail ending in a big leaf | grass, bug, normal |
| [ ] | `tail/tail_fish_fin.png` | 22x22 | accent | aquatic, serpentine, quadruped, biped | Forked fish/mermaid tail fin | water, ice, dragon |
| [ ] | `tail/tail_club.png` | 28x16 | main | quadruped, biped | Heavy club-ended tail | rock, ground, fighting |
| [ ] | `tail/tail_spiked.png` | 28x14 | main | quadruped, biped, serpentine | Tail with spikes | ground, rock, dragon |
| [ ] | `tail/tail_stinger.png` | 22x18 | accent | insectoid, serpentine, quadruped, biped | Curved stinger tail | poison, bug, normal |
| [ ] | `tail/tail_feather_fan.png` | 22x22 | accent | avian, biped | Fanned tail feathers | flying, fire, psychic |
| [ ] | `tail/tail_wispy.png` | 24x16 | main | blob, serpentine, biped, avian | Fading smoky wisp tail | ghost, psychic, fire |
| [ ] | `tail/tail_ice_shard.png` | 24x16 | type | quadruped, biped, aquatic, serpentine | Tail ending in an ice shard | ice, normal, fire |
| [ ] | `tail/tail_dragon.png` | 30x16 | main | quadruped, biped, serpentine | Thick tapered tail with small fin | dragon, fire, ground |
| [ ] | `tail/tail_rattle.png` | 14x12 | accent | serpentine | Rattlesnake rattle tip | poison, ground, normal |
| [ ] | `tail/tail_crystal.png` | 24x16 | accent | quadruped, biped, serpentine | Tail tipped with a crystal | psychic, rock, ice |
## aura (14)
| Done | File | Size | Palette | Plans | Description | Most likely for |
|---|---|---|---|---|---|---|
| [ ] | `aura/aura_embers.png` | 84x84 | type | all | Floating embers | fire |
| [ ] | `aura/aura_sparks.png` | 84x84 | type | all | Crackling sparks | electric |
| [ ] | `aura/aura_bubbles.png` | 84x84 | type | all | Rising bubbles | water, poison |
| [ ] | `aura/aura_leaves.png` | 84x84 | type | all | Drifting leaves | grass, flying, bug |
| [ ] | `aura/aura_snowflakes.png` | 84x84 | type | all | Snowflakes | ice |
| [ ] | `aura/aura_wisps.png` | 84x84 | type | all | Ghost wisps | ghost, psychic |
| [ ] | `aura/aura_rings.png` | 84x84 | type | all | Psychic rings/ripples | psychic |
| [ ] | `aura/aura_dust.png` | 84x84 | type | all | Dust cloud at the feet | ground, rock, fighting |
| [ ] | `aura/aura_toxic.png` | 84x84 | type | all | Toxic bubbles and drips | poison |
| [ ] | `aura/aura_sparkle.png` | 84x84 | type | all | Twinkling sparkles | dragon, normal, psychic |
| [ ] | `aura/aura_gusts.png` | 84x84 | type | all | Wind swirls | flying, dragon |
| [ ] | `aura/aura_pebbles.png` | 84x84 | type | all | Floating pebbles | rock, ground, psychic |
| [ ] | `aura/aura_power.png` | 84x84 | type | all | Power-up energy lines | fighting |
| [ ] | `aura/aura_spores.png` | 84x84 | type | all | Floating spores | bug, grass, poison |
## Simulated pick rates per type
From 3,000 random seeds per type. Use this to spot parts that never show up or show up too much.
### Normal
- **Body plan:** quadruped 41%, biped 32%, avian 15%, blob 8%, serpentine 4%
- **body:** base_pup 18%, base_bunny 14%, base_imp 12%, base_chick 9%, base_fox 8%, base_cub 8%, base_blob 7%, base_fawn 4%
- **head_top:** none 61%, spiky_hair 9%, horns_ram 8%, headband 6%, horn_single 3%, ears_round 2%, ears_floppy 2%, flower_bloom 1%
- **back:** none 47%, mane_fluffy 17%, wings_feather_small 13%, wings_feather 8%, cloud_puffs 6%, coil_spikes 2%, spikes_back 2%
- **tail:** none 36%, tail_stub 23%, tail_fluffy 15%, tail_curly 12%, tail_long_thin 7%, tail_feather_fan 3%, tail_rattle 1%
- **aura:** none 100%
- **markings:** none 42%, belly 31%, spots 15%, speckles 12%
### Fire
- **Body plan:** quadruped 43%, biped 26%, avian 12%, serpentine 11%, blob 8%
- **body:** base_dino 18%, base_pup 11%, base_salamander 11%, base_fox 10%, base_drake 10%, base_hawk 9%, base_wisp 5%, base_wyrm 4%
- **head_top:** crest_flame 53%, none 15%, wisp_flame 10%, horns_dragon 5%, spiky_hair 5%, crest_feather 4%, ears_pointed 2%, horn_single 1%
- **back:** none 34%, flame_mane 19%, sail_fin 14%, wings_dragon 10%, spikes_back 7%, wings_feather 6%, mane_fluffy 5%, wings_feather_small 2%
- **tail:** none 68%, tail_flame_tip 15%, tail_feather_fan 5%, tail_dragon 3%, tail_wispy 3%, tail_stub 2%, tail_fluffy 2%, tail_long_thin 1%
- **aura:** none 100%
- **markings:** none 58%, belly 29%, spots 9%, speckles 5%
### Water
- **Body plan:** aquatic 43%, serpentine 22%, quadruped 16%, biped 12%, blob 8%
- **body:** base_eel 19%, base_guppy 16%, base_otter 14%, base_axolotl 12%, base_salamander 8%, base_slime 6%, base_dino 6%, base_pup 4%
- **head_top:** water_spout 48%, none 26%, ears_fin 11%, horn_single 3%, spiky_hair 2%, horns_ram 1%, horns_dragon 1%, flower_bloom 1%
- **back:** none 41%, dorsal_fin 37%, icicle_spikes 6%, shell_round 6%, sail_fin 4%, shell_spiked 2%, spikes_back 1%, wings_feather_small 1%
- **tail:** none 72%, tail_fish_fin 14%, tail_dragon 7%, tail_rattle 1%
- **aura:** none 100%
- **markings:** none 48%, spots 25%, belly 23%, speckles 4%
### Grass
- **Body plan:** quadruped 38%, biped 28%, blob 18%, insectoid 9%, serpentine 8%
- **body:** base_fawn 19%, base_bunny 15%, base_blob 14%, base_cub 9%, base_pixie 9%, base_grub 8%, base_noodle 6%, base_salamander 4%
- **head_top:** leaf_sprout 42%, flower_bloom 26%, mushroom_cap 15%, none 10%, ears_long 1%
- **back:** leaf_cape 31%, plant_bulb 29%, flower_back 17%, none 9%, mushroom_back 9%, wings_bug 4%
- **tail:** none 87%, tail_leaf 11%, tail_curly 1%
- **aura:** none 100%
- **markings:** none 48%, spots 37%, belly 12%, speckles 4%
### Electric
- **Body plan:** quadruped 38%, biped 28%, blob 18%, insectoid 9%, avian 8%
- **body:** base_fox 26%, base_imp 22%, base_blob 12%, base_bee 6%, base_chick 5%, base_drake 2%, base_fawn 2%, base_pup 2%
- **head_top:** none 23%, cloud_tuft 23%, spiky_hair 22%, antennae 15%, ears_pointed 4%, horn_single 2%, ears_bolt 2%, ears_round 2%
- **back:** coil_spikes 33%, none 33%, mane_fluffy 10%, cloud_puffs 9%, wings_bug 5%, sail_fin 4%, wings_feather_small 2%, wings_feather 1%
- **tail:** none 73%, tail_lightning 13%, tail_long_thin 5%, tail_fluffy 4%, tail_stub 2%, tail_curly 1%
- **aura:** none 100%
- **markings:** none 54%, spots 29%, belly 14%, speckles 4%
### Ice
- **Body plan:** quadruped 28%, aquatic 19%, biped 18%, avian 13%, blob 13%, serpentine 9%
- **body:** base_fawn 14%, base_otter 12%, base_bunny 12%, base_blob 10%, base_fox 9%, base_penguin 8%, base_guppy 7%, base_eel 6%
- **head_top:** ice_crown 52%, none 13%, cloud_tuft 10%, crystal_spikes 10%, water_spout 5%, horn_single 1%, ears_fin 1%, ears_round 1%
- **back:** icicle_spikes 50%, none 18%, crystal_cluster 12%, spikes_back 6%, cloud_puffs 5%, dorsal_fin 4%, mane_fluffy 2%, wings_feather_small 1%
- **tail:** none 77%, tail_ice_shard 14%, tail_stub 3%, tail_crystal 2%, tail_fluffy 1%
- **aura:** none 100%
- **markings:** none 49%, belly 24%, speckles 20%, spots 6%
### Fighting
- **Body plan:** biped 75%, quadruped 20%, insectoid 5%
- **body:** base_brawler 61%, base_cub 10%, base_pup 7%, base_dino 4%, base_bee 4%, base_bunny 4%, base_imp 3%, base_pixie 3%
- **head_top:** headband 38%, none 20%, horns_ram 16%, horn_single 15%, spiky_hair 7%
- **back:** none 42%, mane_fluffy 23%, shell_spiked 11%, rock_plates 10%, spikes_back 2%, shell_round 2%, wings_feather 2%, wings_feather_small 2%
- **tail:** none 72%, tail_club 12%, tail_stub 4%, tail_long_thin 3%, tail_fluffy 3%, tail_curly 2%, tail_spiked 1%
- **aura:** none 100%
- **markings:** none 58%, belly 29%, spots 9%, speckles 5%
### Poison
- **Body plan:** blob 32%, serpentine 24%, quadruped 15%, biped 15%, insectoid 14%
- **body:** base_slime 27%, base_noodle 21%, base_salamander 10%, base_grub 10%, base_pixie 9%, base_beetle 3%, base_wisp 2%, base_dino 2%
- **head_top:** mushroom_cap 29%, none 19%, flower_bloom 15%, horn_single 12%, spiky_hair 12%, antennae 5%, ears_pointed 1%, horns_ram 1%
- **back:** none 30%, spikes_back 15%, mushroom_back 11%, plant_bulb 10%, flower_back 10%, wings_bat 10%, wings_bug 8%, ghost_tatters 2%
- **tail:** none 72%, tail_stinger 13%, tail_rattle 7%, tail_spiked 4%, tail_long_thin 2%
- **aura:** none 100%
- **markings:** none 48%, spots 37%, belly 12%, speckles 4%
### Ground
- **Body plan:** quadruped 48%, biped 29%, serpentine 18%, insectoid 4%
- **body:** base_cub 32%, base_noodle 16%, base_brawler 15%, base_dino 10%, base_drake 7%, base_beetle 3%, base_salamander 2%, base_fawn 2%
- **head_top:** horn_single 35%, none 34%, horns_ram 18%, spiky_hair 2%, flower_bloom 2%, mushroom_cap 1%, leaf_sprout 1%
- **back:** spikes_back 26%, none 22%, rock_plates 19%, shell_spiked 15%, shell_round 14%
- **tail:** none 62%, tail_club 13%, tail_spiked 12%, tail_stub 3%, tail_dragon 3%, tail_rattle 3%
- **aura:** none 100%
- **markings:** none 42%, speckles 33%, belly 20%, spots 5%
### Flying
- **Body plan:** avian 71%, insectoid 10%, quadruped 10%, serpentine 9%
- **body:** base_hawk 35%, base_owl 21%, base_chick 14%, base_bee 8%, base_eel 3%, base_wyrm 3%, base_noodle 3%, base_drake 2%
- **head_top:** cloud_tuft 51%, none 35%, crest_feather 5%, flower_bloom 2%, leaf_sprout 1%, horn_single 1%
- **back:** cloud_puffs 46%, wings_feather_small 24%, none 15%, wings_feather 9%, leaf_cape 4%
- **tail:** none 72%, tail_feather_fan 24%, tail_stub 2%
- **aura:** none 100%
- **markings:** none 69%, belly 17%, spots 9%, speckles 5%
### Psychic
- **Body plan:** biped 39%, quadruped 29%, blob 18%, avian 14%
- **body:** base_pixie 18%, base_wisp 14%, base_fox 13%, base_fawn 12%, base_owl 12%, base_bunny 9%, base_imp 9%, base_cub 2%
- **head_top:** gem_forehead 44%, none 14%, flower_bloom 12%, antennae 7%, wisp_flame 6%, crystal_spikes 4%, ears_long 3%, ears_pointed 2%
- **back:** none 31%, crystal_cluster 28%, cloud_puffs 9%, flower_back 7%, ghost_tatters 6%, wings_bug 5%, wings_bat 5%, wings_feather_small 3%
- **tail:** none 72%, tail_crystal 6%, tail_long_thin 6%, tail_curly 5%, tail_wispy 4%, tail_fluffy 3%, tail_stub 2%, tail_feather_fan 1%
- **aura:** none 100%
- **markings:** none 69%, belly 17%, spots 9%, speckles 5%
### Bug
- **Body plan:** insectoid 83%, serpentine 11%, avian 6%
- **body:** base_grub 29%, base_beetle 28%, base_bee 26%, base_eel 4%, base_noodle 4%, base_wyrm 3%, base_penguin 2%, base_hawk 1%
- **head_top:** antennae 31%, mushroom_cap 22%, none 22%, horn_single 20%
- **back:** wings_bug 31%, none 29%, mushroom_back 21%, leaf_cape 18%
- **tail:** none 86%, tail_stinger 9%, tail_leaf 4%
- **aura:** none 100%
- **markings:** none 54%, spots 29%, belly 14%, speckles 4%
### Rock
- **Body plan:** quadruped 32%, biped 31%, blob 19%, serpentine 19%
- **body:** base_brawler 22%, base_cub 16%, base_pebble 16%, base_drake 10%, base_wyrm 7%, base_noodle 6%, base_eel 6%, base_pixie 2%
- **head_top:** none 23%, horn_single 22%, horns_ram 17%, gem_forehead 17%, crystal_spikes 12%, spiky_hair 2%
- **back:** crystal_cluster 27%, rock_plates 19%, spikes_back 18%, none 15%, shell_spiked 11%, shell_round 6%, wings_dragon 1%
- **tail:** none 73%, tail_spiked 9%, tail_crystal 8%, tail_club 7%, tail_stub 1%
- **aura:** none 100%
- **markings:** none 46%, speckles 36%, belly 11%, spots 6%
### Ghost
- **Body plan:** blob 65%, serpentine 20%, biped 10%, avian 5%
- **body:** base_wisp 50%, base_wyrm 17%, base_slime 10%, base_pixie 6%, base_owl 4%, base_pebble 3%, base_blob 3%, base_imp 2%
- **head_top:** wisp_flame 44%, none 21%, mushroom_cap 8%, gem_forehead 7%, horns_dragon 6%, ears_pointed 3%, ears_round 2%, horn_single 1%
- **back:** ghost_tatters 40%, none 29%, wings_bat 26%, mushroom_back 3%
- **tail:** none 84%, tail_wispy 14%
- **aura:** none 100%
- **markings:** none 69%, belly 17%, spots 9%, speckles 5%
### Dragon
- **Body plan:** serpentine 39%, quadruped 29%, biped 27%, avian 4%
- **body:** base_wyrm 25%, base_dino 21%, base_drake 18%, base_eel 12%, base_salamander 8%, base_hawk 2%, base_brawler 2%, base_imp 2%
- **head_top:** none 30%, gem_forehead 29%, horns_dragon 23%, horn_single 7%, ears_fin 2%, spiky_hair 2%, flower_bloom 2%
- **back:** none 28%, wings_dragon 25%, sail_fin 14%, spikes_back 14%, wings_bat 10%, dorsal_fin 3%, flame_mane 1%
- **tail:** none 57%, tail_dragon 23%, tail_spiked 11%, tail_fish_fin 3%, tail_stub 2%
- **aura:** none 100%
- **markings:** none 58%, belly 29%, spots 9%, speckles 5%

172
build_parts.py Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Compile text-grid part sources into palette-key PNGs.
Sources live in parts_src/<slot>/<name>.txt and compile to parts/<slot>/<name>.png.
Every non-blank line is one pixel row; lines starting with '#' are comments. All rows
must be the same width.
Legend
. transparent o outline
1 main_light 2 main_base 3 main_dark
a accent_light b accent_base c accent_dark
x type_light y type_base z type_dark
w white p pupil
P pivot T tail B back A aura ^ head_top
Layout sources (parts_src/<slot>/<name>.layout) assemble stamps onto a fixed canvas,
used for the 84x84 auras:
size 84 84
stamp <stamp-name> <x> <y> [flip] # parts_src/<slot>/stamps/<stamp-name>.txt, top-left at x,y
marker <char> <x> <y> # e.g. "marker P 42 42"
Later stamps paint over earlier ones; '.' in a stamp never paints.
Usage
python build_parts.py # build everything
python build_parts.py body/quad_slim # build some parts
python build_parts.py mirror FILE # print FILE + its horizontal mirror (shared center column)
python build_parts.py mirror FILE --gap N # mirror with N transparent columns between halves
python build_parts.py flip FILE # print FILE flipped horizontally
"""
import os
import sys
from pathlib import Path
from PIL import Image
from generate_sprite import KEYS, MARKERS, PUPIL, EYE_WHITE
# PARTS_SRC / PARTS_DIR let experiments build into a separate library (e.g. proto/).
SRC = Path(os.environ.get("PARTS_SRC", "parts_src"))
OUT = Path(os.environ.get("PARTS_DIR", "parts"))
LEGEND = {
"o": KEYS["outline"],
"1": KEYS["main_light"], "2": KEYS["main_base"], "3": KEYS["main_dark"],
"a": KEYS["accent_light"], "b": KEYS["accent_base"], "c": KEYS["accent_dark"],
"x": KEYS["type_light"], "y": KEYS["type_base"], "z": KEYS["type_dark"],
"w": EYE_WHITE, "p": PUPIL,
"P": MARKERS["pivot"], "T": MARKERS["tail"], "B": MARKERS["back"],
"A": MARKERS["aura"], "^": MARKERS["head_top"],
}
class SourceError(Exception):
pass
def read_grid(path):
rows = []
for n, line in enumerate(Path(path).read_text().splitlines(), 1):
line = line.rstrip()
if not line or line.startswith("#"):
continue
bad = sorted({c for c in line if c != "." and c not in LEGEND})
if bad:
raise SourceError(f"{path}:{n}: unknown characters {bad}")
rows.append(line)
if not rows:
raise SourceError(f"{path}: empty grid")
widths = {len(r) for r in rows}
if len(widths) != 1:
for n, r in enumerate(rows):
if len(r) != len(rows[0]):
raise SourceError(f"{path}: row {n} is {len(r)} wide, row 0 is {len(rows[0])}")
return rows
def paint(canvas, rows, x0, y0, flip=False):
for y, row in enumerate(rows):
if flip:
row = row[::-1]
for x, c in enumerate(row):
if c == ".":
continue
px, py = x0 + x, y0 + y
if not (0 <= px < len(canvas[0]) and 0 <= py < len(canvas)):
raise SourceError(f"stamp pixel ({px},{py}) outside canvas")
canvas[py][px] = c
def read_layout(path):
path = Path(path)
canvas = None
for n, line in enumerate(path.read_text().splitlines(), 1):
parts = line.split("#")[0].split()
if not parts:
continue
cmd, args = parts[0], parts[1:]
try:
if cmd == "size":
w, h = int(args[0]), int(args[1])
canvas = [["."] * w for _ in range(h)]
elif cmd == "stamp":
rows = read_grid(path.parent / "stamps" / f"{args[0]}.txt")
paint(canvas, rows, int(args[1]), int(args[2]), flip=len(args) > 3 and args[3] == "flip")
elif cmd == "marker":
paint(canvas, [args[0]], int(args[1]), int(args[2]))
else:
raise SourceError(f"unknown command {cmd!r}")
except (IndexError, ValueError, TypeError) as e:
raise SourceError(f"{path}:{n}: {e}")
if canvas is None:
raise SourceError(f"{path}: missing 'size'")
return ["".join(r) for r in canvas]
def to_image(rows):
h, w = len(rows), len(rows[0])
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
px = img.load()
for y, row in enumerate(rows):
for x, c in enumerate(row):
if c != ".":
px[x, y] = LEGEND[c] + (255,)
return img
def sources():
for p in sorted(SRC.glob("*/*")):
if p.suffix in (".txt", ".layout"):
yield p
def build(path):
rows = read_layout(path) if path.suffix == ".layout" else read_grid(path)
out = OUT / path.parent.name / f"{path.stem}.png"
out.parent.mkdir(parents=True, exist_ok=True)
to_image(rows).save(out)
return out, len(rows[0]), len(rows)
def main(argv):
if argv and argv[0] in ("mirror", "flip"):
rows = read_grid(argv[1])
if argv[0] == "flip":
print("\n".join(r[::-1] for r in rows))
else:
gap = int(argv[3]) if len(argv) > 3 and argv[2] == "--gap" else None
for r in rows:
print(r + "." * gap + r[::-1] if gap is not None else r + r[-2::-1])
return 0
wanted = set(argv)
errors = 0
built = 0
for path in sources():
key = f"{path.parent.name}/{path.stem}"
if wanted and key not in wanted:
continue
try:
out, w, h = build(path)
built += 1
if wanted:
print(f"{out} {w}x{h}")
except SourceError as e:
print(f"ERROR {e}", file=sys.stderr)
errors += 1
print(f"built {built} part(s), {errors} error(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

76
business_pokemon_type.py Normal file
View File

@@ -0,0 +1,76 @@
import os
import sys
import json
import requests
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
if not OPENROUTER_API_KEY:
sys.exit("Set OPENROUTER_API_KEY first: export OPENROUTER_API_KEY='sk-or-v1-...'")
# All 15 Gen 1 types, tuned for home service businesses. Each trade is listed under
# exactly one type, and Rock is limited to stone/roofing work so "construction-y"
# businesses spread out by what they actually do.
POKEMON_TYPES = {
"normal": "General or multi-trade home services: general contractors, remodeling, handymen, painters, drywall, flooring, cabinets, house cleaning and maid services, home inspectors",
"fire": "Heat and engines: heating and furnace repair, fireplaces, chimney sweeps, outdoor kitchens and grills, welding, auto repair and mechanics",
"water": "Water: plumbing, leak detection, water heaters, water damage restoration, pools and spas, irrigation and sprinklers, pressure washing, car washes and auto detailing",
"grass": "Plants and yards: landscaping, lawn care and mowing, tree service, sod, gardening, nurseries, mulch",
"electric": "Electricity and tech: electricians, lighting, solar, generators, EV chargers, smart home, home theater, alarm and camera systems, appliance repair",
"ice": "Cooling: air conditioning repair and install, refrigeration, insulation, ice machines, cold storage",
"fighting": "Protection and heavy labor: fencing and gates, security, demolition, moving companies, hauling, furniture assembly",
"poison": "Waste and contaminants: junk removal, dumpster rental, septic, mold remediation, asbestos and lead abatement, hazardous waste",
"ground": "Earth and dirt work: foundation repair, excavation, grading, drainage and French drains, concrete, driveways, paving, patios and hardscaping",
"flying": "Air and high places: gutters, air duct cleaning, dryer vents, indoor air quality, window cleaning, drones, couriers",
"psychic": "Knowledge and planning: interior design, architects, real estate, property management, home warranties, insurance, consulting, marketing",
"bug": "Insects and critters: pest control, exterminators, termite treatment, wildlife and rodent removal, beekeeping",
"rock": "Stone only: roofing, masonry, brick, stone veneer, granite and quartz countertops, tile, retaining walls",
"ghost": "Hidden spaces and the unseen: attics, crawl spaces, basement waterproofing, locksmiths, estate cleanouts, trauma and crime scene cleanup, funeral homes",
"dragon": "Premium or rare: luxury custom homes, high-end remodels, custom pools, exotic or performance auto shops, elite luxury brands",
}
def get_business_type(description: str) -> dict:
response = requests.post(
url="https://openrouter.ai/api/alpha/decisions",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
},
data=json.dumps({
"model": "typesafe/jev-1.13",
"state": f"Business description: {description}",
"questions": {
"pokemon_type": {
"type": "choice",
"instructions": (
"Which Gen 1 Pokémon type best matches this business? Decide by its "
"specific trade or main service, not by the fact that it is construction, "
"home service, or physical work in general. Being construction-related is "
"not a reason to choose Rock; choose Rock only for roofing or stone work. "
"If the description is only a business name, use the trade the name implies."
),
"criteria": POKEMON_TYPES,
}
},
}),
timeout=30,
)
response.raise_for_status()
return response.json()["answers"]["pokemon_type"]
if __name__ == "__main__":
desc = " ".join(sys.argv[1:]) or "Residential lawn mowing, hedge trimming, and flower bed installation"
answer = get_business_type(desc)
choice = answer["choice"]
probs = answer["probabilities"]
top3 = sorted(probs.items(), key=lambda kv: kv[1], reverse=True)[:3]
print(f"Business: {desc}")
print(f"Type: {choice.upper()} ({probs[choice]:.0%})")
print("Top 3: " + ", ".join(f"{t} {p:.0%}" for t, p in top3))
# Low confidence usually means a vague description or a true dual-type business
if probs[choice] < 0.5:
print(f"Low confidence. Possible dual type: {top3[0][0]}/{top3[1][0]}")

729
generate_sprite.py Normal file
View File

@@ -0,0 +1,729 @@
#!/usr/bin/env python3
"""
Generate a Pokémon-style sprite for a business.
Pipeline: Jev picks a type -> seeded RNG picks a base creature and its variation parts
(weighted by type) -> parts snap together via anchor pixels -> markings painted on ->
palette keys swapped for colors pulled from the business logo.
Logo colors: transparent pixels, anti-aliased edges and a solid white/black/gray background
(the colour filling the image border) are ignored; hued brand colours win over white, black
or gray text. A logo that is only white/black (or only its background) still uses that.
Usage
python generate_sprite.py "Rosas Landscaping" logos/rosas.png # Jev picks the type
python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass
python generate_sprite.py "Rosas Landscaping" --type grass --colors "#2E7D32,#FBC02D"
python generate_sprite.py --batch businesses.csv # columns: name,logo,type,description
python generate_sprite.py --make-placeholders # test art for every part
python generate_sprite.py --sprite-list SPRITES.md # art checklist + weight report
Requires: pip install pillow numpy requests
"""
import argparse
import colorsys
import csv
import hashlib
import json
import random
import re
import sys
from collections import Counter
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
import sprite_parts as cfg
CANVAS = 96 # final sprite size (square)
GROUND_MARGIN = 4 # empty rows under the feet
# Palette keys: draw every part using ONLY these exact colors (plus pass-through colors
# like eye white). They get swapped for logo-derived colors at render time.
KEYS = {
"main_light": (255, 128, 128), "main_base": (255, 0, 0), "main_dark": (128, 0, 0),
"accent_light": (128, 255, 128), "accent_base": (0, 255, 0), "accent_dark": (0, 128, 0),
"type_light": (128, 128, 255), "type_base": (0, 0, 255), "type_dark": (0, 0, 128),
"outline": (0, 0, 0),
}
PUPIL = (20, 20, 20) # near-black that is NOT recolored
EYE_WHITE = (255, 255, 255)
# Anchor marker pixels. Place one pixel of the color where a child part attaches.
# They are read, then replaced with the surrounding color.
MARKERS = {
"pivot": (255, 0, 255), "tail": (253, 0, 255), "back": (252, 0, 255),
"aura": (251, 0, 255), "head_top": (250, 0, 255),
}
PICK_ORDER = ["body", "head_top", "back", "tail", "aura"]
DEFAULT_Z = {"aura": 0, "back": 10, "tail": 20, "body": 30, "head_top": 50}
def warn(msg):
print(f" ! {msg}", file=sys.stderr)
# ============================================================ seeded picking
def make_rng(name, reroll=0):
key = f"{name.strip().lower()}|{reroll}"
return random.Random(int(hashlib.sha256(key.encode()).hexdigest()[:16], 16))
def weighted_pick(rng, items):
items = [(it, w) for it, w in items if w > 0]
total = sum(w for _, w in items)
if total <= 0:
return None
r = rng.random() * total
acc = 0.0
for it, w in items:
acc += w
if r < acc:
return it
return items[-1][0]
def part_weight(p, ptype):
return p["types"].get(ptype, p["other"])
def candidates(slot, plan, ptype, tags, excluded, denied=()):
out = []
for p in cfg.SLOTS[slot]:
if plan not in p["plans"] or p["name"] in denied:
continue
if p["requires"] and not set(p["requires"]) & tags:
continue
if set(p["tags"]) & excluded or set(p["excludes"]) & tags:
continue
out.append((p, part_weight(p, ptype)))
return out
def motif_factor(p, motifs):
"""Keep one creature's features on a theme: same motif boosted, clashing motifs damped."""
m = cfg.MOTIFS.get(p["name"])
if m is None or not motifs:
return 1.0
if m in motifs:
return cfg.MOTIF_SAME
if all(frozenset((m, o)) in cfg.MOTIF_PAIRS for o in motifs):
return 1.0
return cfg.MOTIF_CLASH
def pick_parts(ptype, rng):
plan = weighted_pick(rng, cfg.PLAN_WEIGHTS[ptype].items())
chosen, tags, excluded, denied, motifs = {}, {f"plan:{plan}"}, set(), set(), set()
features = 0
for slot in PICK_ORDER:
if slot in cfg.DISABLED_SLOTS:
continue
cands = candidates(slot, plan, ptype, tags, excluded, denied)
if slot in cfg.FEATURE_SLOTS:
if features >= cfg.MAX_FEATURES:
cands = [(p, w) for p, w in cands if p["name"] == "none"]
cands = [(p, w * motif_factor(p, motifs)) for p, w in cands]
p = weighted_pick(rng, cands)
if p is None:
continue
chosen[slot] = p
tags |= set(p["tags"])
excluded |= set(p["excludes"])
denied |= set(p.get("deny", ()))
if p["name"] != "none" and slot in cfg.FEATURE_SLOTS:
features += 1
if cfg.MOTIFS.get(p["name"]):
motifs.add(cfg.MOTIFS[p["name"]])
markings = weighted_pick(rng, [(m, w.get(ptype, w.get("_other", 0))) for m, w in cfg.MARKINGS.items()])
return plan, chosen, markings, tags
def attributes_from(tags):
"""Public attributes: drop compatibility plumbing (*_head family tags, _internal tags, plan)."""
return sorted(t for t in tags if not t.endswith("_head") and not t.startswith(("_", "plan:")))
# ============================================================ loading + compositing
_part_cache = {}
def load_part(parts_dir, slot, name):
path = Path(parts_dir) / slot / f"{name}.png"
if path in _part_cache:
return _part_cache[path]
if not path.exists():
_part_cache[path] = None
return None
arr = np.array(Image.open(path).convert("RGBA"))
rgb, alpha = arr[:, :, :3], arr[:, :, 3] > 0
anchors, marker_mask = {}, np.zeros(alpha.shape, bool)
for mname, col in MARKERS.items():
m = np.all(rgb == col, axis=-1) & alpha
ys, xs = np.nonzero(m)
if len(xs):
anchors[mname] = (int(round(xs.mean())), int(round(ys.mean())))
marker_mask |= m
# Replace marker pixels with the most common opaque, non-marker 4-neighbor
h, w = alpha.shape
for y, x in zip(*np.nonzero(marker_mask)):
neigh = [tuple(arr[ny, nx]) for ny, nx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1))
if 0 <= ny < h and 0 <= nx < w and arr[ny, nx, 3] > 0 and not marker_mask[ny, nx]]
arr[y, x] = Counter(neigh).most_common(1)[0][0] if neigh else (0, 0, 0, 0)
result = (Image.fromarray(arr), anchors)
_part_cache[path] = result
return result
def key_mask(arr, key):
return np.all(arr[:, :, :3] == KEYS[key], axis=-1) & (arr[:, :, 3] > 0)
def apply_markings(img, kind, rng, center=None):
"""Paint accent markings onto main-ramp pixels.
center: the body's aura anchor (torso centre). When given, markings stay on the body
below the face (rows from a little above the torso centre down) and the belly patch
is centred there."""
if kind in (None, "none"):
return img
arr = np.array(img)
h, w = arr.shape[:2]
main = key_mask(arr, "main_light") | key_mask(arr, "main_base") | key_mask(arr, "main_dark")
ys, xs = np.mgrid[0:h, 0:w]
if center:
main &= ys >= center[1] - h * 0.12
if not main.any():
return img
if kind == "spots":
pat = np.zeros((h, w), bool)
pts = np.argwhere(main)
for _ in range(rng.randint(4, 8)):
y, x = pts[rng.randrange(len(pts))]
r = rng.randint(1, 3)
pat |= (xs - x) ** 2 + (ys - y) ** 2 <= r * r
elif kind == "speckles":
pat = np.random.default_rng(rng.randrange(2 ** 32)).random((h, w)) < 0.1
elif kind == "belly":
oy, ox = np.nonzero(arr[:, :, 3] > 0)
x0, x1, y0, y1 = ox.min(), ox.max(), oy.min(), oy.max()
cx, cy = center if center else ((x0 + x1) / 2, y0 + (y1 - y0) * 0.62)
rx, ry = max((x1 - x0) * 0.2, 1), max((y1 - y0) * 0.17, 1)
pat = ((xs - cx) / rx) ** 2 + ((ys - cy) / ry) ** 2 <= 1
else:
return img
mask = pat & main
for src, dst in (("main_light", "accent_light"), ("main_base", "accent_base"), ("main_dark", "accent_dark")):
arr[mask & key_mask(arr, src), :3] = KEYS[dst]
return Image.fromarray(arr)
def outline_behind(canvas, body, pos):
"""Where parts drawn behind the body (back, tail, rear ears) touch the body's silhouette,
add a second outline pixel so the creature's own shape stays readable against them."""
arr = np.array(canvas)
h, w = arr.shape[:2]
m = np.zeros((h, w), bool)
x, y = pos
m[y:y + body.height, x:x + body.width] = np.array(body)[:, :, 3] > 0
ring = np.zeros_like(m)
ring[1:] |= m[:-1]
ring[:-1] |= m[1:]
ring[:, 1:] |= m[:, :-1]
ring[:, :-1] |= m[:, 1:]
hit = ring & ~m & (arr[:, :, 3] > 0)
arr[hit] = KEYS["outline"] + (255,)
return Image.fromarray(arr)
def compose(parts_dir, chosen, markings, rng):
body = chosen["body"]
loaded = load_part(parts_dir, "body", body["name"])
if loaded is None:
raise FileNotFoundError(f"Missing body sprite: {parts_dir}/body/{body['name']}.png")
bimg, banch = loaded
bimg = apply_markings(bimg, markings, rng, banch.get("aura"))
bpiv = banch.get("pivot", (bimg.width // 2, bimg.height - 1))
bpos = (-bpiv[0], -bpiv[1])
layers = [(body["z"] or DEFAULT_Z["body"], 0, bimg, bpos)]
for i, slot in enumerate(PICK_ORDER[1:], 1):
p = chosen.get(slot)
if not p or p["name"] == "none":
continue
loaded = load_part(parts_dir, slot, p["name"])
if loaded is None:
warn(f"missing sprite {slot}/{p['name']}.png, skipped")
continue
img, anch = loaded
a = banch.get(slot)
if a is None:
warn(f"no '{slot}' anchor on {body['name']}, {p['name']} skipped")
continue
piv = anch.get("pivot", (img.width // 2, img.height // 2))
pos = (bpos[0] + a[0] - piv[0], bpos[1] + a[1] - piv[1])
layers.append((p["z"] if p["z"] is not None else DEFAULT_Z[slot], i, img, pos))
layers.sort(key=lambda l: (l[0], l[1]))
minx = min(l[3][0] for l in layers)
miny = min(l[3][1] for l in layers)
maxx = max(l[3][0] + l[2].width for l in layers)
maxy = max(l[3][1] + l[2].height for l in layers)
canvas = Image.new("RGBA", (maxx - minx, maxy - miny), (0, 0, 0, 0))
for _, i, img, (x, y) in layers:
if i == 0:
canvas = outline_behind(canvas, img, (x - minx, y - miny))
canvas.alpha_composite(img, (x - minx, y - miny))
return canvas.crop(canvas.getbbox())
def place_on_canvas(sprite):
w, h = sprite.size
size = max(CANVAS, w + 2, h + GROUND_MARGIN + 1)
if size > CANVAS:
warn(f"sprite is {w}x{h}, canvas expanded to {size}px")
out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
out.alpha_composite(sprite, ((size - w) // 2, size - GROUND_MARGIN - h))
return out
def recolor(img, palette):
arr = np.array(img)
masks = {k: key_mask(arr, k) for k in palette} # compute all before writing
for k, rgb in palette.items():
arr[masks[k], :3] = rgb
return Image.fromarray(arr)
# ============================================================ colors
def hex_to_rgb(h):
h = h.strip().lstrip("#")
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
def rgb_to_hex(c):
return "#{:02X}{:02X}{:02X}".format(*c)
def to_hls(rgb):
return colorsys.rgb_to_hls(*(c / 255 for c in rgb))
def from_hls(h, l, s):
return tuple(int(round(c * 255)) for c in colorsys.hls_to_rgb(h % 1, min(max(l, 0), 1), min(max(s, 0), 1)))
def hue_toward(h, target, amount):
d = ((target - h + 0.5) % 1.0) - 0.5
return (h + max(-amount, min(amount, d))) % 1.0
def color_dist(a, b):
rmean = (a[0] + b[0]) / 2
dr, dg, db = a[0] - b[0], a[1] - b[1], a[2] - b[2]
return ((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db) ** 0.5
def rotate_hue(rgb, amount):
h, l, s = to_hls(rgb)
return from_hls(h + amount, l, s)
def ramp(rgb):
"""light/base/dark with hue shifting: highlights lean yellow, shadows lean blue."""
h, l, s = to_hls(rgb)
l = min(max(l, 0.25), 0.75)
light = from_hls(hue_toward(h, 1 / 6, 0.03), min(l + 0.17, 0.95), s * 0.92)
base = from_hls(h, l, s)
dark = from_hls(hue_toward(h, 2 / 3, 0.035), max(l - 0.2, 0.06), s * 1.08)
return light, base, dark
def outline_color(rgb):
h, l, s = to_hls(rgb)
return from_hls(hue_toward(h, 2 / 3, 0.05), min(l * 0.35, 0.16), s * 0.8)
def _dist_to(px, rgb):
"""color_dist from many pixels (N,3 float) to one colour."""
r, g, b = rgb
rmean = (px[:, 0] + r) / 2
dr, dg, db = px[:, 0] - r, px[:, 1] - g, px[:, 2] - b
return np.sqrt((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db)
def is_neutral(rgb):
"""White, black or gray: carries no brand hue."""
mx, mn = max(rgb), min(rgb)
light, chroma = (mx + mn) / 510, (mx - mn) / 255
return chroma < 0.12 or light > 0.93 or light < 0.08
def logo_background(arr):
"""The solid background colour of a logo, if its border is mostly one opaque colour."""
h, w = arr.shape[:2]
border = np.concatenate([arr[0], arr[-1], arr[:, 0], arr[:, -1]])
opaque = border[border[:, 3] >= 200][:, :3].astype(float)
if len(opaque) < 0.4 * len(border):
return None # transparent or busy border: no solid background
best = None
for c in opaque[:: max(1, len(opaque) // 64)]:
share = np.mean(_dist_to(opaque, c) < 40)
if best is None or share > best[1]:
best = (c, share)
if best[1] < 0.4:
return None
near = opaque[_dist_to(opaque, best[0]) < 40]
return tuple(int(v) for v in near.mean(0))
def _quantize(px, n):
q = Image.fromarray(px.astype(np.uint8).reshape(1, -1, 3), "RGB").quantize(colors=n, method=Image.Quantize.MEDIANCUT)
pal, counts = q.getpalette(), q.getcolors()
total = sum(c for c, _ in counts)
merged = []
for rgb, frac in sorted(((tuple(pal[i * 3:i * 3 + 3]), c / total) for c, i in counts), key=lambda cf: -cf[1]):
for m in merged:
if color_dist(m[0], rgb) < 40:
m[1] += frac
break
else:
merged.append([rgb, frac])
return [(tuple(c), f) for c, f in merged]
def _interior(arr, tol=24):
"""Opaque pixels whose 4 neighbours are opaque and nearly the same colour. Drops the
anti-aliased blends at shape edges, which would otherwise show up as muddy extra colours."""
rgb = arr[:, :, :3].astype(float)
ok = arr[:, :, 3] >= 200
keep = ok.copy()
for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nb = np.roll(np.roll(rgb, dy, 0), dx, 1)
nok = np.roll(np.roll(ok, dy, 0), dx, 1)
keep &= nok & (np.abs(rgb - nb).max(-1) < tol)
keep[0, :] = keep[-1, :] = keep[:, 0] = keep[:, -1] = False
return keep
def extract_logo_colors(path, n=6):
"""Return [(rgb, fraction)] of a logo's brand colours, most important first.
- Transparent pixels and anti-aliased edge blends are ignored.
- A solid white/black/gray background (the colour filling most of the image border) is
removed. A coloured background is kept: it is part of the brand.
- Brand hues come first; neutral colours (white, black, gray) that are part of the logo
itself rank after every hued colour.
- If nothing but the background is left (e.g. a plain logo on its own colour), the
background colour is used rather than nothing.
"""
img = Image.open(path).convert("RGBA")
img.thumbnail((256, 256))
arr = np.array(img)
opaque = arr[:, :, 3] >= 200
if not opaque.any():
return []
inner = _interior(arr)
if inner.sum() < 0.25 * opaque.sum(): # tiny or noisy logo: use every opaque pixel
inner = opaque
px = arr[inner][:, :3].astype(float)
bg = logo_background(arr)
if bg is not None and is_neutral(bg):
fg = px[_dist_to(px, bg) >= 60]
if len(fg) < max(20, 0.002 * len(px)):
return [(bg, 1.0)]
px = fg
found = _quantize(px, n)
hued = [cf for cf in found if not is_neutral(cf[0])]
neutral = [cf for cf in found if is_neutral(cf[0])]
return sorted(hued, key=lambda cf: -cf[1]) + sorted(neutral, key=lambda cf: -cf[1])
def pick_main_accent(colors, ptype):
type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype])
if not colors:
main = type_rgb
accent = rotate_hue(type_rgb, 0.5)
else:
main = colors[0][0]
cands = [(c, f) for c, f in colors[1:] if f >= 0.04 and color_dist(c, main) >= 90]
if cands:
# hued brand colours beat white/black/gray text for the accent
accent = max(cands, key=lambda cf: color_dist(cf[0], main) * cf[1] ** 0.5
* (0.6 if is_neutral(cf[0]) else 1.0))[0]
elif color_dist(type_rgb, main) >= 90:
accent = type_rgb
else:
accent = rotate_hue(main, 0.5)
# Keep main and accent readable against each other
(_, lm, _), (ha, la, sa) = to_hls(main), to_hls(accent)
if abs(lm - la) < 0.12 and color_dist(main, accent) < 140:
la = la + 0.22 if lm < 0.55 else la - 0.22
accent = from_hls(ha, la, sa)
return main, accent
def build_palette(main, accent, ptype):
type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype])
# Elemental parts (leaves, flames, sparks) must read against the body: if the type colour
# is close to the main colour, push its lightness away from the main colour.
if color_dist(type_rgb, main) < 150:
(_, lm, _), (ht, lt, st) = to_hls(main), to_hls(type_rgb)
type_rgb = from_hls(ht, lm + 0.24 if lm < 0.5 else lm - 0.24, st)
pal = {}
for group, rgb in (("main", main), ("accent", accent), ("type", type_rgb)):
pal[f"{group}_light"], pal[f"{group}_base"], pal[f"{group}_dark"] = ramp(rgb)
pal["outline"] = outline_color(main)
return pal
# ============================================================ generate
def slugify(s):
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "business"
def classify(description):
try:
from business_pokemon_type import get_business_type # imported lazily so --type works offline
except ImportError:
sys.exit("business_pokemon_type.py not found next to this script; pass --type instead.")
except SystemExit:
sys.exit("No --type given and OPENROUTER_API_KEY is not set: export it or pass --type <type>.")
ans = get_business_type(description)
return ans["choice"].lower(), ans.get("probabilities")
def generate(name, parts_dir, out_dir, ptype=None, description=None, logo=None, colors=None,
reroll=0, scale=4):
probs = None
if not ptype:
ptype, probs = classify(description or name)
ptype = ptype.lower()
if ptype not in cfg.PLAN_WEIGHTS:
sys.exit(f"Unknown type '{ptype}'. Options: {', '.join(cfg.TYPES)}")
rng = make_rng(name, reroll)
plan, chosen, markings, tags = pick_parts(ptype, rng)
if colors:
logo_colors = [(hex_to_rgb(c), 1.0) for c in colors]
elif logo:
logo_colors = extract_logo_colors(logo)
if not logo_colors:
warn("no usable colors in logo, falling back to type color")
else:
logo_colors = []
main, accent = pick_main_accent(logo_colors, ptype)
palette = build_palette(main, accent, ptype)
sprite = recolor(place_on_canvas(compose(parts_dir, chosen, markings, rng)), palette)
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
slug = slugify(name) + (f"-r{reroll}" if reroll else "")
sprite.save(out_dir / f"{slug}.png")
if scale > 1:
sprite.resize((sprite.width * scale, sprite.height * scale), Image.NEAREST).save(out_dir / f"{slug}@{scale}x.png")
info = {
"name": name, "type": ptype, "type_probabilities": probs, "plan": plan,
"parts": {s: p["name"] for s, p in chosen.items()}, "markings": markings,
"attributes": attributes_from(tags),
"colors": {"main": rgb_to_hex(main), "accent": rgb_to_hex(accent),
"type": cfg.TYPE_COLORS[ptype], "outline": rgb_to_hex(palette["outline"])},
}
(out_dir / f"{slug}.json").write_text(json.dumps(info, indent=2))
print(f"{name}: {ptype.upper()} {plan} | {', '.join(info['attributes']) or 'no attributes'} | "
f"main {info['colors']['main']} accent {info['colors']['accent']} -> {out_dir / slug}.png")
return info
# ============================================================ placeholders
BASE_ANCHORS = {"pivot": (0.5, 1.0), "head_top": (0.35, 0.08), "back": (0.7, 0.35), "tail": (0.9, 0.7),
"aura": (0.5, 0.55)}
CHILD_ANCHORS = {
"head_top": {"pivot": (0.5, 1.0)}, "back": {"pivot": (0.5, 0.85)}, "tail": {"pivot": (0.05, 0.6)},
"aura": {"pivot": (0.5, 0.5)},
}
DEFAULT_SIZE = {"head_top": (16, 14), "back": (40, 30), "tail": (26, 14), "aura": (84, 84)}
def anchors_for(slot, p):
return BASE_ANCHORS if slot == "body" else CHILD_ANCHORS[slot]
def size_for(slot, p):
return tuple(p["size"] or DEFAULT_SIZE[slot])
def _shaded(d, box, pal):
x0, y0, x1, y1 = [int(v) for v in box]
w, h = x1 - x0, y1 - y0
d.ellipse((x0, y0, x1, y1), fill=KEYS[f"{pal}_dark"], outline=KEYS["outline"])
if w > 4 and h > 4:
d.ellipse((x0 + 1, y0 + 1, x1 - max(2, int(w * 0.2)), y1 - max(2, int(h * 0.25))), fill=KEYS[f"{pal}_base"])
d.ellipse((x0 + int(w * 0.2), y0 + int(h * 0.15), x0 + int(w * 0.4), y0 + int(h * 0.3)), fill=KEYS[f"{pal}_light"])
def _rect(d, box, pal):
d.rectangle([int(v) for v in box], fill=KEYS[f"{pal}_base"], outline=KEYS["outline"])
def draw_placeholder(slot, p):
w, h = size_for(slot, p)
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
pal = p["palette"]
if slot == "body": # base creature: round body with a big head and two dot eyes
_shaded(d, (w * 0.3, h * 0.45, w - 1, h - 1), pal)
_shaded(d, (0, 0, w * 0.65, h * 0.7), pal)
for ex in (0.2, 0.42):
d.rectangle((int(w * ex), int(h * 0.3), int(w * ex) + 1, int(h * 0.3) + 2), fill=PUPIL)
elif slot == "head_top":
d.polygon([(0, h - 1), (w // 2, 0), (w - 1, h - 1)], fill=KEYS[f"{pal}_base"], outline=KEYS["outline"])
d.line([(w // 2, 2), (w // 2, h - 3)], fill=KEYS[f"{pal}_light"])
elif slot == "aura":
rng = random.Random(p["name"])
for _ in range(14):
x, y = rng.randrange(4, w - 8), rng.randrange(4, h - 8)
r = rng.randint(2, 3)
d.ellipse((x, y, x + r * 2, y + r * 2), fill=KEYS["type_base"], outline=KEYS["type_dark"])
else:
_shaded(d, (0, 0, w - 1, h - 1), pal)
for mname, (fx, fy) in anchors_for(slot, p).items():
img.putpixel((min(int(fx * w), w - 1), min(int(fy * h), h - 1)), MARKERS[mname] + (255,))
return img
def make_placeholders(parts_dir, overwrite=False):
made = 0
for slot, parts in cfg.SLOTS.items():
(Path(parts_dir) / slot).mkdir(parents=True, exist_ok=True)
for p in parts:
if p["name"] == "none":
continue
path = Path(parts_dir) / slot / f"{p['name']}.png"
if path.exists() and not overwrite:
continue
draw_placeholder(slot, p).save(path)
made += 1
print(f"Wrote {made} placeholder sprites to {parts_dir}/")
# ============================================================ sprite list + weight report
def simulate(ptype, n=3000):
plans, slots, marks = Counter(), {s: Counter() for s in PICK_ORDER}, Counter()
for i in range(n):
plan, chosen, m, _ = pick_parts(ptype, random.Random(i))
plans[plan] += 1
marks[m] += 1
for s in PICK_ORDER:
slots[s][chosen[s]["name"] if s in chosen else "none"] += 1
return plans, slots, marks
def pct(counter, n, top=None):
return ", ".join(f"{k} {v / n:.0%}" for k, v in counter.most_common(top) if v / n >= 0.01)
def write_sprite_list(path):
L = ["# Sprite checklist", ""]
total = sum(1 for parts in cfg.SLOTS.values() for p in parts if p["name"] != "none")
L += [f"**{total} sprites** across {len(cfg.SLOTS)} slots. Save each as `parts/<slot>/<name>.png`.", "",
"## Art rules", "",
f"- Final canvas is {CANVAS}x{CANVAS}. Sizes below are suggested part canvases; crop tight with transparency.",
"- Characters face left in a 3/4 view, light from the top-left. 1px outline, no anti-aliasing, no semi-transparent pixels.",
"- Draw only with the palette keys below. Any other color (eye white, pupils) is left as-is.", ""]
L += ["| Key | Hex | Use |", "|---|---|---|"]
uses = {"main": "body / most fur and skin", "accent": "horns, wings, beaks, crests, shells",
"type": "elemental bits: flames, leaves, sparks, ice, water"}
for k, v in KEYS.items():
L.append(f"| {k} | {rgb_to_hex(v)} | {'outline (recolored)' if k == 'outline' else uses[k.split('_')[0]]} |")
L += [f"| pupil | {rgb_to_hex(PUPIL)} | not recolored |", f"| eye white | {rgb_to_hex(EYE_WHITE)} | not recolored |", ""]
L += ["### Anchor marker pixels", "",
"One pixel each. `pivot` is where the part attaches to its parent; the others are where children attach.", "",
"| Marker | Hex | Needed in |", "|---|---|---|",
f"| pivot | {rgb_to_hex(MARKERS['pivot'])} | every part (bodies: bottom center between the feet) |",
f"| head_top | {rgb_to_hex(MARKERS['head_top'])} | all bodies (top of the head) |",
f"| tail | {rgb_to_hex(MARKERS['tail'])} | all bodies |",
f"| back | {rgb_to_hex(MARKERS['back'])} | all bodies |",
f"| aura | {rgb_to_hex(MARKERS['aura'])} | all bodies (center of mass; belly patch centre) |", ""]
for slot in ["body", "head_top", "back", "tail", "aura"]:
parts = [p for p in cfg.SLOTS[slot] if p["name"] != "none"]
L += [f"## {slot} ({len(parts)})", "", "| Done | File | Size | Palette | Plans | Description | Most likely for |",
"|---|---|---|---|---|---|---|"]
for p in parts:
top = sorted(cfg.TYPES, key=lambda t: -part_weight(p, t))[:3]
top = ", ".join(t for t in top if part_weight(p, t) > 0)
plans = "all" if p["plans"] == cfg.ALL else ", ".join(p["plans"])
w, h = size_for(slot, p)
L.append(f"| [ ] | `{slot}/{p['name']}.png` | {w}x{h} | {p['palette']} | {plans} | {p['desc']} | {top} |")
L.append("")
L += ["## Simulated pick rates per type", "",
"From 3,000 random seeds per type. Use this to spot parts that never show up or show up too much.", ""]
for t in cfg.TYPES:
n = 3000
plans, slots, marks = simulate(t, n)
L += [f"### {t.capitalize()}", "", f"- **Body plan:** {pct(plans, n)}"]
for s in PICK_ORDER:
L.append(f"- **{s}:** {pct(slots[s], n, 8)}")
L += [f"- **markings:** {pct(marks, n)}", ""]
Path(path).write_text("\n".join(L))
print(f"Wrote {path}")
# ============================================================ CLI
def main():
ap = argparse.ArgumentParser(description="Generate Pokémon-style business sprites")
ap.add_argument("name", nargs="?", help="Business name (also the random seed)")
ap.add_argument("logo_file", nargs="?", help="Logo image to pull colors from (same as --logo)")
ap.add_argument("--desc", help="Business description for Jev (defaults to the name)")
ap.add_argument("--type", help="Skip Jev and use this type")
ap.add_argument("--logo", help="Logo image to pull colors from")
ap.add_argument("--colors", help='Manual colors, e.g. "#2E7D32,#FBC02D" (main first)')
ap.add_argument("--reroll", type=int, default=0, help="Alternate creature for the same business")
ap.add_argument("--parts", default="parts", help="Parts folder")
ap.add_argument("--out", default="sprites", help="Output folder")
ap.add_argument("--scale", type=int, default=4, help="Also save an upscaled copy (1 to disable)")
ap.add_argument("--batch", help="CSV with columns name,logo,type,description (only name required)")
ap.add_argument("--make-placeholders", action="store_true", help="Create test art for every part")
ap.add_argument("--overwrite", action="store_true", help="With --make-placeholders, replace existing files")
ap.add_argument("--sprite-list", metavar="PATH", help="Write the art checklist and weight report")
a = ap.parse_args()
if a.make_placeholders:
make_placeholders(a.parts, a.overwrite)
if a.sprite_list:
write_sprite_list(a.sprite_list)
colors = a.colors.split(",") if a.colors else None
if a.batch:
with open(a.batch, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
if not (row.get("name") or "").strip():
continue
generate(row["name"].strip(), a.parts, a.out, ptype=(row.get("type") or "").strip() or None,
description=(row.get("description") or "").strip() or None,
logo=(row.get("logo") or "").strip() or None, scale=a.scale)
elif a.name:
logo = a.logo or a.logo_file
if logo and not Path(logo).exists():
sys.exit(f"Logo not found: {logo}")
generate(a.name, a.parts, a.out, a.type, a.desc, logo, colors, a.reroll, a.scale)
elif not (a.make_placeholders or a.sprite_list):
ap.print_help()
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

View File

@@ -0,0 +1,32 @@
...............oooooo...........
.............oo222222oo.........
............o2222222222o........
.........ooo2H2222222222o.......
........o222221111122B222o......
......oo222221111112222222o.....
....oo22221111111111222222o.....
...o22222111111111111122223o....
.oo222221111111111111222223o....
o22222211111111111111122222o....
.ooo222111111111111111222233o...
..o2222222111111111212222233o...
.o22222221111112222222222233o...
o222222222211122222222222233o...
.oooo2222222222222A222222333o...
...o222222222222222222222233o...
...o223322222222222222222233o...
....oooo22222222222222222223o...
........o2222222222222222223o...
........o22222222222333332333o..
.........o22222222333333333T3o..
.........o23333333333333333333o.
..........o23333333333333333332o
...........o3333333333333333333o
............o3obco33333oooooooo.
.............oobco333ooo........
..............obcoooocco........
..............obco..occo........
.............oobcoo.occo........
............oabbbcooobcoo.......
............obbbccoobbcco.......
............ooooooPoooooo.......

View File

@@ -0,0 +1,28 @@
..............ooo.............
..........oooo222oooo.........
........oo22222222222oo.......
.......o222222222222222o......
......o22221111112222222o.....
.....o222111111112B222222o....
....o222111111111112222222o...
...o22211H11111111122222222o..
..o222111111111111122222222o..
..o2221111111111112222222223o.
.o22222111111111222222222223o.
.o22222111111122222222222233o.
.o222221111112222222222222T3o.
.o2222221112222A222222222333o.
.o22222222222222222222233333o.
.o22222222222222222222333333o.
.o2222222222222222223333333o..
..o222222222222222233333333o..
..o22222222222222333333333o...
...o222222222223333333333o....
....o3222222233333333333o.....
.....o33333333333333333o......
......o333333333333333o.......
.......oo33333333333oo........
.........ooco333ocoo..........
..........ocoooooco...........
.........obbco.obbco..........
..........ooo.P.ooo...........

View File

@@ -0,0 +1,39 @@
.................oooooooooo.................
.............oooo2222222222oooo.............
...........oo222222222222222222oo...........
.........oo22oooooooHooo222222222oo.........
........o111111111122222oo222oooo22o........
.......o211111111111121122o2o2111222o.......
.......o221111111111111122oo221111222o......
......o2o22212211122211222oo2111111222o.....
......o22o222221222222222oo22111122223o.....
.....o2o2o22223o22223o222oo22111122223o.....
.....o2o22o223o1o223o1o23oo22111122223o.....
....o22o22o23o111o2o111oo2o22211122233o.....
....o22o222oo1111o3o122222o22221112233o.....
....o222o22oo21111o12222222o22B211223o......
....o222o2222211112222222222o222122223o.....
...o2223o22222112222222222222o22122223o.....
...o222o222222122222222222222o22222223o.....
...o222o222222222222222222222o212222223o....
..o2222o222222222222222222222o222222223o....
..o2222o2222222222222A2222222o222222223o....
..o2222o222222222222222222223o2212222233o...
.o22222o222222222222222222233o2212222233o...
.o222223o22222222222222222333o22122222333o..
o2222333o22222222222222233333o22222T23333o..
o22223333o2222222222233333333o22222233333o..
o22233332o2222222223333333333o22222233333o..
.o33333333o222222333333333333o22222233333o..
.o33333333oo33323333333333333o22222333333o..
..oo3333oo32oo33333333333333ooo222333333o...
....oooo.o2223ooo33333333ooo112oo333333o....
.........o2322333oo33333o22222222oo33oo.....
.........o222233333oooooo2222222223oo.......
........oo222333333o....o2222222223o........
.......o2222333333o....o222222222233o.......
......o222223333333o...o222222233333o.......
......o233333333333o...o222223333333o.......
.......o3333333333o....o222233333333o.......
........oo333333oo......ooo333333ooo........
..........oooooo......P....oooooo...........

View File

@@ -0,0 +1,31 @@
..............................
..............................
............oooooo............
..........oo222222oo..........
.........o22222H2222o.........
........o222211122222o........
.......o22211111122222o.......
.......o22111111112222o.......
......o2221111111122223o......
......o2221111111122223o......
......o2221111111122223o......
......o222211111122ooo3o......
.....o2o2211111122o1B2oo......
....o22o2111111222o1222o......
...o22o22211112222o122o3o.....
..o22o22221112222o2223o32o....
..o22o222211222A2o222o333o....
..o23o22221222222o123o333o....
...o3o2222122222o222o3333o....
....o22222222222o222o3333o....
....o22222222222o223o33T3o....
....o222222222222ooo33333o....
.....o222222222233333333o.....
......o2222222233333333o......
.......oo3333333333333o.......
.....oo22ooo333322233o........
....o2222222o111112222o.......
....o2223333o2111222222o......
....o33333333o22222232o.......
.....oo33333oo2333333o........
.......ooooo..Poooooo.........

View File

@@ -0,0 +1,45 @@
............oooooooo............
..........oo22222222oo..........
.........o2ooooooooo22o.........
........o1o2111H1122oo2oo.......
.......o1111111111112oo22o......
......o2211111111211o21122o.....
......oo22222111222oo11122o.....
......o2o2222o22223o2111233o....
......o2o222o1o223o2o21223o.....
.....o2o2o2o111o2o22o21223o.....
.....o22ooo2211o3o223o1222o.....
.....o22ooo22212o2233o22223o....
.....o233o222222222333o2223o....
....o22333o2222222233Bo1223o....
....o22333o2222222233o21223o....
...oo2233oo2222222233o212233ooo.
..o2o2233oo2222222233o221233222o
.o2222233oo2222222233oo21223ooo.
o32222233o222122A23333o22223o...
.oo223333o222122233333oT2223o...
..o233333o222222333333oo12233o..
..o233333o22222233333oo212233o..
...o3333ooo2222233333o3o22333o..
....oooo.o2oo333333oo23o2233o...
.........o223oooooo2223o333o....
.........o223333o222233oooo.....
.........o223333o222233o........
.........o22333o222223o.........
.........o22333o222223o.........
.........o22333o222233o.........
.........o22333o222233o.........
.........o22333o222233o.........
.........o22333o222223o.........
.........o2233o.o22223o.........
.........o2233o.o22223o.........
.........o2233o.o22223o.........
.........o2333o.o21223o.........
.........o2233o.o21223o.........
........oo2233o.o21223o.........
......oo22233o.oo11223o.........
.....o22222333o1111222o.........
.....o23333333o21122333o........
....o33333333o222222333o........
.....oooooooo22223333oo.........
.............oooPoooo...........

View File

@@ -0,0 +1,38 @@
.....................oooo.................
...................oo2222o................
.................oo22222o.................
..............ooo22222222ooo..............
............oo22222222222222oo............
..........oo222222112222222222oo..........
.........o2222211111122222222222o.........
........o222211111111^12222222222o........
.......o22211111111111111222222222o.......
......o2222111111111111112222oo2222o......
......o222211111111111111112o12o222o......
.....o2222111111111111111122o22o2223o.....
.....o22221111111111111112222oB22223o.....
.....o222211111111111112222222222222o.....
....o22222211111111E12222222222222233o....
....o22222211111111222222222222222233o....
....o22222222111222222222222222222333o....
....o22222222112222222222222222223333o....
....o22222222222222222222222222233333o....
...o222222222222222222222222222333333oo...
..o22222222222222222222S222222233333332o..
..o222222222222222222A22222222233333332o..
..o222222222222222222222222222233333333o..
..o222o2222222222222222222222223333o333o..
..o212oo2222222oo2222oo22222o2233T3o233o..
.o2123o2o21223o11o23o12o223o2o21223o223o..
.o2123o2o21223o11o23o22o223o2o21223o223o..
.o2223o2o22223o22o23o22o223o2o22223o233o..
.o22223o222233o22oooo22o223o2o22223oooo...
..o223oo222233o22222222o223o2o22223o......
...ooo22o22223o222222222ooo22o2223o2ooo...
..o11222o22223o22222222222233o2223oo222o..
.o222222o22223o222222233333333oooo11o122o.
.o2222222o223o2223333333333333333o22o222o.
..o2222222ooo333333333333333333333oo333o..
...ooo333333333333333333333333333333ooo...
......oooooo333333333333333333oooooo......
............oooooooooPoooooooo............

View File

@@ -0,0 +1,40 @@
......................................
...............oooooooo...............
............ooo22222222ooo............
..........oo22222222222222oo..........
.........o222222111222222222o.........
........o22221111111222222222o........
.......o22211111111^1222222222o.......
......o222111111111111122222222o......
.....o22211111111111111122222222o.....
.....o22211111111111111112222222o.....
....o2222211111111111112222222223o....
....o2222211111111111222222B22223o....
....o2222211111111122222222222233o....
....o222222111111E222222222222233o....
....o2222221111222222222222222233o....
....o2222221112222222222222222333o....
....o2222222222222222222222223333o....
....o2222222222222222222222233333o....
....o22222222222222A2222222333333o....
....o2222222222222222S222233333T3o....
.....o22222222222222222233333333oo....
....o1ooo2222222222222233333333o22o...
...o2111o222222222222233333333o2223o..
...o2122o22222222222223333333o22333o..
...o2223o33222222222223333333o23333o..
...o2233oo322222222222233333o.ooooo...
....oooo..oo222222222223333o..........
...........o22222222222333o...........
............o3222222222233o...........
.............o322222222223o...........
..............o332222222233o..........
..............o233233333333o..........
...............o33333333333o....o.....
................o33333333333o.oo2o....
.................o33333333332o2222o...
..................oo3333333331222o....
....................o33333333222o.....
.....................o333333333o......
......................ooooo333o.......
...........................oPo........

View File

@@ -0,0 +1,31 @@
......................................
.............oooooooooooo.............
..........ooo222222222222ooo..........
........oo222222222222222222oo........
.......o22222222222^2222222222o.......
......o222222111111122222222222o......
.....o22211111111111122222222222o.....
....o2221111111111111112222222222o....
...o222111111111111111112222222222o...
..o22211111111111111111112222222222o..
..o2222111111111111111111122B222222o..
.o2222211111111111111111111222222223o.
.o2222221111111111111122222222222233o.
.o222222111111111E222222222222222233o.
.o2222222111111222222222222222222233o.
.o2222222111122222222222222222222233o.
.o22222222122222222A2222222222223333o.
.o2222222222222222222222222222223333o.
.o2222222222222222222222222222223333o.
.o22222222222222222222S2222222223333o.
.o2222222222222222222222222222333333o.
..o22222222222222222222222223333333o..
..o222222222222222222222333333333T3o..
.o2222222222222222233333333333333333o.
.o2222222222333333333333333333333333o.
..o22223223333333333333333333333333o..
...o323333333333333333333333333333o...
....oo33333333333333333333333333oo....
......oooo333333333333333333oooo......
..........oooooooooooooooooo..........
...................P..................

View File

@@ -0,0 +1,35 @@
..................................................
............ooooooo...............................
.........ooo2222222ooooooooo......................
........o222222222222o222222oooo..................
......oo22222112222222o222222222oo................
.....o22221111111222222oo222B22222oo..............
....o2222111111^112222222o2222222222oo............
...o2221111111111122222222o22222222222oo..........
...o2221111111111111222222o2222222222222ooo.......
..o222111111111111111222222o222222222222222ooo....
..o222211111111111112222222o222222222222222222o...
.o22222111111111112222222222o222222222222222222o..
.o22222111111111122222222223o222222222222222222o..
.o2222211111E112222222222223o222222222222222T23o..
.o22222211111122222222222223o222222222222222233o..
.o2222221111222222222222ooo3o222222222333333333o..
.o2222221122222222222A22oaaoo22222333333333333o...
.o2222222122222222222222oaaaaooo233333333333oo....
.o2222222222222222222222obaaaaaaooo33333oooo......
.o2222S22222222222222223obbbaaaaaaaoo3oo..........
.o22222222222222222222333obbbbbbaaaaaoo...........
..o2222222222222222223333obbcbbbbbbbbbbo..........
..o22222222222222223333333obbccbbbbbbboo..........
...o2222222222222233333333obbbbccccccoo...........
...o2222222222233333333333oobbbbbbbbo.............
....o23222233333333333333o3obbbbbbbbbo............
.....o33333333333333333oo333obcbbbbbbbo...........
......oo33333333333333o33333obbccbbbccco..........
........o333333333333o333333oobbbcccoo............
.........ooo3333333ooooooooo..ooooo...............
............ooooooo...............................
.............obbbbco..............................
..............obbbbco.............................
...............obbcco.............................
................oPoo..............................

View File

@@ -0,0 +1,30 @@
..............................................
....................oooooooooooooo............
.................ooo22222222222222ooo.........
................o2222222222B222222222o........
..............oo2222211112222222222222oo......
.............o22221111111111122222222222o.....
............o2222111111111111111222222222o....
...........o2211111oooooooooooooo222222222o...
......ooooo22211ooo22222222222222ooo2222222o..
.....o22222ooooo21111111111222222222oo22222o..
....o221111122o11111111111111222222222o22223o.
...o22111H11222o11111111111111112222222o2223o.
..o2211111111222o11111111111111111222222o233o.
..o2221111111122o111111222222222222222222T33o.
..o2221111112222o122222222222222222222222o33o.
..o2221111222222o22222222A222222222222222o33o.
..o2222222222222o222222222222222222222333o3o..
..o2222222222223o222222222222222222223333o3o..
...o22222222223o222222222222222222223333o3o...
....o22222222oo22333333333333333333oo33o3o....
.....o3oooooo12o33333333oo33333333o12oo3o.....
......o2233o1122oooooooo12oooooooo1122oo......
.....o2223o111222o3333o1122o3333o211122o......
.....o223o2112233o333o211122o33oo2211222o.....
....o2233o211223oo222o211123oooo2o2122222o....
....o223o2212233oo223o211223o...oo2222223o....
.....o33o222223o.o233o222223o....oo222233o....
......o3o222233o..o33o222233o.....o233333o....
.......ooo2233o....o33o2233o.......o3333o.....
..........o33o......oo.oP3o.........oooo......

View File

@@ -0,0 +1,25 @@
................................................
................................................
................................................
.......................o........................
......................o2o.......o...............
......oooooooo.......o22o......o2o......o.......
.....o22222222o.....o122o.....o22o.....o2o......
....o2211112222o...o1112o....o122o....o22o......
...o22111H122222ooooooooo...oo212o...o122o......
..o22111111122222o222222o.ooo2oooo..o2212o......
..o22111111112222o1111222o2222222ooooooooo......
.o2221111122222223o1112222o11B12222o22222oooo...
.o2221111222222223o11122222o1111222o111222o22o..
.o2221112222222223o11122223o11111222o111222o22o.
.o2222222222222333o22222223o11122223o111223o122o
.o2222222222223333o22222A233o2222223o222222T223o
..o22222222223333o222222223o22222223o22oo233o23o
..o2222222oooo333o222ooo333o22oooo33o2o12o3o233o
...o22223o1122o3o222o112o33o2o1122o3oo2112oo333o
....o3333o2112oo222o21122oo22o2112oo2o2122oo33o.
.....o333o1122o2333o21222o332o1122oo3o1123o33o..
......ooo21223oo333o21233o33o21223o3o21223ooo...
........o21223o.oooo2223o.ooo21223ooo21223o.....
........o22233o....o2223o...o22233o.o22233o.....
.........o233o.....o2233P....o233o...o233o......

View File

@@ -0,0 +1,35 @@
...............oooo.................
.......o......o2222o................
......obo....o221222o...............
......obbo..o2211H222o..............
......obbboo2oooo11222o.............
..o...obbbcoo1122o1222o.............
.obo..obbbboo1112o22223o............
.obao..obbco21222o22223o............
.obaao.obcco21233o22B23o............
.obaabo.oco22223o22222o2oo..........
.obbaao.obo21233o22223o222o.........
..obabaooo22223o222223o1222o........
..obbbbbboooo33o2222233o122o........
...obbbbbbbbboo2222223o11223o.......
...obcbcbbaaboo2222223o11122o.......
....occccbbbbo12222223o111223o......
.....oocccccco22222233o222222o......
.......oooooo22222A233o2222222o.....
..........o22222222333o22222222o....
...........o222222333o322222222o....
...........o2oooo3333o333322T223o...
............o1122o33o3o333333333o...
............o1112o3o22ooo33333333o..
...........o21122oo2233o.ooo333332o.
...........o21223o22233o....oo3333o.
...........o21223o22233o......oooo..
...........o21233oo2233o............
...........o2223o.o22333o...........
..........o22223o..o2333o...........
..........o21233o..o2333o...........
..........o2123o...o23333o..........
..........o2223o....o2333o..........
..........o2223o....o2333o..........
.........o22233o....o2333o..........
..........o223o..P...o333o..........

View File

@@ -0,0 +1,20 @@
.................oooooooooooooo......
.......oooooooooo22222222222222o.....
....ooo22222222222222222B2222222o....
...o1111111111111111111111oooooo2o...
..o11111H111111111111111oo111112o2o..
.oaaaaaaaaab11111111111o111111111T2o.
.oaaaaaaaabb11111111111o11111111122o.
.o2baaaaabb11111111111o2111111111222o
o221baaabb111111111111o2111111122223o
o22221bb1112222222A222o2111111222223o
.o22111112222222222222o2111122222223o
.o22211112222222222222o2211222222233o
.o222111222222222222222o21222222233o.
..o22111232222222222222o22222222333o.
...o212223o3333333333333o22222333oo..
...o2aaaa3ooooooo3333333oaaaaa3oo3o..
...oaaaaabo223333oooooooaaaaaao333o..
...obbbbbboo3333o......obbaaabo33o...
....obbbbo.o2333o.......obbbbo333o...
.....oooo...oooo.P.......oooooooo....

View File

@@ -0,0 +1,20 @@
.......oooo.oooooooooooo.........
.....oo1222o222222222222oo.......
....o111112111111122222B22o......
...o11111111111111111222222o.....
..o11111bH111111111111ooooo2o....
.o2aaaaaaaaabb1111111o11112oTo...
.oaaaaaaaaaabb111111o1111112oo...
.obaaaaaaaaab111111o1111111122o..
o2bbaaaaaaabb222222o1111111222o..
.o2bbaaaaabb2222A2o211111122222o.
.o222ooobbb2222222o211111222223o.
.o22o111obb2222222o221112222233o.
..o2o111o2b33333333o2112222233o..
...o2112o3333333333o2212222333o..
...o21123333333ooooo22222233ooo..
...o11223332233o....o222223o222o.
..o211223o22333o...o222223o22233o
..o222223o233333o..o222233o3333o.
...o22233oo3333o....o22333ooooo..
....ooooo..oooo.P....ooooo.......

View File

@@ -0,0 +1,26 @@
............oooooooo..............
........oooo22222222oooo..........
......oo2222211122222B22oo........
.....o22211111111222222222o.......
....o2211111111111111222222o......
...o221111111111111111122222o.....
..o2211H111111111111111122222o....
.oaaaaaaaaaab1111111oooooooo22o...
.oaaaaaaaaabb11111oo11111122oTo...
o22aaaaaaabb11111o1111111111222o..
o222baaaabb111111o1111111111122o..
o2222baabb111111o211111111111222o.
o22221bb11111122A211111111112223o.
o222211111112222o111111111122222o.
o22111122122222o22111111222222233o
.o21111122222222o221112222222223o.
.o21111122222222o222112222222233o.
.o21111122222222o222122222222233o.
.o2111122o2222222o2222222222233o..
.o2111222o2222222o222222222333oo..
.o2112222o33333333o222222333oo3o..
.o2aaaaab3oo3333333oaaaaa3oo333o..
.oaaaaaaao23ooo3333oaaaaao23333o..
.obbbbbbbo23333oooobbaaabo23333o..
..obbbbbbo3333o....obbbbbo3333o...
...oooooo.o33o.P....ooooo.o33o....

View File

@@ -0,0 +1,27 @@
............oooooo..............................
..........oo222222oo............................
.........o2222222222o...........................
........o2222H1222222o..........................
.......o22211111122222o.........................
......o2221111111222222oo.......................
.....o2221111111112222222oo.....................
...oo2221111111111122222222oo...................
..o22222111111111111122222222oo.................
.o2222211111111111111122222B222oo...............
..oo22221111111111111112222222222o..............
..o2222211111111222222222222222222oo............
.o2222211111112222222222222222222222o...........
..oo222222222222222222222222222222222o..........
..o22222222222222222222222222222222222oo........
.o2222222222222222222A222222222222222222oo......
..oooo222222222222222222222222222222222222oo....
.....o22oooooo222222222222222222222222222222oo..
....oooo112222o2222222222222222222222222222222o.
...o1111112222o3333333333333332222222222222222o.
..o1111112223o3333333333333333333333322232T2232o
.o2211122223o333333333333333333333333333333333o.
.o221222223o3333333333333333333333333333333333o.
o2222222333oooooooo33ooooooooo333333333333oooo..
o22o22333oo........oo233333333o333333ooooo......
.o2o33ooo...........o3333333ooooooooo...........
..ooooo..............Poooooo....................

View File

@@ -0,0 +1,36 @@
..........oooooo..............................
.........o222222o.............................
........o22111222o.....ooo....................
........o2111H122o...ooaao....................
.......o221111223o.ooaabbo....................
.......o221122223ooaabbco.....................
......o2221122223oabbcco......................
......o2211122223obccoo.......................
......o2211112233ooooooo......................
.....o22111111233oaabbbco.....................
.....o2211111123ooabbbccco....................
....o22211111123oobccccoo.....................
....o22211111122ooooooooooooooooooo...........
....o222111111222oo2222222222222222ooo........
....o222111112222o22122222222B11112222oo......
....o2222111122222o112222222222211111222o.....
....o22222111222222o113333333333221112222o....
.....o22221222222222o133333333333211222222o...
......o22222222222222o23333333333122222222o...
......o222222222222222o22333333211222222233o..
.......oo2222222222332o2222222111112222233o...
.....oo22o222222223333o2211111111112333333o...
...oo22211o22222233333o22221111112222333ooo...
..o2221111o22223333333o333222222222233oo222o..
.o221111111o2333333333o33A332222223ooo222222o.
o22211111111o33333333oooooooooooooo2222222222o
o222211111111o333333o22222222222222222222T223o
o2222222222222oooooo2222222222222222222222233o
o22222222222222222222222222222222222222332333o
.o222222222222222222222222222222222223333333o.
..o3332222222222222222222222222222333333333o..
...oo333333333222222222233333333333333333oo...
.....oo33333333333333333333333333333333oo.....
.......ooo33333333333333333333333333ooo.......
..........oooooo33333333333333oooooo..........
................oooooooPoooooo................

View File

@@ -0,0 +1,47 @@
............oooo..........................
...........o2222o.........................
..........o222122oo.......................
.........o222111222o......................
.........o2211111222o.....................
........o22211H111222o....................
.........o221111111222o...................
.........o222111111222o...................
..........o222111222223o..................
...........o22211222222o..................
............o22222222223o.................
.............o2222222223o.................
.............o2222222223o.................
..............o2222222333o................
..............o2222223333ooo..............
.............o21122223333obboo............
...........oo211122233333obbbco...........
..........o22211112333B3ooobccco..........
.........o22211112233333oobbccco..........
.........o22111122223333obbccco...........
.......ooo2211122222333obcccoo............
.....ooaao2221222223333oooo...............
...ooaaabo221122223333o...................
.ooaaaabbo2211122333oo....................
oaaaabbbbo221111233o......................
.oooabbbco22111113o.......................
.oaabbbcco22111123o.......................
oabbbbccco22111122o.......................
.ooobccco2221111222o......................
....oooo2221111222A2o.....................
.......o2222112222222o....................
.......o22221122222222ooooo...............
........o2222222222222o2222oooo...........
......oo2o2222222222222o2111222oo.........
....oo2221o122222222222o222111122oo.......
...o2221111o223222222332o3322111222o......
..o221111111o22222222333333321122222ooooo.
.o2221111112o2222223333333332222222222222o
.o2222111112oo232333333333311222222222222o
.o222222122222oo333333333o11112222T222222o
.o22222222222222ooooooooo11111222233333oo.
..o2222222222222111111111111122233333oo...
...o3332222222222211111112222223333oo.....
....oo3333333333322222222222223333o.......
......oo33333333333222222233333ooo........
........oooo333333333333333oooo...........
............ooooooooPoooooo...............

View File

@@ -0,0 +1,7 @@
oo............o
.ooo........ooo
...oo.....ooo..
.ooo......oooo.
owwpo....owwppo
oppwo..P.opppwo
.ooo......oooo.

View File

@@ -0,0 +1,3 @@
oo........oo
pp...P....pp
pp........pp

View File

@@ -0,0 +1,9 @@
.oooo......ooooo.
oppppo....opppppo
owwppo....owwpppo
owwppo....owwpppo
oppppo..P.opppppo
oppppo....opppppo
opppwo....oppppwo
opwwpo....oppwwpo
.oooo......ooooo.

View File

@@ -0,0 +1,8 @@
.oooo.....ooooo.
owabbo...owabbbo
oabcbo...oabcbbo
obcbco.P.obcbcbo
obbcco...obbcbco
ocbcco...ocbcbco
.occo.....occco.
..oo.......ooo..

View File

@@ -0,0 +1,9 @@
..oooooo..
.owwwwwwo.
owppppwwwo
opwwpppwwo
opwwPppwwo
opppppwwwo
owppppwwwo
.owwwwwwo.
..oooooo..

View File

@@ -0,0 +1,7 @@
.oo......ooo..
oyyo....oyyyo.
oxyo....oxxyo.
oxxo..P.oxxyo.
oxyo....oxyyo.
oyzo....oyyzo.
.oo......ooo..

View File

@@ -0,0 +1,3 @@
.oo......ooo.
oooo..P.ooooo
o..o....oo.oo

View File

@@ -0,0 +1,8 @@
oo..............oo
oaoo..........oobo
oaoooooooooooooobo
oaowpoaaabbowpwobo
oboppobbPbboppwoco
oboooobbbbboooooco
occccbboooobbcccco
.ooooooo..oooooo..

View File

@@ -0,0 +1,8 @@
.oooo.....ooooo.
oppppo...opppppo
owwppo...owwpppo
owwppo...owwpppo
oppppo.P.opppppo
oppppo...opppppo
opppwo...oppppwo
.oooo.....ooooo.

View File

@@ -0,0 +1,6 @@
o...........o
ooo......oooo
oooo....ooooo
owpo..P.oppwo
oppo....oppwo
.oo......ooo.

View File

@@ -0,0 +1,5 @@
.ooo.....oooo.
o112o...o1122o
ooooo..Poooooo
oppwo...opppwo
.ooo.....oooo.

View File

@@ -0,0 +1,21 @@
......................
.........oooooooo.....
......ooo22222222o....
.....o222212^22222o...
....o22111111122222o..
...o221111111112223o..
..o2211111111111222o..
.o222111111111222223o.
.o222211111112222223o.
.o222211112E22222223o.
.o222211122222222233o.
.o222211222222222233o.
.o222222222222222333o.
.o2S2222222222223333o.
.o222222222222233333o.
..o2222222222233333o..
...o22222223333333o...
....o333333333333o....
.....o333333P333o.....
......ooo3333ooo......
.........oooo.........

View File

@@ -0,0 +1,24 @@
...........ooooooo..........
........ooo2222222oo........
.......o222222222222o.......
.....oo22222111222222o......
...oo22ooooooo^1222222o.....
..o22111111122oooo22222o....
.o2211111111122112oo222o....
o22222222222222222oo2223o...
.o2222222222223ooo222223o...
..o2ooooooooooo112222223o...
...o22211111112222222222o...
..o2221111112222222222223o..
..o222111112E222222222222o..
.o222211122222222222222223o.
o2222221222222222222222333o.
o222222222222222222223oo333o
o2222S22222222222223oo33333o
o22222222222222233oo3333333o
o2222222222222233333333333o.
.o22222222223333333333333o..
..o333323333333333333333o...
...oo33333333333333333oo....
.....ooo3333333P333ooo......
........ooooooooooo.........

View File

@@ -0,0 +1,22 @@
...........ooooo..........
.........oo22222oo........
........o222112222o.......
.......o22111122222oo.....
......o2211111^222222oooo.
.....o2221111112222222222o
.....o221111111112222222o.
.....o22111111112222233o..
....o22111111112222222ooo.
...oo2111111E222222232322o
.oo222111111222222222332o.
o2222211112222222222233o..
o222222111222222222223ooo.
o22222S222222222222333322o
o22222222222222222333332o.
.o2oooo2222222223333333o..
..oo332222222223333333o...
....oo322233333333333o....
......o3333333333333o.....
.......o3333333P33oo......
........oooooooooo........
..........................

View File

@@ -0,0 +1,22 @@
..........oooooo..........
........oo222222oo........
......oo2222122222oo......
.....o22221112222222o.....
....o22111111^1222222o....
....o2211111111112222o....
...o222111111111222223o...
...o222111111111122223o...
...o222211111122222223o...
...o221111112222222223o...
..oo2222112E2222222233oo..
.o2222221222222222222332o.
o222222122222222222223322o
.ooo222222222222222233ooo.
..o22222222222222233333o..
.o22222S2222222333333332o.
o222222222222333333333332o
.ooo222323333333333333ooo.
....oo33333333333333oo....
......ooo33333P33ooo......
.........oooooooo.........
..........................

View File

@@ -0,0 +1,18 @@
..........................
..........................
............ooooo.........
....oooo...o22^22o....oo..
...o2222o.o2222222o.oobbo.
..o221122o22112222oobbbbo.
.o2211122121111222obbbbo..
.o22111111111112222oooooo.
.o221111111E1112222222obbo
.o2111112221112222222obbbo
o222222222221222222222obo.
o2222222222222o22223333o..
oooooSoooooooo22223333obo.
.o2222222222222333333obbo.
..o22323333333333333oboo..
...o3333333333333333oo....
....ooo3333333P33ooo......
.......oooooooooo.........

View File

@@ -0,0 +1,22 @@
..........................o.
........................oo2o
........oooooooo......oo222o
......oo22222222oooooo2222o.
.....o22212^2222222222222o..
....o22211111222222222222o..
...o2211ooo1111112111222o...
..o22ooooooooo111111223o....
.o22ooooooooooo11122223o....
.o2ooooooooooooo1222223o....
o2oooooooooooooo2222223o....
o2ooooooooooooooo222223o....
o2o333333E333333o222233o....
o2o3333333333333o222233o....
o22o33333333333o2222233o....
o22o33333333333o2322233o....
.o22ooS333333oo23333333o....
.o2222ooooooo2233333333o....
.o222322222223P33333333o....
.o22333o33333o33333o333o....
..o233o.o333o.o333o.o3o.....
...ooo...ooo...ooo...o......

View File

@@ -0,0 +1,20 @@
.......ooooooooo......
.....oo222222222oo....
....o2221112222222o...
...o221111111222222o..
..o22111111^11122222o.
..o22111111111112222o.
.o2221111111112222223o
.o2222111112222222223o
.o2222112222222222223o
.o2222222E22222222233o
.o2222222222222222333o
.o2222222222222223333o
.o2222222222222233333o
.o2222oo22222223333oo.
.o222o22ooo2233333o...
.o222oS222oooooooo....
..o2o222222223333o....
...o2222223P3333o.....
....oo3333333333o.....
......oooooooooo......

View File

@@ -0,0 +1,20 @@
.................oo.....oo..
...............oobbo..oobbo.
..........oo..oaaabo.oabbbo.
.........obboobbaaboobbabbo.
........obbbo2ooooo22obbbbo.
.......obboo2222222222ooco..
......oooo21111^11222222oo..
...ooo22211111111111222222o.
..o22221111111111111122223o.
.o211111111111111122222223o.
o2222111111111E12222222223o.
o2222211111122222222222233o.
o222222222222oo22222222333o.
oooooSooooooo222222222333o..
.o22222222222222222223333o..
..o3333332222222222333333o..
...ooo333333333333333333o...
......oooo3333333333333o....
..........ooo33P33333oo.....
.............oooooooo.......

View File

@@ -0,0 +1,22 @@
...........oo.....oo......
....ooooooo22o.ooo22oo....
...o222221o222o2222222o...
...o21111o22222o2222222o..
..o22111111^2222o2222223o.
.o221111111112222oo22233o.
.o2211111111112222o22333o.
o22211111111222222o22o333o
o22211112222222222o333o33o
o222211222E2222223o333o33o
o23222222222222223o33o333o
o22322222222222223o33o333o
o22322222222222233o333333o
o22222222222222333o333333o
.o2222oooooooo3333o333333o
.ooooo11111122ooooo33333o.
.o211S111111o111122o333o..
.o22111122222o221222o3o...
.o22222222222o2222233o....
..o22222233333P33233o.....
...oo33333333333333o......
.....oooooooooooooo.......

View File

@@ -0,0 +1,22 @@
........oooooooooo........
......oo2222222222oo......
.....o22211111112222o.....
....o2211111111122222o....
...o221111111^11122222o...
..o22111111111111122222o..
.o2211111111111111122222o.
.o2111111111111111112222o.
o222111111111111111122222o
o222111111111111111222223o
o222111111111111122222223o
o222111111111122222222223o
o2221111111E1112222222223o
o221111111111111222222223o
o211111111111111122222233o
o211111111111111122222333o
o2111S1111111111122223333o
o2211111o1o11222222333333o
.o2211111o22222333333333o.
..o22212222223333333333o..
...oo33333oooooooo333oo...
.....ooooo....P...ooo.....

View File

@@ -0,0 +1,6 @@
.ooooooo.
oaaaaaabo
oabbbbbbP
obbbbbbco
.occcccco
..oooooo.

Some files were not shown because too many files have changed in this diff Show More