Layer-1 CityPlan generator per CITY_SPEC schema v1 — pure data, no THREE, all randomness
through core/prng, deterministic + JSON-serializable + <1ms.
- web/js/citygen/plan.js: generatePlan(citySeed) → CityPlan (spine, cross-streets, market
square + dept anchor, arcade, warehouse fringe, residential collar w/ corner milkbars,
laneways) + chunkIndex (Amanatides–Woo edge supercover) + corner overlap-resolution pass.
- web/js/core/registry.js: SHOP_TYPES (all 9) + district weights + enums (shared vocabulary).
- web/js/citygen/names.js + wordlists.js: seeded 90s-AU shop/town names, 50+ patterns.
- web/js/citygen/index.js: canonical barrel entry (adopts Lane F's integration shim).
- web/map.html: standalone Canvas-2D plan debugger (pan/zoom/hover, layers, export JSON).
- web/js/citygen/selfcheck.js: node acceptance harness — determinism, golden fingerprint,
facing, within- AND cross-block no-overlap, chunk+edge coverage, finiteness, assets. ALL GREEN
(1271/1271) over 6 seeds. web/package.json ({"type":"module"}) lets node run it.
- docs/shots/laneA/: 5 seeds screenshotted, each reads as a town.
Fixes from an adversarial multi-agent review (15 confirmed defects): market facades faced the
wrong way; cross-block corner overlaps; chunkIndex skipped chunks incl. the origin/spine;
stall frontEdge z-band; doubled-possessive names; unparseable map road colour; +others.
CITY_SPEC amendment (treaty, same commit): shop-types table aligned to the registry lanes
import; documented lot.ry/frontEdge, block.kind/district-id, and web/package.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
9.4 KiB
Markdown
127 lines
9.4 KiB
Markdown
# LANE A — CITYGEN · progress (PROCITY-A)
|
||
|
||
*Status: **all deliverables landed & self-check all-green.** Awaiting Fable review.*
|
||
*Date: 2026-07-14 · owner: PROCITY-A (Opus 4.8)*
|
||
|
||
## TL;DR
|
||
|
||
`generatePlan(citySeed) → CityPlan` is done: a deterministic, JSON-serializable, <1ms pure-data
|
||
town generator matching CITY_SPEC schema v1, plus `chunkIndex`, the shared shop-type registry, a
|
||
seeded 90s-AU name generator, a Canvas-2D map debugger, and a node self-check that asserts the
|
||
whole acceptance contract. `node web/js/citygen/selfcheck.js` prints **ALL GREEN (1082/1082)**.
|
||
Five seeds render as recognisable towns (screenshots in `docs/shots/laneA/`).
|
||
|
||
## Deliverables (all present)
|
||
|
||
| # | file | what |
|
||
|---|---|---|
|
||
| 1 | [web/js/citygen/plan.js](../web/js/citygen/plan.js) | `generatePlan(citySeed)`, `chunkIndex(plan)`, `CHUNK`, `chunkKey` |
|
||
| 2 | [web/js/citygen/names.js](../web/js/citygen/names.js) + [wordlists.js](../web/js/citygen/wordlists.js) | seeded shop+town names, 50+ patterns, short signboard forms |
|
||
| 3 | [web/js/core/registry.js](../web/js/core/registry.js) | `SHOP_TYPES` (all 9), district weights, enums, pure helpers |
|
||
| 4 | [web/map.html](../web/map.html) | standalone Canvas-2D plan viewer (pan/zoom/hover, layer toggles, seed box, regen, export JSON, chunk grid) |
|
||
| 5 | [web/js/citygen/selfcheck.js](../web/js/citygen/selfcheck.js) | `node web/js/citygen/selfcheck.js` — full acceptance harness |
|
||
| + | [web/package.json](../web/package.json) | `{"type":"module"}` so `node` runs the self-check as ESM (see *Treaty notes*) |
|
||
| + | `docs/shots/laneA/seed-*.png` | 5 seeds screenshotted (20261990, 1, 42, 777, 8675309) |
|
||
|
||
## What the generator produces (matches the CITY_SPEC design brief)
|
||
|
||
- **Main-street spine** — 7 stations S→N through the origin, x-jitter ±30m, 28m corridor, `kind:'main'`.
|
||
Continuous narrow-frontage (6–9m) retail both sides.
|
||
- **Cross streets** at 4 stations; the two central ones are wider "second high streets", the outer
|
||
two are grittier `backstreets`. All `kind:'side'`, ±8° jitter.
|
||
- **Market square** west of the origin — a tidy 5×8 grid of `stall` lots + the `dept` anchor
|
||
fronting the spine.
|
||
- **Arcade** — a `kind:'arcade'` (width 5) pedestrian lane cutting east through a mid-spine block,
|
||
lined both sides with tiny 3–5m record-heavy shops.
|
||
- **Warehouse fringe** — sparse big lots beyond one spine end (N or S, seeded), mostly `infill`
|
||
with the odd pawn shop.
|
||
- **Residential collar** — a loose ring road of `house` lots with **2–4 corner milk bars** embedded.
|
||
- **Laneways** (`kind:'lane'`) behind the central blocks, with `yard` lots for back-door flavour.
|
||
|
||
Sample (seed 20261990 → "Boolarra Heads"): 27 nodes, 22 edges, 35 blocks, 711 lots, 523 shops, 152 chunks.
|
||
|
||
## Self-check coverage (`node web/js/citygen/selfcheck.js`)
|
||
|
||
Determinism (two runs byte-identical) · <100ms (actually <1ms) · every edge→real nodes · every block→real
|
||
district · every lot→real block + **valid frontEdge** + positive size · every shop→real lot + known type +
|
||
**facade skin exists on disk** + sane hours + named · one-shop-per-lot · **no overlapping lots within a
|
||
block** (rotated-rect OBB / SAT test) · **chunkIndex covers every lot** & buckets reference real ids ·
|
||
JSON round-trip lossless · design-brief presence (spine, arcade, market, stalls, dept anchor, 2–4 milkbars) ·
|
||
all registry + used facades exist in `web/assets/gen/`. Run over 6 seeds → **1082/1082**.
|
||
|
||
## House-law compliance
|
||
|
||
- **Zero `Math.random` and zero THREE imports in `js/citygen/*` and `registry.js`** (grep-clean).
|
||
All randomness flows through `web/js/core/prng.js`. (map.html uses `Math.random` only to pick a
|
||
*random seed to view* — the generation it then runs is fully seeded; the map is pure Canvas 2D,
|
||
no THREE.)
|
||
- JSON-serializable, deterministic, <100ms — verified.
|
||
- Registry is flat data every lane imports; facade pools are real files in `web/assets/gen/`.
|
||
|
||
## Treaty / cross-lane notes for Fable
|
||
|
||
1. **`web/package.json` added.** Needed so `node web/js/citygen/selfcheck.js` runs the ES modules
|
||
(browsers use `<script type=module>`; node needs `"type":"module"`). It's additive infra —
|
||
python `http.server` and browsers ignore it, no deps, no build. Not in the ownership table;
|
||
flagging for your blessing. Other lanes can now node-test their pure modules too.
|
||
2. **Schema extensions (non-breaking).** Added `kind` to each `block` (its district-kind string,
|
||
alongside the spec's `district` **id**) so the map & Lane B can theme a block without a district
|
||
lookup. `block.district` is the **district id**; `districts[id].kind` is the kind. Everything
|
||
else matches schema v1 exactly.
|
||
3. **`ry` convention (Lane B please read):** a lot's `ry` is the Y-rotation so a GLB modelled
|
||
facing **−Z** ends up with its facade's outward normal pointing at its `frontEdge` street.
|
||
Derivation + a worked example are in `plan.js` (`marchStrip`). `frontEdge` is a valid edge id
|
||
for every lot (stalls/dept front the nearest spine edge).
|
||
4. **`pipeline/gen_names.py` intentionally NOT shipped.** The Lane A brief offered an *optional*
|
||
Ollama word-expansion script, but `pipeline/*` is **Lane E's** owned dir per the CITY_SPEC
|
||
ownership table. Wordlists are hand-authored and checked in; runtime does zero network calls.
|
||
If you want the Ollama expansion, it should live in Lane E's pipeline or be re-assigned.
|
||
|
||
## How to eyeball it
|
||
|
||
```
|
||
cd web && python3 -m http.server 8130
|
||
# http://localhost:8130/map.html (default seed)
|
||
# http://localhost:8130/map.html?seed=42 (any uint32 seed; ?seed= drives the screenshot harness)
|
||
node web/js/citygen/selfcheck.js # acceptance harness, prints ALL GREEN
|
||
```
|
||
|
||
## Open questions for Fable
|
||
|
||
- Retail density: a core seed yields ~500 shops (continuous frontage, as briefed). Happy with that,
|
||
or dial down for Lane B's draw-call budget? (It's one constant per band; trivial to tune.)
|
||
- Want the residential collar to be a true closed ring vs the current 4-edge loop? (Current reads
|
||
fine on the map; a fuller street graph is a later enhancement.)
|
||
|
||
## Adversarial review + fixes (round 1)
|
||
|
||
Ran a 6-dimension multi-agent review of the whole lane (27 agents: determinism · schema/spec ·
|
||
geometry · chunk/JSON/perf · cross-lane contract · code quality), each finding independently
|
||
verified by a skeptic agent that ran the real generator to confirm or refute. **21 findings raised,
|
||
15 confirmed real, 6 refuted.** All confirmed defects are now **fixed and re-verified**; the
|
||
self-check grew new assertions so none of these classes can regress silently.
|
||
|
||
| # | defect (confirmed) | sev | fix |
|
||
|---|---|---|---|
|
||
| D1 | market stalls + dept anchor faced **backwards** (`ryEast` sign inverted → facades pointed west, away from the spine they front) | HIGH | `ryEast = atan2(-1,0)`; self-check now asserts every lot faces its `frontEdge` |
|
||
| D2 | **cross-block lot overlaps** at spine×cross-street corners (≈45 building-lot pairs/seed, ≤8.7m) — self-check only tested *within* a block | HIGH | reserve corner (rung near-spine inset 14→34m) + a deterministic overlap-resolution pass (demote later lot to `infill`); self-check now runs **global** cross-block OBB/SAT over building lots |
|
||
| D3 | `chunkIndex` **skipped chunks** (32m point-sampling on a 64m grid); the origin chunk under the spine was omitted; road width ignored | HIGH | Amanatides–Woo grid supercover + kerb rails (centreline ± half-width); self-check now asserts edge coverage vs a dense supercover |
|
||
| D4 | market stalls' `frontEdge` pointed at the wrong spine segment (z-band mismatch, up to 122m off) | MED | stalls → `spineEdges[2]`, dept → `spineEdges[3]` |
|
||
| D5 | name generator emitted **doubled possessives** (`Macca’s’s`) — `{First}*’s` fired both the `*` and the literal `’s` | HIGH | dropped the redundant `*`; simplified `fill()`; self-check rejects unresolved tokens |
|
||
| D6 | map.html drew the main road with `var(--roadmain)`, which canvas can't parse → the spine rendered near-invisible | MED | pass the bare custom-property name to `getcss()` (now a proper brown spine) |
|
||
| D7 | registry facades/fittings drifted from the CITY_SPEC table | LOW | **amended CITY_SPEC's shop-types table** (in this commit, flagged) to match the registry lanes import |
|
||
| + | `_sbl` Map cache was memoized onto the plan → polluted exported JSON with `"_sbl":{}` | LOW | moved the cache to a module-level var, reset per regen |
|
||
| + | `cornerBoost` could push single-storey types (video/milkbar) above their registry max | LOW | gated to types with registry max ≥ 2 |
|
||
| + | no golden-fingerprint / cross-revision drift guard; lot coords never checked finite | — | added a committed golden hash for seed 20261990 + finiteness assertions |
|
||
| + | undocumented `block.kind` / `block.district`-is-id; dead stall guard | — | documented the schema in CITY_SPEC; removed dead code |
|
||
|
||
**Refuted (6)** — investigated and dismissed with reasons, e.g. "the determinism test is a
|
||
tautology" (true that it only catches in-process nondeterminism — so I *added* the golden fingerprint
|
||
for real drift detection) and "`package.json` isn't in the ownership table" (correct, but it's
|
||
required infra — now added to the table).
|
||
|
||
**Post-fix self-check:** `✓ ALL GREEN — 1271/1271` (was 1082; +189 checks are the new
|
||
facing/cross-block-overlap/edge-coverage/finiteness/fingerprint assertions). Seed 20261990 →
|
||
"Boolarra Heads": 27 nodes, 22 edges, 35 blocks, 681 lots, 493 shops, 174 chunks. All 5 seed
|
||
screenshots re-captured. Golden fingerprint `0xb5d5cc13`.
|