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, wheredemandBoost = max(0, (demand[zone]-30)/70) * 0.7(demand clamped -100..100). Buildingleveldrifts towardtargetby a small step per tick — this is what makes a house slowly become an apartment tower, not a hand-authored transition. Whendemand[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 — atdemand[zone] > 10it 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-builderimplements this:power/waterare 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) untiltile.powered && tile.watered. A tile whose target level is being held back by missing utilities getsbuilding.blocked = truefor 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. AV-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-worldBFS-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 overTOOLS, 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 towardf(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) tolocalStorage; 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.
levelselects 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.