| name | iso-city-builder |
| description | Generates playable isometric city-builder "games" — zone residential/commercial/industrial land, draw roads, watch buildings grow (or abandon) as demand and land value change, with cars routing over whatever road network the player actually built. Use when the description involves a city builder, SimCity-style game, "let me zone and build a city", or a construction/economy sim in isometric pixel art. Sibling to iso-living-world (which is a fixed, non-buildable ambient diorama) — pick this skill instead whenever the player needs to place zones/roads themselves. |
Iso City Builder
Generates a playable isometric city-builder: player paints zones and roads on an empty grid, buildings spawn and grow on their own once a zone has road access and demand supports it, cars route over the live road network. Reference implementation: examples/iso-city-builder — vanilla canvas, no framework, built directly on top of the iso-living-world rendering core (same projection, same painter's-algorithm sort, same sprite-anchoring rules — read skills/iso-living-world/SKILL.md first, this file only covers what's different: the buildable, simulated layer underneath the diorama).
Genre tag
Set gdd.genre to "iso-city-builder". winCondition/loseCondition are "false" — there's no fail state, only a running simulation. QA should verify simulation invariants (zones only grow with road access, money never goes negative-and-stuck, traffic always finds a path when one exists) rather than a scripted win path.
Engine contracts (see examples/iso-city-builder/main.js)
Reuses iso-living-world's projection, depth-sort, and sprite-anchoring rules unchanged. What's new:
- Tile model: each grid cell holds
{ zone: 'none'|'residential'|'commercial'|'industrial', road: bool, building: null|{type, level}, landValue, coverage }. Zone and building are decoupled — a tile can be zoned with nothing built yet, or hold an "abandoned" building after de-zoning. Never conflate "zoned" with "occupied."
- Roads are plain grid tiles, not a separate graph. Compute each tile's N/E/S/W road-neighbor booleans at draw time (cheap at builder-scale grids, 32×32 or smaller). Without dedicated road sprite art, don't fall back to flat-filled diamonds — draw it as a real vector road: pavement diamond, a sidewalk trapezoid on any of the 4 diamond edges whose facing neighbor isn't a road (edge T-R faces the N neighbor, R-B faces E, B-L faces S, L-T faces W — derivable directly from the
iso() projection, a neighbor's projected center always lands on 2× that edge's midpoint vector), dashed centerlines from tile-center toward each road neighbor, and a plain paved patch with no dashes wherever a tile has ≥3 road neighbors (reads as an intersection). This alone is most of the difference between a builder that "looks like lines on grass" and one that looks like a real street grid — worth getting right before spending budget on road sprite art.
- Growth gate: a zoned, building-less tile only spawns a building once a BFS flood-fill (max depth 8, 4-directional, walking only through same-zone or empty-but-zoned tiles) reaches a road tile. No road within 8 tiles ⇒ the zone paints but never builds. Re-run this check whenever a road is added/removed near a zoned tile.
- Density/abandonment roll: once a building exists, each sim tick compute
target = landValue/24 + coverage/28 + age/60 + demandBoost, where demandBoost = max(0, (demand[zone]-30)/70) * 0.7 (demand clamped -100..100). Building level drifts toward target by a small step per tick — this is what makes a house slowly become an apartment tower, not a hand-authored transition. When demand[zone] < -20 (and the building is old enough that abandonment reads as decline, not instant flicker), roll for abandonment instead: chance = min(0.02, 0.005 + (-demand-20)/2000) per tick. In the reference, an abandoned building does not un-abandon in place — at demand[zone] > 10 it instead rolls to clear back to bare zoned land (chance = min(0.12, (demand-10)/600)), and a fresh building respawns through the normal zoned+road-access spawn path. That's a truer "decline then rebuild" read than a direct flip back to occupied, and is worth matching if the abandon/recover cycle needs to look convincing rather than just work.
- Utilities gate: the reference only lets a zone's smallest starter tier (e.g. a small house/shop/farm) grow without power+water; every denser tier needs the tile to be inside a power-plant and water-tower coverage area, which means the player has to place and pay for utility buildings as their own build category.
examples/iso-city-builder implements this: power/water are two more single-tile utility buildings in the tool table (kind: 'utility'), coverage is a cheap per-tick Chebyshev-radius check from every placed utility tile (not a flood-fill — flood-fill only matters once roads/obstacles can block a service radius, which this grid-scale genre doesn't need), and growth is capped at the starter tier (level < 1) until tile.powered && tile.watered. A tile whose target level is being held back by missing utilities gets building.blocked = true for one tick, which the renderer turns into a small red badge over the roofline — that visible "wants to grow but can't" feedback is what makes the utilities layer read as a real system instead of an invisible multiplier. A V-key coverage overlay (green = both, amber = one, red = neither) is the fast way to debug/showcase this.
- Traffic: cars path with plain BFS over road tiles only (reuse the
iso-living-world BFS-to-tile-node pattern, but the walkable set is "is this tile a road" instead of a fixed street layout) — no A*, this genre never needs weighted edges at builder-scale grids. Snap start/destination to the nearest road tile adjacent to a building before pathing. At intersections (tile with ≥3 road neighbors) pick a random valid turn rather than implementing full traffic-light logic — a light state machine is not worth the complexity at this scale, and unsignalled turn-taking still reads as "traffic."
- Build-mode tool table: one declarative array drives both the palette UI and placement logic —
TOOLS = [{id, label, cost, kind: 'zone'|'road'|'bulldoze', zoneType?}]. The UI is generated by mapping over TOOLS, never hand-built per tool. Selecting a tool + click-dragging over tiles paints/erases; bulldozing clears zone+road+building on a tile and refunds nothing.
- Economy: track
{money, population, jobs, demand: {residential, commercial, industrial}} on a fixed tick (e.g. once/sec, decoupled from render framerate). Zoning/road placement costs money (deduct on paint, not per-tile-per-tick). Population/jobs derive from summed building levels by zone type; demand nudges toward f(jobs, population) each tick (e.g. residential demand rises when jobs > population, commercial/industrial demand rises with population) — keep the formula simple and tunable, the exact curve matters less than demand actually responding to what the player builds.
- Save/load: serialize the whole tile grid + economy state as one JSON blob (
JSON.stringify) to localStorage; no partial-save schema needed at this grid scale.
World-architect rules for this genre
- Start empty, not pre-built: unlike
iso-living-world's fully-populated diorama, a city-builder starts as a mostly bare grid — a few seed roads and maybe one starter building — so the player's zoning choices visibly shape the skyline over the first few minutes.
- Same asset economy as iso-living-world: reuse the same few building/prop sprites (house, house2, apt, shop, cafe, tower, tree, car) with hue-rotate for variety — a builder doesn't need more sprites than a diorama, it needs the sprites gated behind zone+level instead of placed by generation-time RNG.
level selects which sprite tier renders (e.g. residential level 0-1 → house, 2-3 → apt, 4+ → tower).
- Road drag-painting should snap to the grid and preview the tile under the cursor before commit, same interaction model as zone painting — one paint tool, two different effects on the tile.
- Readable HUD: money, population, and per-zone demand (as a small bar or arrow, not a raw number) visible at all times — the player needs to see demand response to their zoning within a few ticks or the loop feels dead.
- Camera/controls: keep
iso-living-world's pan/zoom; add a persistent tool palette (bottom or side dock) and a click-to-paint / drag-to-paint-a-line interaction instead of (or alongside) the free-walk player character — a builder is about the city, not about walking it, so a controllable avatar is optional here.