PROCITY/docs/A-progress.md
m3ultra 8b7ca9aae4 Lane A (CityGen): deterministic town generator, registry, names, 2D map, self-check
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>
2026-07-14 12:06:41 +10:00

9.4 KiB
Raw Blame History

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 generatePlan(citySeed), chunkIndex(plan), CHUNK, chunkKey
2 web/js/citygen/names.js + wordlists.js seeded shop+town names, 50+ patterns, short signboard forms
3 web/js/core/registry.js SHOP_TYPES (all 9), district weights, enums, pure helpers
4 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 node web/js/citygen/selfcheck.js — full acceptance harness
+ 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 (69m) 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 35m 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 24 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, 24 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 AmanatidesWoo 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 (Maccass) — {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.