From f952f513d0745ae4adaa140c2d00ba6961c06e09 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Wed, 15 Jul 2026 04:15:12 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20A=20round=208:=20OSM=20to=20parity=20+?= =?UTF-8?q?=20a=20second=20town=20(Katoomba)=20=E2=80=94=20carried=20R7=20?= =?UTF-8?q?brief,=20full?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The carried brief (slipped R7) lands complete — all four items, qa.sh --strict GREEN. 1. Selfcheck PARITY: extracted structuralSuite(plan,label) and run the FULL invariant suite on every (source, town) — synthetic + both OSM towns get identical coverage (storeys, one-shop-per-lot, facing, within/cross-block overlap, corridor coverage, determinism, exactly-one-openLate). ALL GREEN 1727/1727 (was 1362; OSM went subset → full ×2 towns). 2. SECOND TOWN: Katoomba (Blue Mountains, 19 shops, op-shop/book heavy) — regional contrast to inner-Melbourne. osm_fixture.js → OSM_TOWNS{melbourne,katoomba}+DEFAULT_TOWN; generatePlanFor(seed, source, {town}); ?plansrc=osm&town=katoomba. Golden 0x0f652510. Melbourne 0x34cfdec0 + synthetic 0x3fa36874 UNCHANGED (town key kept off the plan). 3. "Add a town" recipe in LANE_A_NOTES (R8): extract → OSM_TOWNS → pin golden → tell F. 4. Normalization + LOG: importer bends real data to the contracts (unknown kind→opshop, hours-clamp so only one shop is openLate, video-preferred landmark w/ fallback) and logs it via {report}; selfcheck prints per-town (e.g. melbourne hoursClamped:5, openLate:video). For F: three goldens to pin, gate keyed (seed, plansrc, town); selector gained {town} — see LANE_A_NOTES R8. Screenshots docs/shots/laneA/osm-{melbourne,katoomba}.png. Co-Authored-By: Claude Opus 4.8 --- docs/A-progress.md | 28 ++- docs/LANES/LANE_A_NOTES.md | 47 +++++ docs/shots/laneA/osm-katoomba.png | Bin 0 -> 69798 bytes web/js/citygen/index.js | 13 +- web/js/citygen/osm_fixture.js | 245 +++++++++++++---------- web/js/citygen/plan_osm.js | 21 +- web/js/citygen/selfcheck.js | 312 +++++++++++++----------------- web/map.html | 8 +- 8 files changed, 374 insertions(+), 300 deletions(-) create mode 100644 docs/shots/laneA/osm-katoomba.png diff --git a/docs/A-progress.md b/docs/A-progress.md index 6ec9467..f0f78f3 100644 --- a/docs/A-progress.md +++ b/docs/A-progress.md @@ -1,8 +1,32 @@ # LANE A — CITYGEN · progress (PROCITY-A) -*Status: **all deliverables landed; self-check all-green (1362/1362); rounds 2–6 closed** (see below). +*Status: **all deliverables landed; self-check all-green (1727/1727); rounds 2–8 closed** (see below). `qa.sh --strict` GREEN.* -*Date: 2026-07-14 · owner: PROCITY-A (Opus 4.8)* +*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)* + +## Round 8 (2026-07-15) — OSM to parity + a second town (carried R7 brief, FULLY LANDED) + +The carried brief (slipped R7) is done — all four items, not a partial. + +1. **Selfcheck parity**: extracted the full invariant suite into `structuralSuite(plan, label)` and + run it on **every (source, town)** — synthetic + both OSM towns now get identical coverage + (storeys, one-shop-per-lot, solid-use, facing, within/cross-block overlap, corridor coverage, + determinism, exactly-one-openLate). **ALL GREEN 1727/1727** (was 1362 — OSM went from a subset to + the full suite ×2 towns). +2. **Second town**: **Katoomba** (Blue Mountains, 19 shops, op-shop/book heavy) — a small-regional + contrast to inner-Melbourne (95, book heavy). `?plansrc=osm&town=katoomba`. `osm_fixture.js` is now + `OSM_TOWNS { melbourne, katoomba }` + `DEFAULT_TOWN`; `generatePlanFor(seed, source, { town })`. + Golden pinned `0x0f652510`. **Melbourne golden `0x34cfdec0` and synthetic `0x3fa36874` unchanged.** +3. **"Add a town" recipe** documented in `LANE_A_NOTES.md` (R8) — extraction → `OSM_TOWNS` entry → + pin golden → tell F. Mechanical. +4. **Normalization + logging**: importer normalizes real data to the contracts (unknown kind→opshop, + hours clamp so only one shop is openLate, video-preferred landmark w/ fallback) and **logs** what it + changed via `{ report }` — selfcheck prints e.g. `melbourne: 95 shops · normalized {typesRemapped:0, + hoursClamped:5, openLate:video}`. The contract never bends; the data does. + +Extraction one-liner (Katoomba, reusable — see recipe): cell 0.05 around (-33.714, 150.312), same +KIND map + dedupe + sort-by-id as Melbourne (cell 0.06 around -37.803, 144.967). Screenshots: +`docs/shots/laneA/osm-{melbourne,katoomba}.png`. `qa.sh --strict` GREEN. ## Round 6 (2026-07-14) — the second plan source (`?plansrc=osm`), FULLY LANDED diff --git a/docs/LANES/LANE_A_NOTES.md b/docs/LANES/LANE_A_NOTES.md index bff6545..8e0a4cb 100644 --- a/docs/LANES/LANE_A_NOTES.md +++ b/docs/LANES/LANE_A_NOTES.md @@ -2,6 +2,53 @@ Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`. +## Round 8 (2026-07-15) + +### → Lane F: OSM is at parity + there's a SECOND town. Three goldens to pin, one new selector arg. + +- **Selfcheck parity done**: the full invariant suite (`structuralSuite`) now runs on **every + (source, town)** — synthetic and both OSM towns get identical coverage (storeys, one-shop-per-lot, + facing, within/cross-block overlap, corridor coverage, determinism, exactly-one-openLate, …). + `node selfcheck.js` → **ALL GREEN 1727/1727**. +- **Selector gained a town arg** (`web/js/citygen/index.js`): + `generatePlanFor(seed, source, { town })`. Shell wiring: read `&town`, pass it through — + `generatePlanFor(seed, src, { town: q.get('town') || undefined })`. `map.html` is the reference + impl (`?plansrc=osm&town=katoomba`). Unknown/absent town → the default (`melbourne`). + `osmTownKeys()` lists the available towns for a UI picker. +- **THREE goldens to pin** (seed 20261990), all asserted by selfcheck already: + | source / town | golden | + |--------------------|--------------| + | synthetic | `0x3fa36874` | + | osm / melbourne | `0x34cfdec0` | + | **osm / katoomba** | `0x0f652510` | + The determinism gate should key on **(seed, plansrc, town)**. Synthetic + melbourne goldens are + **unchanged** from round 6. +- Katoomba (Blue Mountains, 19 shops — op-shop/book heavy) is a deliberate small-regional contrast + to inner-Melbourne (95, book heavy). Both boot end-to-end; screenshots in `docs/shots/laneA/`. + +### Normalization: the importer bends the DATA to the contract, never the contract (round-8 item 4) + +Real OSM data that fights a CityPlan contract is normalized deterministically, and the change is +**logged** (selfcheck prints a per-town line; `generatePlanOSM(seed, town, { report })` fills `report`): +- unknown OSM `shop=` kind → `opshop` (`typesRemapped`); +- a shop closing ≥22:00 → clamped to 21:00 so only the one openLate landmark is late (`hoursClamped`); +- openLate landmark prefers a **video**; a town with no video falls back to another non-stall type + and `report.openLate` records it (both Melbourne + Katoomba have video, so both read `video`). + +### RECIPE — add another OSM town (mechanical, ~10 min) + +1. **Pick a cluster** in thriftgod's `city_source.json` (Overpass cache). Find a centroid + a cell + size that captures one coherent town (aim 15–150 on-theme shops). The extraction one-liner used + for melbourne/katoomba is in `A-progress.md` (round 6/8) — reuse it with your `(cLat,cLon,cell)`. +2. **Append the town** to `OSM_TOWNS` in `web/js/citygen/osm_fixture.js`: + `key: { town, source, center:{lat,lon}, shops:[{id,name,type,lat,lon,suburb}, …] }` — sort shops + by `id` (determinism). Map OSM `shop=` kinds → registry types (charity→opshop, books→book, + music→record, toys→toy, video_games→video, antiques/second_hand→opshop/pawn). +3. **Pin its golden**: run `node web/js/citygen/selfcheck.js`, read the printed + `osm/: fingerprint 0x…`, add `: 0x…` to `OSM_GOLDENS` in `selfcheck.js`. Re-run → green. +4. **Tell F** the new (seed, plansrc, town) hash for the determinism gate. Done — zero network, the + full parity suite covers it automatically. + ## Round 6 (2026-07-14) ### → Lane F (F2): the `?plansrc=osm` plan-source seam is LANDED — wire + pin diff --git a/docs/shots/laneA/osm-katoomba.png b/docs/shots/laneA/osm-katoomba.png new file mode 100644 index 0000000000000000000000000000000000000000..c18b6c9d9eb2d120f36055b5a954141cdcfa4951 GIT binary patch literal 69798 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 U=TZ!SChDK6v^+0Pnv~AOHXW literal 0 HcmV?d00001 diff --git a/web/js/citygen/index.js b/web/js/citygen/index.js index b4e1311..107871c 100644 --- a/web/js/citygen/index.js +++ b/web/js/citygen/index.js @@ -9,15 +9,16 @@ export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap } from './plan.js'; export { shopName, townName } from './names.js'; -export { generatePlanOSM } from './plan_osm.js'; +export { generatePlanOSM, osmTownKeys } from './plan_osm.js'; -// planSource selector (round 6). Default 'synthetic' → the byte-identical golden-hash generator; -// 'osm' → the real-data fixture importer. Lane F wires ?plansrc=osm to the `source` arg here so the -// shell bootstrap picks the producer without B–F caring which one ran. Unknown source → synthetic. +// planSource selector (round 6; multi-town round 8). Default 'synthetic' → the byte-identical +// golden-hash generator; 'osm' → the real-data fixture importer. Lane F wires ?plansrc=osm (+ optional +// &town=) to these args so the shell bootstrap picks the producer/town without B–F caring which +// ran. Unknown source → synthetic; unknown town → the default town. import { generatePlan as _synth } from './plan.js'; import { generatePlanOSM as _osm } from './plan_osm.js'; -export function generatePlanFor(seed, source = 'synthetic') { - return source === 'osm' ? _osm(seed) : _synth(seed); +export function generatePlanFor(seed, source = 'synthetic', opts = {}) { + return source === 'osm' ? _osm(seed, opts.town, opts) : _synth(seed); } // Convenience default so `citygen.default` also resolves to the (synthetic) generator. diff --git a/web/js/citygen/osm_fixture.js b/web/js/citygen/osm_fixture.js index 86b07f6..54aa4f8 100644 --- a/web/js/citygen/osm_fixture.js +++ b/web/js/citygen/osm_fixture.js @@ -1,110 +1,141 @@ -// PROCITY OSM plan-source fixture — Lane A (round 6). REAL secondhand/charity/music/book/ -// toy/antique shops, one compact inner-Melbourne cluster, extracted from thriftgod city_source.json -// (an Overpass cache; committed here so the game makes ZERO network calls — CITY_SPEC law). -// Generated deterministically (sorted by OSM node id). Checked in, hand-editable, byte-stable. +// PROCITY OSM plan-source fixtures — Lane A. REAL AU secondhand/charity/music/book/toy/antique +// shops extracted deterministically from thriftgod city_source.json (an Overpass cache), committed +// here so the game makes ZERO network calls (CITY_SPEC law). Each town is one compact real cluster. // -// Each shop: { name (real), type (PROCITY registry id), lat, lon, suburb }. The OSM importer -// (plan_osm.js) projects lat/lon → local metres and lays a valid CityPlan; see LANE_A_NOTES. +// Multi-town (round 8): OSM_TOWNS[key] → { town, source, center{lat,lon}, shops[{id,name,type,lat,lon,suburb}] }. +// Select with ?plansrc=osm&town=. To ADD a town see the recipe in docs/LANES/LANE_A_NOTES.md. -export const OSM_FIXTURE = { - town: "Melbourne", - source: "thriftgod/city_source.json (OSM Overpass cache, inner Melbourne)", - center: { lat: -37.803, lon: 144.967 }, - shops: [ - {"id":582845633,"name":"Jesper Junior","type":"toy","lat":-37.799331,"lon":144.97814,"suburb":""}, - {"id":589473054,"name":"Central Catholic Bookshop","type":"book","lat":-37.811707,"lon":144.962939,"suburb":"Melbourne"}, - {"id":598109860,"name":"Readings","type":"book","lat":-37.798052,"lon":144.967187,"suburb":""}, - {"id":612755480,"name":"The Haunted Bookshop","type":"book","lat":-37.815426,"lon":144.961705,"suburb":"Melbourne"}, - {"id":612771027,"name":"Hill of Content","type":"book","lat":-37.81148,"lon":144.97199,"suburb":""}, - {"id":632244266,"name":"Goldmine Records","type":"record","lat":-37.791222,"lon":144.97579,"suburb":""}, - {"id":635708269,"name":"Dymocks","type":"book","lat":-37.815462,"lon":144.96572,"suburb":"Melbourne"}, - {"id":642325730,"name":"The Paperback","type":"book","lat":-37.811675,"lon":144.971313,"suburb":"Melbourne"}, - {"id":663937314,"name":"Pop Mart","type":"toy","lat":-37.814031,"lon":144.964538,"suburb":""}, - {"id":699437448,"name":"Big Dreams","type":"toy","lat":-37.799152,"lon":144.984398,"suburb":""}, - {"id":699461941,"name":"Red Cross Shop","type":"opshop","lat":-37.799348,"lon":144.984349,"suburb":""}, - {"id":803089441,"name":"Kay Craddock Antiquarian Bookseller","type":"book","lat":-37.814775,"lon":144.968242,"suburb":""}, - {"id":990674449,"name":"Happy Valley","type":"book","lat":-37.800458,"lon":144.98416,"suburb":"Collingwood"}, - {"id":990676685,"name":"St Mark's Recycled","type":"opshop","lat":-37.80039,"lon":144.984176,"suburb":""}, - {"id":990860527,"name":"Villain","type":"toy","lat":-37.798355,"lon":144.977178,"suburb":""}, - {"id":997422797,"name":"Alice & Co.","type":"book","lat":-37.80279,"lon":144.977551,"suburb":""}, - {"id":1070219314,"name":"SOUNDMERCH","type":"record","lat":-37.805386,"lon":144.984819,"suburb":"Collingwood"}, - {"id":1072618081,"name":"White Rabbit Record Bar","type":"record","lat":-37.793645,"lon":144.93006,"suburb":""}, - {"id":1074085246,"name":"Northside Records","type":"record","lat":-37.80642,"lon":144.982482,"suburb":""}, - {"id":1104149817,"name":"Hot Potatoes Super $2 Shop","type":"opshop","lat":-37.802722,"lon":144.983783,"suburb":""}, - {"id":1379457331,"name":"Used Pty Ltd - The Stage","type":"pawn","lat":-37.799431,"lon":144.989961,"suburb":""}, - {"id":1671557044,"name":"Other Goods & Services","type":"opshop","lat":-37.7742,"lon":144.971388,"suburb":"Brunswick East"}, - {"id":2011953059,"name":"AllBooks4Less","type":"book","lat":-37.8105,"lon":144.966688,"suburb":""}, - {"id":2056959150,"name":"Tabernacle Games","type":"toy","lat":-37.78028,"lon":144.986264,"suburb":"Fitzroy North"}, - {"id":2090596140,"name":"Fitzroy Toy Library","type":"toy","lat":-37.803392,"lon":144.980385,"suburb":""}, - {"id":2105173577,"name":"Book Grocer","type":"book","lat":-37.815225,"lon":144.960413,"suburb":""}, - {"id":2108114139,"name":"Used Pty Ltd","type":"pawn","lat":-37.799008,"lon":144.983676,"suburb":""}, - {"id":2139774534,"name":"Plug Seven Records","type":"record","lat":-37.799668,"lon":144.984007,"suburb":"Fitzroy"}, - {"id":2139774821,"name":"Smith Street Bazaar","type":"pawn","lat":-37.799836,"lon":144.983957,"suburb":"Fitzroy"}, - {"id":2160049170,"name":"New International Book Shop","type":"book","lat":-37.806832,"lon":144.966259,"suburb":""}, - {"id":2164686254,"name":"Embiggen Books","type":"book","lat":-37.810103,"lon":144.966151,"suburb":"Melbourne"}, - {"id":2402316117,"name":"Already Read Bookshop","type":"book","lat":-37.783229,"lon":144.982101,"suburb":"Fitzroy North"}, - {"id":2677671238,"name":"Etruria Antiques Gallery","type":"pawn","lat":-37.799412,"lon":144.989808,"suburb":"Collingwood"}, - {"id":2688014250,"name":"The History","type":"pawn","lat":-37.802586,"lon":144.986922,"suburb":"Collingwood"}, - {"id":3412569256,"name":"Flight Experience","type":"video","lat":-37.810466,"lon":144.965145,"suburb":"Melbourne"}, - {"id":3418896122,"name":"Social Club Books","type":"book","lat":-37.796822,"lon":144.985198,"suburb":""}, - {"id":3788338576,"name":"Hylands Bookshop","type":"book","lat":-37.811456,"lon":144.966498,"suburb":"Melbourne"}, - {"id":3949641000,"name":"Goldmine Records","type":"record","lat":-37.779472,"lon":144.986947,"suburb":"Fitzroy North"}, - {"id":3949687621,"name":"Poison City","type":"record","lat":-37.795207,"lon":144.979159,"suburb":"Fitzroy"}, - {"id":4972218985,"name":"Retro Wolf","type":"toy","lat":-37.794166,"lon":144.949683,"suburb":""}, - {"id":5406503614,"name":"Readings","type":"book","lat":-37.809718,"lon":144.964589,"suburb":""}, - {"id":6228833108,"name":"Gregory's Antiques and Lights","type":"pawn","lat":-37.785753,"lon":144.981522,"suburb":""}, - {"id":6237887803,"name":"Readings","type":"book","lat":-37.809443,"lon":144.966332,"suburb":""}, - {"id":6507557579,"name":"Record Paradise","type":"record","lat":-37.773631,"lon":144.960868,"suburb":"Brunswick"}, - {"id":6649578474,"name":"Readings Kids","type":"book","lat":-37.79792,"lon":144.967205,"suburb":""}, - {"id":6680206192,"name":"Brotherhood of St Laurence Op Shop","type":"opshop","lat":-37.773636,"lon":144.971215,"suburb":""}, - {"id":6687064305,"name":"Vinnies","type":"opshop","lat":-37.774974,"lon":144.960631,"suburb":""}, - {"id":6702087933,"name":"Legacy","type":"opshop","lat":-37.811229,"lon":144.964332,"suburb":""}, - {"id":7202622331,"name":"Retro Depot","type":"opshop","lat":-37.782348,"lon":144.977928,"suburb":""}, - {"id":7273662704,"name":"Bookshop","type":"book","lat":-37.774882,"lon":144.960955,"suburb":""}, - {"id":7391506777,"name":"Ace Antiques","type":"pawn","lat":-37.803155,"lon":144.94892,"suburb":""}, - {"id":9097507475,"name":"Vinyl Space","type":"record","lat":-37.799535,"lon":144.988755,"suburb":""}, - {"id":9336124056,"name":"World Food Books","type":"book","lat":-37.816635,"lon":144.966746,"suburb":""}, - {"id":10048390715,"name":"EB Games","type":"video","lat":-37.816891,"lon":144.964598,"suburb":""}, - {"id":10598284186,"name":"Mui Karaoke Rooms","type":"record","lat":-37.811449,"lon":144.96435,"suburb":""}, - {"id":10611335971,"name":"Mind Games","type":"toy","lat":-37.814339,"lon":144.965791,"suburb":""}, - {"id":10713580252,"name":"City Of Vintage","type":"opshop","lat":-37.773999,"lon":144.960805,"suburb":""}, - {"id":10729013736,"name":"Epic Vintage","type":"opshop","lat":-37.776826,"lon":144.960351,"suburb":""}, - {"id":11017168508,"name":"Mary Martin Bookshop","type":"book","lat":-37.807847,"lon":144.957651,"suburb":""}, - {"id":11031787378,"name":"The Book Grocer","type":"book","lat":-37.815477,"lon":144.9514,"suburb":""}, - {"id":11118777232,"name":"Elite Office Machines Co.","type":"pawn","lat":-37.797065,"lon":144.968274,"suburb":""}, - {"id":11129101658,"name":"F.J. Rocca","type":"pawn","lat":-37.797543,"lon":144.969696,"suburb":""}, - {"id":11133299408,"name":"Brotherhood of St Lawrence","type":"opshop","lat":-37.814546,"lon":144.964109,"suburb":""}, - {"id":11135649030,"name":"Warhammer","type":"video","lat":-37.813084,"lon":144.970498,"suburb":""}, - {"id":11140063962,"name":"The Salvation Army","type":"opshop","lat":-37.817091,"lon":144.953989,"suburb":""}, - {"id":11162131777,"name":"Mary Martin Bookshop","type":"book","lat":-37.807745,"lon":144.95666,"suburb":""}, - {"id":11185870784,"name":"EB Games","type":"video","lat":-37.812749,"lon":144.937428,"suburb":"Docklands"}, - {"id":11244890775,"name":"North Melbourne Books","type":"book","lat":-37.802823,"lon":144.949112,"suburb":""}, - {"id":11287631726,"name":"Plenty of Games","type":"video","lat":-37.81572,"lon":144.964811,"suburb":""}, - {"id":11287631727,"name":"The Gaming Verse","type":"video","lat":-37.817709,"lon":144.961688,"suburb":""}, - {"id":11555994364,"name":"Strange World Records","type":"record","lat":-37.77301,"lon":144.971585,"suburb":""}, - {"id":11660848323,"name":"Melbourne Book Market","type":"book","lat":-37.806808,"lon":144.956428,"suburb":""}, - {"id":11957731033,"name":"Archie Brothers STRIKE","type":"video","lat":-37.811779,"lon":144.936627,"suburb":""}, - {"id":12175749075,"name":"I Heart Snacks","type":"video","lat":-37.811059,"lon":144.963269,"suburb":""}, - {"id":12175767221,"name":"Pop Mart","type":"toy","lat":-37.810856,"lon":144.963047,"suburb":""}, - {"id":12175767225,"name":"Heydays","type":"toy","lat":-37.810779,"lon":144.963015,"suburb":""}, - {"id":12175767227,"name":"I Am","type":"toy","lat":-37.810815,"lon":144.963167,"suburb":""}, - {"id":12175779440,"name":"Merry Seasons","type":"toy","lat":-37.810464,"lon":144.962038,"suburb":""}, - {"id":12320608201,"name":"Amplify Bookstore","type":"book","lat":-37.806902,"lon":144.955544,"suburb":""}, - {"id":12418141339,"name":"The Little Bookroom","type":"book","lat":-37.771822,"lon":144.978527,"suburb":""}, - {"id":12432795349,"name":"One Stop Anime","type":"video","lat":-37.815261,"lon":144.965727,"suburb":""}, - {"id":12437671918,"name":"Mooii","type":"toy","lat":-37.810519,"lon":144.96303,"suburb":""}, - {"id":12511070907,"name":"Rotart Charity Books","type":"opshop","lat":-37.812736,"lon":144.938173,"suburb":""}, - {"id":12511070911,"name":"Books Gifts Station","type":"book","lat":-37.813078,"lon":144.938613,"suburb":""}, - {"id":12511098032,"name":"Popclutcha","type":"video","lat":-37.813266,"lon":144.93777,"suburb":""}, - {"id":12511098647,"name":"Toyworld","type":"toy","lat":-37.813905,"lon":144.938006,"suburb":""}, - {"id":12611273300,"name":"Dungeon of Magic","type":"video","lat":-37.814504,"lon":144.964088,"suburb":""}, - {"id":12611672199,"name":"All Star Comics","type":"book","lat":-37.817545,"lon":144.961612,"suburb":"Melbourne"}, - {"id":12686552001,"name":"City Basement Books","type":"book","lat":-37.818532,"lon":144.963194,"suburb":""}, - {"id":12686552101,"name":"Metropolis Bookshop","type":"book","lat":-37.812039,"lon":144.965298,"suburb":""}, - {"id":12695960455,"name":"Bookshop by Uro","type":"book","lat":-37.799835,"lon":144.985564,"suburb":""}, - {"id":13075851874,"name":"Licorice Pie","type":"record","lat":-37.799856,"lon":144.985715,"suburb":""}, - {"id":13206134926,"name":"Sticky Institute","type":"book","lat":-37.806746,"lon":144.960086,"suburb":""}, - {"id":13229707300,"name":"Readings","type":"book","lat":-37.81289,"lon":144.963426,"suburb":""}, - {"id":13967406755,"name":"Law Bookshop","type":"book","lat":-37.815692,"lon":144.958884,"suburb":"Melbourne"} - ], +export const OSM_TOWNS = { + melbourne: { + town: "Melbourne", + source: "thriftgod/city_source.json (OSM Overpass cache, inner Melbourne)", + center: {"lat":-37.803,"lon":144.967}, + shops: [ + {"id":582845633,"name":"Jesper Junior","type":"toy","lat":-37.799331,"lon":144.97814,"suburb":""}, + {"id":589473054,"name":"Central Catholic Bookshop","type":"book","lat":-37.811707,"lon":144.962939,"suburb":"Melbourne"}, + {"id":598109860,"name":"Readings","type":"book","lat":-37.798052,"lon":144.967187,"suburb":""}, + {"id":612755480,"name":"The Haunted Bookshop","type":"book","lat":-37.815426,"lon":144.961705,"suburb":"Melbourne"}, + {"id":612771027,"name":"Hill of Content","type":"book","lat":-37.81148,"lon":144.97199,"suburb":""}, + {"id":632244266,"name":"Goldmine Records","type":"record","lat":-37.791222,"lon":144.97579,"suburb":""}, + {"id":635708269,"name":"Dymocks","type":"book","lat":-37.815462,"lon":144.96572,"suburb":"Melbourne"}, + {"id":642325730,"name":"The Paperback","type":"book","lat":-37.811675,"lon":144.971313,"suburb":"Melbourne"}, + {"id":663937314,"name":"Pop Mart","type":"toy","lat":-37.814031,"lon":144.964538,"suburb":""}, + {"id":699437448,"name":"Big Dreams","type":"toy","lat":-37.799152,"lon":144.984398,"suburb":""}, + {"id":699461941,"name":"Red Cross Shop","type":"opshop","lat":-37.799348,"lon":144.984349,"suburb":""}, + {"id":803089441,"name":"Kay Craddock Antiquarian Bookseller","type":"book","lat":-37.814775,"lon":144.968242,"suburb":""}, + {"id":990674449,"name":"Happy Valley","type":"book","lat":-37.800458,"lon":144.98416,"suburb":"Collingwood"}, + {"id":990676685,"name":"St Mark's Recycled","type":"opshop","lat":-37.80039,"lon":144.984176,"suburb":""}, + {"id":990860527,"name":"Villain","type":"toy","lat":-37.798355,"lon":144.977178,"suburb":""}, + {"id":997422797,"name":"Alice & Co.","type":"book","lat":-37.80279,"lon":144.977551,"suburb":""}, + {"id":1070219314,"name":"SOUNDMERCH","type":"record","lat":-37.805386,"lon":144.984819,"suburb":"Collingwood"}, + {"id":1072618081,"name":"White Rabbit Record Bar","type":"record","lat":-37.793645,"lon":144.93006,"suburb":""}, + {"id":1074085246,"name":"Northside Records","type":"record","lat":-37.80642,"lon":144.982482,"suburb":""}, + {"id":1104149817,"name":"Hot Potatoes Super $2 Shop","type":"opshop","lat":-37.802722,"lon":144.983783,"suburb":""}, + {"id":1379457331,"name":"Used Pty Ltd - The Stage","type":"pawn","lat":-37.799431,"lon":144.989961,"suburb":""}, + {"id":1671557044,"name":"Other Goods & Services","type":"opshop","lat":-37.7742,"lon":144.971388,"suburb":"Brunswick East"}, + {"id":2011953059,"name":"AllBooks4Less","type":"book","lat":-37.8105,"lon":144.966688,"suburb":""}, + {"id":2056959150,"name":"Tabernacle Games","type":"toy","lat":-37.78028,"lon":144.986264,"suburb":"Fitzroy North"}, + {"id":2090596140,"name":"Fitzroy Toy Library","type":"toy","lat":-37.803392,"lon":144.980385,"suburb":""}, + {"id":2105173577,"name":"Book Grocer","type":"book","lat":-37.815225,"lon":144.960413,"suburb":""}, + {"id":2108114139,"name":"Used Pty Ltd","type":"pawn","lat":-37.799008,"lon":144.983676,"suburb":""}, + {"id":2139774534,"name":"Plug Seven Records","type":"record","lat":-37.799668,"lon":144.984007,"suburb":"Fitzroy"}, + {"id":2139774821,"name":"Smith Street Bazaar","type":"pawn","lat":-37.799836,"lon":144.983957,"suburb":"Fitzroy"}, + {"id":2160049170,"name":"New International Book Shop","type":"book","lat":-37.806832,"lon":144.966259,"suburb":""}, + {"id":2164686254,"name":"Embiggen Books","type":"book","lat":-37.810103,"lon":144.966151,"suburb":"Melbourne"}, + {"id":2402316117,"name":"Already Read Bookshop","type":"book","lat":-37.783229,"lon":144.982101,"suburb":"Fitzroy North"}, + {"id":2677671238,"name":"Etruria Antiques Gallery","type":"pawn","lat":-37.799412,"lon":144.989808,"suburb":"Collingwood"}, + {"id":2688014250,"name":"The History","type":"pawn","lat":-37.802586,"lon":144.986922,"suburb":"Collingwood"}, + {"id":3412569256,"name":"Flight Experience","type":"video","lat":-37.810466,"lon":144.965145,"suburb":"Melbourne"}, + {"id":3418896122,"name":"Social Club Books","type":"book","lat":-37.796822,"lon":144.985198,"suburb":""}, + {"id":3788338576,"name":"Hylands Bookshop","type":"book","lat":-37.811456,"lon":144.966498,"suburb":"Melbourne"}, + {"id":3949641000,"name":"Goldmine Records","type":"record","lat":-37.779472,"lon":144.986947,"suburb":"Fitzroy North"}, + {"id":3949687621,"name":"Poison City","type":"record","lat":-37.795207,"lon":144.979159,"suburb":"Fitzroy"}, + {"id":4972218985,"name":"Retro Wolf","type":"toy","lat":-37.794166,"lon":144.949683,"suburb":""}, + {"id":5406503614,"name":"Readings","type":"book","lat":-37.809718,"lon":144.964589,"suburb":""}, + {"id":6228833108,"name":"Gregory's Antiques and Lights","type":"pawn","lat":-37.785753,"lon":144.981522,"suburb":""}, + {"id":6237887803,"name":"Readings","type":"book","lat":-37.809443,"lon":144.966332,"suburb":""}, + {"id":6507557579,"name":"Record Paradise","type":"record","lat":-37.773631,"lon":144.960868,"suburb":"Brunswick"}, + {"id":6649578474,"name":"Readings Kids","type":"book","lat":-37.79792,"lon":144.967205,"suburb":""}, + {"id":6680206192,"name":"Brotherhood of St Laurence Op Shop","type":"opshop","lat":-37.773636,"lon":144.971215,"suburb":""}, + {"id":6687064305,"name":"Vinnies","type":"opshop","lat":-37.774974,"lon":144.960631,"suburb":""}, + {"id":6702087933,"name":"Legacy","type":"opshop","lat":-37.811229,"lon":144.964332,"suburb":""}, + {"id":7202622331,"name":"Retro Depot","type":"opshop","lat":-37.782348,"lon":144.977928,"suburb":""}, + {"id":7273662704,"name":"Bookshop","type":"book","lat":-37.774882,"lon":144.960955,"suburb":""}, + {"id":7391506777,"name":"Ace Antiques","type":"pawn","lat":-37.803155,"lon":144.94892,"suburb":""}, + {"id":9097507475,"name":"Vinyl Space","type":"record","lat":-37.799535,"lon":144.988755,"suburb":""}, + {"id":9336124056,"name":"World Food Books","type":"book","lat":-37.816635,"lon":144.966746,"suburb":""}, + {"id":10048390715,"name":"EB Games","type":"video","lat":-37.816891,"lon":144.964598,"suburb":""}, + {"id":10598284186,"name":"Mui Karaoke Rooms","type":"record","lat":-37.811449,"lon":144.96435,"suburb":""}, + {"id":10611335971,"name":"Mind Games","type":"toy","lat":-37.814339,"lon":144.965791,"suburb":""}, + {"id":10713580252,"name":"City Of Vintage","type":"opshop","lat":-37.773999,"lon":144.960805,"suburb":""}, + {"id":10729013736,"name":"Epic Vintage","type":"opshop","lat":-37.776826,"lon":144.960351,"suburb":""}, + {"id":11017168508,"name":"Mary Martin Bookshop","type":"book","lat":-37.807847,"lon":144.957651,"suburb":""}, + {"id":11031787378,"name":"The Book Grocer","type":"book","lat":-37.815477,"lon":144.9514,"suburb":""}, + {"id":11118777232,"name":"Elite Office Machines Co.","type":"pawn","lat":-37.797065,"lon":144.968274,"suburb":""}, + {"id":11129101658,"name":"F.J. Rocca","type":"pawn","lat":-37.797543,"lon":144.969696,"suburb":""}, + {"id":11133299408,"name":"Brotherhood of St Lawrence","type":"opshop","lat":-37.814546,"lon":144.964109,"suburb":""}, + {"id":11135649030,"name":"Warhammer","type":"video","lat":-37.813084,"lon":144.970498,"suburb":""}, + {"id":11140063962,"name":"The Salvation Army","type":"opshop","lat":-37.817091,"lon":144.953989,"suburb":""}, + {"id":11162131777,"name":"Mary Martin Bookshop","type":"book","lat":-37.807745,"lon":144.95666,"suburb":""}, + {"id":11185870784,"name":"EB Games","type":"video","lat":-37.812749,"lon":144.937428,"suburb":"Docklands"}, + {"id":11244890775,"name":"North Melbourne Books","type":"book","lat":-37.802823,"lon":144.949112,"suburb":""}, + {"id":11287631726,"name":"Plenty of Games","type":"video","lat":-37.81572,"lon":144.964811,"suburb":""}, + {"id":11287631727,"name":"The Gaming Verse","type":"video","lat":-37.817709,"lon":144.961688,"suburb":""}, + {"id":11555994364,"name":"Strange World Records","type":"record","lat":-37.77301,"lon":144.971585,"suburb":""}, + {"id":11660848323,"name":"Melbourne Book Market","type":"book","lat":-37.806808,"lon":144.956428,"suburb":""}, + {"id":11957731033,"name":"Archie Brothers STRIKE","type":"video","lat":-37.811779,"lon":144.936627,"suburb":""}, + {"id":12175749075,"name":"I Heart Snacks","type":"video","lat":-37.811059,"lon":144.963269,"suburb":""}, + {"id":12175767221,"name":"Pop Mart","type":"toy","lat":-37.810856,"lon":144.963047,"suburb":""}, + {"id":12175767225,"name":"Heydays","type":"toy","lat":-37.810779,"lon":144.963015,"suburb":""}, + {"id":12175767227,"name":"I Am","type":"toy","lat":-37.810815,"lon":144.963167,"suburb":""}, + {"id":12175779440,"name":"Merry Seasons","type":"toy","lat":-37.810464,"lon":144.962038,"suburb":""}, + {"id":12320608201,"name":"Amplify Bookstore","type":"book","lat":-37.806902,"lon":144.955544,"suburb":""}, + {"id":12418141339,"name":"The Little Bookroom","type":"book","lat":-37.771822,"lon":144.978527,"suburb":""}, + {"id":12432795349,"name":"One Stop Anime","type":"video","lat":-37.815261,"lon":144.965727,"suburb":""}, + {"id":12437671918,"name":"Mooii","type":"toy","lat":-37.810519,"lon":144.96303,"suburb":""}, + {"id":12511070907,"name":"Rotart Charity Books","type":"opshop","lat":-37.812736,"lon":144.938173,"suburb":""}, + {"id":12511070911,"name":"Books Gifts Station","type":"book","lat":-37.813078,"lon":144.938613,"suburb":""}, + {"id":12511098032,"name":"Popclutcha","type":"video","lat":-37.813266,"lon":144.93777,"suburb":""}, + {"id":12511098647,"name":"Toyworld","type":"toy","lat":-37.813905,"lon":144.938006,"suburb":""}, + {"id":12611273300,"name":"Dungeon of Magic","type":"video","lat":-37.814504,"lon":144.964088,"suburb":""}, + {"id":12611672199,"name":"All Star Comics","type":"book","lat":-37.817545,"lon":144.961612,"suburb":"Melbourne"}, + {"id":12686552001,"name":"City Basement Books","type":"book","lat":-37.818532,"lon":144.963194,"suburb":""}, + {"id":12686552101,"name":"Metropolis Bookshop","type":"book","lat":-37.812039,"lon":144.965298,"suburb":""}, + {"id":12695960455,"name":"Bookshop by Uro","type":"book","lat":-37.799835,"lon":144.985564,"suburb":""}, + {"id":13075851874,"name":"Licorice Pie","type":"record","lat":-37.799856,"lon":144.985715,"suburb":""}, + {"id":13206134926,"name":"Sticky Institute","type":"book","lat":-37.806746,"lon":144.960086,"suburb":""}, + {"id":13229707300,"name":"Readings","type":"book","lat":-37.81289,"lon":144.963426,"suburb":""}, + {"id":13967406755,"name":"Law Bookshop","type":"book","lat":-37.815692,"lon":144.958884,"suburb":"Melbourne"} + ], + }, + katoomba: { + town: "Katoomba", + source: "thriftgod/city_source.json (OSM Overpass cache, Katoomba / Blue Mountains)", + center: {"lat":-33.714,"lon":150.312}, + shops: [ + {"id":4047416904,"name":"Anglicare","type":"opshop","lat":-33.716738,"lon":150.310919,"suburb":""}, + {"id":4047416906,"name":"Cat Defence Op Shop","type":"opshop","lat":-33.717477,"lon":150.31186,"suburb":""}, + {"id":4047416910,"name":"Red Shield Family Store","type":"opshop","lat":-33.717291,"lon":150.309527,"suburb":""}, + {"id":4047416911,"name":"Steven Walter","type":"opshop","lat":-33.710934,"lon":150.309931,"suburb":""}, + {"id":4047416913,"name":"Vinnies","type":"opshop","lat":-33.717464,"lon":150.311782,"suburb":""}, + {"id":4958417278,"name":"Anglicare Op Shops","type":"opshop","lat":-33.714949,"lon":150.311257,"suburb":""}, + {"id":4958417724,"name":"Katoomba Music","type":"record","lat":-33.715074,"lon":150.311532,"suburb":""}, + {"id":5237464912,"name":"Katoomba Vintage Emporium","type":"pawn","lat":-33.713729,"lon":150.311403,"suburb":""}, + {"id":6938379064,"name":"Katoomba Antique Centre","type":"pawn","lat":-33.712206,"lon":150.310654,"suburb":""}, + {"id":6938414604,"name":"Salvos","type":"opshop","lat":-33.714654,"lon":150.311316,"suburb":"Katoomba"}, + {"id":6938414612,"name":"EB Games","type":"video","lat":-33.714851,"lon":150.31127,"suburb":""}, + {"id":6938921533,"name":"MacArthur's Arcade","type":"pawn","lat":-33.71389,"lon":150.311766,"suburb":""}, + {"id":6938921534,"name":"Mr Pickwick's Bookshop","type":"book","lat":-33.713915,"lon":150.311761,"suburb":""}, + {"id":7040012373,"name":"Black Antiques","type":"pawn","lat":-33.709806,"lon":150.309278,"suburb":""}, + {"id":7110813637,"name":"Steven Walter Children's Cancer Foundation","type":"opshop","lat":-33.712811,"lon":150.321286,"suburb":"Katoomba"}, + {"id":9231634025,"name":"Little Lost Bookshop","type":"book","lat":-33.716393,"lon":150.310835,"suburb":""}, + {"id":9577859712,"name":"B'COS Agora Second Hand Bookshop","type":"book","lat":-33.714295,"lon":150.311173,"suburb":""}, + {"id":9791871398,"name":"The Velvet Fog","type":"record","lat":-33.712544,"lon":150.311566,"suburb":""}, + {"id":11242974232,"name":"Sanctus Catholic Books and Gift Shop","type":"book","lat":-33.715718,"lon":150.31095,"suburb":""} + ], + }, }; + +export const DEFAULT_TOWN = "melbourne"; +// Back-compat single-town export (pre-round-8 callers). +export const OSM_FIXTURE = OSM_TOWNS[DEFAULT_TOWN]; diff --git a/web/js/citygen/plan_osm.js b/web/js/citygen/plan_osm.js index e0f5ea7..3106543 100644 --- a/web/js/citygen/plan_osm.js +++ b/web/js/citygen/plan_osm.js @@ -13,7 +13,7 @@ import { rng, seedFor, mulberry32, pick, frange } from '../core/prng.js'; import { SHOP_TYPES } from '../core/registry.js'; -import { OSM_FIXTURE } from './osm_fixture.js'; +import { OSM_TOWNS, DEFAULT_TOWN } from './osm_fixture.js'; const r2 = v => Math.round(v * 100) / 100; const r4 = v => Math.round(v * 10000) / 10000; @@ -35,9 +35,12 @@ function signOf(name) { return (s || String(name).slice(0, 15)).toUpperCase(); } -export function generatePlanOSM(citySeed) { +export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) { citySeed = (citySeed >>> 0); - const fx = OSM_FIXTURE; + const fx = OSM_TOWNS[town] || OSM_TOWNS[DEFAULT_TOWN]; + // Normalization log: what the importer had to bend to fit the CityPlan contracts (real OSM data + // "doesn't bend the contract"). Surfaced via opts.report (selfcheck prints it) — never on the plan. + const norm = { town: fx.town, source: 'osm', shops: fx.shops.length, typesRemapped: 0, hoursClamped: 0, openLate: 'video' }; const nodes = [], edges = [], districts = [], blocks = [], lots = [], shops = []; let nid = 0, eid = 0, did = 0, bid = 0, lid = 0, sid = 0; @@ -55,6 +58,7 @@ export function generatePlanOSM(citySeed) { function addShop(lotId, fixtureShop) { const id = sid++; const type = SHOP_TYPES[fixtureShop.type] ? fixtureShop.type : 'opshop'; + if (type !== fixtureShop.type) norm.typesRemapped++; // unknown OSM kind → opshop (normalization) const reg = SHOP_TYPES[type]; const seed = seedFor(citySeed, 'osmshop', fixtureShop.id); const sr = mulberry32(seed); @@ -64,6 +68,7 @@ export function generatePlanOSM(citySeed) { if (sr() < 0.3) open = clampHour(open + (sr() < 0.5 ? -1 : 1)); if (sr() < 0.3) close = clampHour(close + (sr() < 0.5 ? -1 : 1)); if (close <= open) close = clampHour(open + 2); + if (close > LATE_HOUR - 1) norm.hoursClamped++; close = Math.min(close, LATE_HOUR - 1); // only the openLate landmark reaches ≥ LATE_HOUR shops.push({ id, lot: lotId, type, name: fixtureShop.name, sign: signOf(fixtureShop.name), seed, facadeSkin, storeys, hours: [open, close] }); @@ -120,6 +125,7 @@ export function generatePlanOSM(citySeed) { if (shops.length) { const videos = shops.filter(s => s.type === 'video'); const pool = videos.length ? videos : shops.filter(s => s.type !== 'stall'); + norm.openLate = videos.length ? 'video' : (pool.length ? pool[0].type + ' (no video in town)' : 'none'); if (pool.length) { const lr = rng(citySeed, 'osm-openlate', 0); const chosen = pick(lr, pool); @@ -144,11 +150,13 @@ export function generatePlanOSM(citySeed) { const M = 40; const size = { w: Math.max(256, Math.ceil((maxx - minx) + 2 * M)), d: Math.max(256, Math.ceil((maxz - minz) + 2 * M)) }; + if (opts.report) Object.assign(opts.report, norm); // caller-supplied log sink (selfcheck prints it) + return { version: 1, citySeed, - source: 'osm', - name: fx.town || 'Melbourne', + source: 'osm', // town key stays OFF the plan (keeps the Melbourne golden frozen); the + name: fx.town || 'Melbourne', // display name + the selector arg convey which town this is. size, districts, streets: { nodes, edges }, @@ -157,3 +165,6 @@ export function generatePlanOSM(citySeed) { shops, }; } + +// The catalogue of available OSM town keys (for the shell/map selector + selfcheck iteration). +export function osmTownKeys() { return Object.keys(OSM_TOWNS); } diff --git a/web/js/citygen/selfcheck.js b/web/js/citygen/selfcheck.js index 8c91ea0..4b53057 100644 --- a/web/js/citygen/selfcheck.js +++ b/web/js/citygen/selfcheck.js @@ -8,7 +8,7 @@ import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js'; -import { generatePlanOSM } from './plan_osm.js'; +import { generatePlanOSM, osmTownKeys } from './plan_osm.js'; import { allFacadeSkins, SHOP_TYPES } from '../core/registry.js'; import { xmur3 } from '../core/prng.js'; @@ -30,6 +30,113 @@ function nearestOnSeg(px, pz, ax, az, bx, bz) { let t = ((px - ax) * dx + (pz - az) * dz) / L2; t = Math.max(0, Math.min(1, t)); return [ax + dx * t, az + dz * t]; } +const isFiniteNum = v => typeof v === 'number' && Number.isFinite(v); + +// The FULL structural invariant suite for ANY CityPlan — synthetic OR osm (round-8 parity: both +// sources get identical coverage). `label` prefixes each check. Synthetic-only "brief" checks +// (market/arcade/dept/milkbar) live at the call site, guarded, since a real town has none. +function structuralSuite(plan, label) { + const edgeIds = new Set(plan.streets.edges.map(e => e.id)); + const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); + const nodeIds = new Set(plan.streets.nodes.map(n => n.id)); + const blockIds = new Set(plan.blocks.map(b => b.id)); + const lotIds = new Set(plan.lots.map(l => l.id)); + const districtIds = new Set(plan.districts.map(d => d.id)); + + ok(plan.streets.nodes.every(n => isFiniteNum(n.x) && isFiniteNum(n.z)), `${label}: node coords finite`); + ok(plan.lots.every(l => isFiniteNum(l.x) && isFiniteNum(l.z) && isFiniteNum(l.w) && isFiniteNum(l.d) && isFiniteNum(l.ry)), `${label}: lot numbers finite`); + ok(plan.blocks.every(b => b.poly.every(p => isFiniteNum(p[0]) && isFiniteNum(p[1]))), `${label}: block poly finite`); + ok(plan.streets.edges.every(e => nodeIds.has(e.a) && nodeIds.has(e.b)), `${label}: every edge references real nodes`); + ok(plan.blocks.every(b => districtIds.has(b.district)), `${label}: every block in a real district`); + ok(plan.lots.every(l => blockIds.has(l.block)), `${label}: every lot in a real block`); + ok(plan.lots.every(l => edgeIds.has(l.frontEdge)), `${label}: every lot has a valid frontEdge`); + ok(plan.lots.every(l => l.w > 0 && l.d > 0), `${label}: every lot has positive size`); + ok(plan.shops.every(sh => lotIds.has(sh.lot)), `${label}: every shop has a real lot`); + ok(plan.shops.every(sh => SHOP_TYPES[sh.type]), `${label}: every shop has a known type`); + ok(plan.shops.every(sh => SHOP_TYPES[sh.type].facades.includes(sh.facadeSkin)), `${label}: every facade is in its type's pool`); + ok(plan.shops.every(sh => sh.hours[0] >= 0 && sh.hours[1] <= 23 && sh.hours[1] > sh.hours[0]), `${label}: every shop has sane hours`); + ok(plan.shops.every(sh => sh.name && sh.sign), `${label}: every shop is named`); + ok(plan.shops.every(sh => !/[{}]|undefined/.test(sh.name + sh.sign)), `${label}: no unresolved tokens / undefined in names`); + ok(plan.shops.every(sh => { + const [mn, mx] = SHOP_TYPES[sh.type].storeys; const pmax = mx >= 2 ? Math.min(3, mx + 1) : mx; + return sh.storeys >= mn && sh.storeys <= pmax; + }), `${label}: storeys within registry range (corner-boost ≤ min(max+1,3); single-storey never boosted)`); + + const lotShopCounts = {}; + for (const sh of plan.shops) lotShopCounts[sh.lot] = (lotShopCounts[sh.lot] || 0) + 1; + ok(Object.values(lotShopCounts).every(c => c === 1), `${label}: no lot has two shops`); + ok(plan.shops.every(sh => SOLID.has(plan.lots[sh.lot].use)), `${label}: every shop sits on a solid lot`); + + let backwards = null; + for (const l of plan.lots) { + const e = plan.streets.edges.find(x => x.id === l.frontEdge); const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; + const [nx, nz] = nearestOnSeg(l.x, l.z, a.x, a.z, b.x, b.z); const [fx, fz] = facing(l); + if ((nx - l.x) * fx + (nz - l.z) * fz < -0.01) { backwards = l.id; break; } + } + ok(backwards === null, `${label}: every lot faces its frontEdge` + (backwards !== null ? ` (lot ${backwards} backwards)` : '')); + + const byBlock = {}; + for (const l of plan.lots) (byBlock[l.block] ||= []).push(l); + let inBlock = null; + for (const arr of Object.values(byBlock)) { + const cq = arr.map(lotCorners); + for (let i = 0; i < arr.length && !inBlock; i++) for (let j = i + 1; j < arr.length; j++) + if (obbOverlap(cq[i], cq[j])) { inBlock = [arr[i].id, arr[j].id]; break; } + if (inBlock) break; + } + ok(!inBlock, `${label}: no overlapping lots within a block` + (inBlock ? ` (lots ${inBlock})` : '')); + + const solids = plan.lots.filter(l => SOLID.has(l.use)); + const cs = solids.map(lotCorners); + const aabb = cs.map(q => { let a = [1e9, 1e9, -1e9, -1e9]; for (const [x, z] of q) { a[0] = Math.min(a[0], x); a[1] = Math.min(a[1], z); a[2] = Math.max(a[2], x); a[3] = Math.max(a[3], z); } return a; }); + let crossBlock = null; + for (let i = 0; i < solids.length && !crossBlock; i++) for (let j = i + 1; j < solids.length; j++) { + if (solids[i].block === solids[j].block) continue; + const A = aabb[i], B = aabb[j]; + if (A[2] < B[0] || B[2] < A[0] || A[3] < B[1] || B[3] < A[1]) continue; + if (obbOverlap(cs[i], cs[j])) { crossBlock = [solids[i].id, solids[j].id]; break; } + } + ok(!crossBlock, `${label}: no overlapping building lots across blocks` + (crossBlock ? ` (lots ${crossBlock})` : '')); + + const idx = chunkIndex(plan); + const covered = new Set(); + for (const cellB of Object.values(idx.chunks)) { + for (const id of cellB.lots) covered.add(id); + ok(cellB.lots.every(id => lotIds.has(id)) && cellB.shops.every(id => plan.shops.some(sh => sh.id === id)) && cellB.edges.every(id => edgeIds.has(id)), `${label}: chunk buckets reference real ids`); + } + ok(plan.lots.every(l => covered.has(l.id)), `${label}: chunkIndex covers every lot`); + + const cell = v => Math.floor(v / CHUNK); + let edgeGap = null; + for (const e of plan.streets.edges) { + const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; + const dx = b.x - a.x, dz = b.z - a.z, len = Math.hypot(dx, dz) || 1; + const hw = (e.width || 0) / 2, px = -dz / len * hw, pz = dx / len * hw; + const steps = Math.max(1, Math.ceil(len / 2)); + for (const f of [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) { + for (let k = 0; k <= steps && !edgeGap; k++) { + const x = a.x + dx * k / steps + px * f, z = a.z + dz * k / steps + pz * f; + const bk = idx.chunks[`${cell(x)},${cell(z)}`]; + if (!bk || !bk.edges.includes(e.id)) edgeGap = [e.id, f, cell(x), cell(z)]; + } + if (edgeGap) break; + } + if (edgeGap) break; + } + ok(!edgeGap, `${label}: chunkIndex covers the full road corridor (road+verge)` + (edgeGap ? ` (edge ${edgeGap[0]} @ off ${edgeGap[1]} missing @ ${edgeGap[2]},${edgeGap[3]})` : '')); + + ok(JSON.stringify(JSON.parse(JSON.stringify(plan))) === JSON.stringify(plan), `${label}: JSON round-trip lossless`); + + // exactly one late-night landmark; openLate field ⟺ hours[1]≥22 (same shop); it's the video (or, + // for a town with no video at all, the fallback landmark). Holds for synthetic AND osm. + const late = plan.shops.filter(sh => sh.openLate); + const ge22 = plan.shops.filter(sh => sh.hours[1] >= 22); + ok(late.length === 1, `${label}: exactly one open-late shop (${late.length})`); + ok(ge22.length === 1, `${label}: exactly one shop closes ≥22:00 (${ge22.length})`); + ok(late.length === 1 && ge22.length === 1 && late[0] === ge22[0], `${label}: openLate field ⟺ hours[1]≥22 (same shop)`); + ok(late.length === 1 && late[0].hours[1] >= 22 && late[0].type !== 'stall', `${label}: open-late shop closes ≥22:00 and isn't a stall`); + ok(late.length === 1 && (late[0].type === 'video' || !plan.shops.some(x => x.type === 'video')), `${label}: the open-late shop is the video rental (or fallback if no video)`); +} // ── 1. determinism: two runs byte-identical ───────────────────────────────────────── section('determinism (deep-equal across two runs)'); @@ -56,191 +163,42 @@ for (const s of SEEDS) { ok(ms < 100, `seed ${s}: generated in ${ms.toFixed(1)}ms`); } -// ── 3. structural invariants over every seed ──────────────────────────────────────── -section('structural invariants'); +// ── 3. structural invariants — SYNTHETIC source (full suite) + synthetic-only brief checks ── +section('structural invariants — synthetic'); const facadeSet = new Set(); -const isFiniteNum = v => typeof v === 'number' && Number.isFinite(v); for (const s of SEEDS) { const plan = generatePlan(s); - const edgeIds = new Set(plan.streets.edges.map(e => e.id)); - const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); - const nodeIds = new Set(plan.streets.nodes.map(n => n.id)); - const blockIds = new Set(plan.blocks.map(b => b.id)); - const lotIds = new Set(plan.lots.map(l => l.id)); - const districtIds = new Set(plan.districts.map(d => d.id)); - - // every number finite (JSON would silently turn NaN/Infinity into null) - ok(plan.streets.nodes.every(n => isFiniteNum(n.x) && isFiniteNum(n.z)), `seed ${s}: node coords finite`); - ok(plan.lots.every(l => isFiniteNum(l.x) && isFiniteNum(l.z) && isFiniteNum(l.w) && isFiniteNum(l.d) && isFiniteNum(l.ry)), `seed ${s}: lot numbers finite`); - ok(plan.blocks.every(b => b.poly.every(p => isFiniteNum(p[0]) && isFiniteNum(p[1]))), `seed ${s}: block poly finite`); - - ok(plan.streets.edges.every(e => nodeIds.has(e.a) && nodeIds.has(e.b)), `seed ${s}: every edge references real nodes`); - ok(plan.blocks.every(b => districtIds.has(b.district)), `seed ${s}: every block in a real district`); - ok(plan.lots.every(l => blockIds.has(l.block)), `seed ${s}: every lot in a real block`); - ok(plan.lots.every(l => edgeIds.has(l.frontEdge)), `seed ${s}: every lot has a valid frontEdge`); - ok(plan.lots.every(l => l.w > 0 && l.d > 0), `seed ${s}: every lot has positive size`); - ok(plan.shops.every(sh => lotIds.has(sh.lot)), `seed ${s}: every shop has a real lot`); - ok(plan.shops.every(sh => SHOP_TYPES[sh.type]), `seed ${s}: every shop has a known type`); - ok(plan.shops.every(sh => SHOP_TYPES[sh.type].facades.includes(sh.facadeSkin)), `seed ${s}: every facade is in its type's pool`); - ok(plan.shops.every(sh => sh.hours[0] >= 0 && sh.hours[1] <= 23 && sh.hours[1] > sh.hours[0]), `seed ${s}: every shop has sane hours`); - ok(plan.shops.every(sh => sh.name && sh.sign), `seed ${s}: every shop is named`); - ok(plan.shops.every(sh => !/[{}]|undefined/.test(sh.name + sh.sign)), `seed ${s}: no unresolved tokens / undefined in names`); - // STOREYS strictly within the registry range, corner-boost included (Lane F finding #5 class): the - // occasional 3-storey corner anchor adds ≤1 storey capped at 3, and ONLY for tall-capable types - // (registry max ≥ 2) — so single-storey types (video/milkbar/stall, [1,1]) must NEVER exceed 1. - // (The old `<= Math.max(mx,3)` was too lax — it would have passed a video at 3 storeys.) - ok(plan.shops.every(sh => { - const [mn, mx] = SHOP_TYPES[sh.type].storeys; - const pmax = mx >= 2 ? Math.min(3, mx + 1) : mx; - return sh.storeys >= mn && sh.storeys <= pmax; - }), `seed ${s}: storeys within registry range (corner-boost ≤ min(max+1,3); single-storey types never boosted)`); - - // one lot ↔ at most one shop - const lotShopCounts = {}; - for (const sh of plan.shops) lotShopCounts[sh.lot] = (lotShopCounts[sh.lot] || 0) + 1; - ok(Object.values(lotShopCounts).every(c => c === 1), `seed ${s}: no lot has two shops`); - // a shop only ever sits on a solid lot use - ok(plan.shops.every(sh => SOLID.has(plan.lots[sh.lot].use)), `seed ${s}: every shop sits on a solid lot`); - - // FACING: every lot's facade normal points at (not away from) its frontEdge (the review's D1 class) - let backwards = null; - for (const l of plan.lots) { - const e = plan.streets.edges.find(x => x.id === l.frontEdge); - const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; - const [nx, nz] = nearestOnSeg(l.x, l.z, a.x, a.z, b.x, b.z); - const tox = nx - l.x, toz = nz - l.z; // vector from lot to its street - const [fx, fz] = facing(l); - if (tox * fx + toz * fz < -0.01) { backwards = l.id; break; } // facing away from the street it fronts - } - ok(backwards === null, `seed ${s}: every lot faces its frontEdge` + (backwards !== null ? ` (lot ${backwards} backwards)` : '')); - - // OVERLAP within a block (all lots) — strips are sequential ⇒ disjoint by construction - const byBlock = {}; - for (const l of plan.lots) (byBlock[l.block] ||= []).push(l); - let inBlock = null; - for (const arr of Object.values(byBlock)) { - const cs = arr.map(lotCorners); - for (let i = 0; i < arr.length && !inBlock; i++) for (let j = i + 1; j < arr.length; j++) - if (obbOverlap(cs[i], cs[j])) { inBlock = [arr[i].id, arr[j].id]; break; } - if (inBlock) break; - } - ok(!inBlock, `seed ${s}: no overlapping lots within a block` + (inBlock ? ` (lots ${inBlock})` : '')); - - // OVERLAP across blocks (building lots) — the corner-deconfliction guarantee (review D2) - const solids = plan.lots.filter(l => SOLID.has(l.use)); - const cs = solids.map(lotCorners); - const aabb = cs.map(q => { let a = [1e9, 1e9, -1e9, -1e9]; for (const [x, z] of q) { a[0] = Math.min(a[0], x); a[1] = Math.min(a[1], z); a[2] = Math.max(a[2], x); a[3] = Math.max(a[3], z); } return a; }); - let crossBlock = null; - for (let i = 0; i < solids.length && !crossBlock; i++) for (let j = i + 1; j < solids.length; j++) { - if (solids[i].block === solids[j].block) continue; - const A = aabb[i], B = aabb[j]; - if (A[2] < B[0] || B[2] < A[0] || A[3] < B[1] || B[3] < A[1]) continue; // AABB reject - if (obbOverlap(cs[i], cs[j])) { crossBlock = [solids[i].id, solids[j].id]; break; } - } - ok(!crossBlock, `seed ${s}: no overlapping building lots across blocks` + (crossBlock ? ` (lots ${crossBlock})` : '')); - - // CHUNKINDEX: covers every lot; buckets reference real ids - const idx = chunkIndex(plan); - const covered = new Set(); - for (const cellB of Object.values(idx.chunks)) { - for (const id of cellB.lots) covered.add(id); - ok(cellB.lots.every(id => lotIds.has(id)) && cellB.shops.every(id => plan.shops.some(sh => sh.id === id)) && cellB.edges.every(id => edgeIds.has(id)), - `seed ${s}: chunk buckets reference real ids`); - } - ok(plan.lots.every(l => covered.has(l.id)), `seed ${s}: chunkIndex covers every lot`); - - // CHUNKINDEX corridor coverage: every chunk the road CORRIDOR passes through lists the edge — not - // just the centreline. Samples densely along the edge AND across the ±width/2 verge band (incl. - // offsets BETWEEN the chunkIndex rails, so this genuinely tests band interior, not just the rails). - // Guards the Lane B furniture-drop class: verge props must never fall in a chunk that omits their edge. - const cell = v => Math.floor(v / CHUNK); - let edgeGap = null; - for (const e of plan.streets.edges) { - const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; - const dx = b.x - a.x, dz = b.z - a.z, len = Math.hypot(dx, dz) || 1; - const hw = (e.width || 0) / 2, px = -dz / len * hw, pz = dx / len * hw; - const steps = Math.max(1, Math.ceil(len / 2)); // 2m dense along-edge samples - for (const f of [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) { // across the corridor band - for (let k = 0; k <= steps && !edgeGap; k++) { - const x = a.x + dx * k / steps + px * f, z = a.z + dz * k / steps + pz * f; - const bk = idx.chunks[`${cell(x)},${cell(z)}`]; - if (!bk || !bk.edges.includes(e.id)) edgeGap = [e.id, f, cell(x), cell(z)]; - } - if (edgeGap) break; - } - if (edgeGap) break; - } - ok(!edgeGap, `seed ${s}: chunkIndex covers the full road corridor (road+verge)` + (edgeGap ? ` (edge ${edgeGap[0]} @ off ${edgeGap[1]} missing @ ${edgeGap[2]},${edgeGap[3]})` : '')); - - // JSON round-trip is lossless (and carries no stray non-schema keys) - ok(JSON.stringify(JSON.parse(JSON.stringify(plan))) === JSON.stringify(plan), `seed ${s}: JSON round-trip lossless`); - - // design brief presence - ok(plan.streets.edges.some(e => e.kind === 'main'), `seed ${s}: has a main-street spine`); - ok(plan.streets.edges.some(e => e.kind === 'arcade'), `seed ${s}: has an arcade`); - ok(plan.districts.some(d => d.kind === 'market'), `seed ${s}: has a market district`); - ok(plan.shops.some(sh => sh.type === 'stall'), `seed ${s}: market has stalls`); - ok(plan.shops.some(sh => sh.type === 'dept'), `seed ${s}: has a department-store anchor`); + structuralSuite(plan, `syn ${s}`); + // synthetic-only brief presence (a real OSM town has none of these) + ok(plan.streets.edges.some(e => e.kind === 'main'), `syn ${s}: has a main-street spine`); + ok(plan.streets.edges.some(e => e.kind === 'arcade'), `syn ${s}: has an arcade`); + ok(plan.districts.some(d => d.kind === 'market'), `syn ${s}: has a market district`); + ok(plan.shops.some(sh => sh.type === 'stall'), `syn ${s}: market has stalls`); + ok(plan.shops.some(sh => sh.type === 'dept'), `syn ${s}: has a department-store anchor`); const milkbars = plan.shops.filter(sh => sh.type === 'milkbar').length; - ok(milkbars >= 2 && milkbars <= 4, `seed ${s}: 2–4 corner milk bars (${milkbars})`); - - // exactly one deterministic late-night landmark, and the `openLate` field ⟺ `hours[1] >= 22` - // (Lane F's night gate may key off EITHER — they identify the same single shop). - const late = plan.shops.filter(sh => sh.openLate); - const ge22 = plan.shops.filter(sh => sh.hours[1] >= 22); - ok(late.length === 1, `seed ${s}: exactly one open-late shop (${late.length})`); - ok(ge22.length === 1, `seed ${s}: exactly one shop closes ≥22:00 (${ge22.length})`); - ok(late.length === 1 && ge22.length === 1 && late[0] === ge22[0], `seed ${s}: openLate field ⟺ hours[1]≥22 (same shop)`); - ok(late.length === 1 && late[0].hours[1] >= 22 && late[0].type !== 'stall', `seed ${s}: open-late shop closes ≥22:00 and isn't a market stall`); - ok(late.length === 1 && (late[0].type === 'video' || !plan.shops.some(x => x.type === 'video')), `seed ${s}: the open-late shop is the video rental`); - + ok(milkbars >= 2 && milkbars <= 4, `syn ${s}: 2–4 corner milk bars (${milkbars})`); for (const sh of plan.shops) facadeSet.add(sh.facadeSkin); } -// ── 3b. OSM plan source (?plansrc=osm): same structural invariants + its own golden ──── -// The synthetic golden (0x3fa36874) is untouched above; this parameterizes the harness by source. -// Brief checks (market/arcade/dept/milkbar) are synthetic-only and deliberately NOT asserted here — -// a real-data town has no synthetic market square. Structural invariants must hold identically. -section('OSM plan source (fixture-driven, zero-network)'); -const OSM_GOLDEN = { seed: 20261990, hash: 0x34cfdec0 }; +// ── 3b. OSM plan-source PARITY: every town runs the identical structuralSuite + its own golden ── +// (round 8) Real-data towns get the same coverage as synthetic; only the synthetic-brief checks are +// skipped. Goldens are pinned per town; the synthetic golden above stays 0x3fa36874. +section('OSM plan source parity (fixture-driven, zero-network)'); +const OSM_GOLDENS = { melbourne: 0x34cfdec0, katoomba: 0x0f652510 }; const OSM_SEEDS = [20261990, 1, 42, 777]; -for (const s of OSM_SEEDS) { - const plan = generatePlanOSM(s); - const edgeIds = new Set(plan.streets.edges.map(e => e.id)); - const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); - const lotIds = new Set(plan.lots.map(l => l.id)); - ok(plan.source === 'osm', `osm ${s}: source tagged 'osm'`); - ok(JSON.stringify(generatePlanOSM(s)) === JSON.stringify(plan), `osm ${s}: deterministic (same fixture+seed)`); - ok(plan.lots.length > 0 && plan.shops.length > 0, `osm ${s}: non-empty (${plan.shops.length} real shops)`); - ok(plan.lots.every(l => isFiniteNum(l.x) && isFiniteNum(l.z) && isFiniteNum(l.w) && isFiniteNum(l.d) && isFiniteNum(l.ry)), `osm ${s}: lot numbers finite`); - ok(plan.streets.edges.every(e => nodeById.has(e.a) && nodeById.has(e.b)), `osm ${s}: edges reference real nodes`); - ok(plan.lots.every(l => edgeIds.has(l.frontEdge)), `osm ${s}: every lot has a valid frontEdge`); - ok(plan.lots.every(l => l.w > 0 && l.d > 0), `osm ${s}: every lot has positive size`); - ok(plan.shops.every(sh => lotIds.has(sh.lot) && SHOP_TYPES[sh.type] && SHOP_TYPES[sh.type].facades.includes(sh.facadeSkin)), `osm ${s}: shops valid (lot + type + facade-in-pool)`); - ok(plan.shops.every(sh => sh.hours[0] >= 0 && sh.hours[1] <= 23 && sh.hours[1] > sh.hours[0]), `osm ${s}: sane hours`); - ok(plan.shops.every(sh => sh.name && sh.sign && !/[{}]|undefined/.test(sh.name + sh.sign)), `osm ${s}: every shop named (real OSM names)`); - let back = null; - for (const l of plan.lots) { - const e = plan.streets.edges.find(x => x.id === l.frontEdge); const a = nodeById.get(e.a), b = nodeById.get(e.b); - const [nx, nz] = nearestOnSeg(l.x, l.z, a.x, a.z, b.x, b.z); const [fx, fz] = facing(l); - if ((nx - l.x) * fx + (nz - l.z) * fz < -0.01) { back = l.id; break; } +for (const town of osmTownKeys()) { + const report = {}; + for (const s of OSM_SEEDS) { + const plan = generatePlanOSM(s, town, s === 20261990 ? { report } : {}); + ok(plan.source === 'osm', `osm/${town} ${s}: source tagged 'osm'`); + ok(JSON.stringify(generatePlanOSM(s, town)) === JSON.stringify(plan), `osm/${town} ${s}: deterministic (same fixture+seed)`); + structuralSuite(plan, `osm/${town} ${s}`); + for (const sh of plan.shops) facadeSet.add(sh.facadeSkin); } - ok(back === null, `osm ${s}: every lot faces its frontEdge` + (back !== null ? ` (lot ${back})` : '')); - const cs = plan.lots.map(lotCorners); let ov = null; - for (let i = 0; i < plan.lots.length && !ov; i++) for (let j = i + 1; j < plan.lots.length; j++) - if (obbOverlap(cs[i], cs[j])) { ov = [plan.lots[i].id, plan.lots[j].id]; break; } - ok(!ov, `osm ${s}: no overlapping lots (within or across blocks)` + (ov ? ` (${ov})` : '')); - const idx = chunkIndex(plan); const cov = new Set(); - for (const c of Object.values(idx.chunks)) for (const id of c.lots) cov.add(id); - ok(plan.lots.every(l => cov.has(l.id)), `osm ${s}: chunkIndex covers every lot`); - ok(JSON.stringify(JSON.parse(JSON.stringify(plan))) === JSON.stringify(plan), `osm ${s}: JSON round-trip lossless`); - const late = plan.shops.filter(sh => sh.openLate), ge22 = plan.shops.filter(sh => sh.hours[1] >= 22); - ok(late.length === 1 && ge22.length === 1 && late[0] === ge22[0] && late[0].type === 'video', `osm ${s}: exactly one open-late video`); - for (const sh of plan.shops) facadeSet.add(sh.facadeSkin); -} -{ - const hash = xmur3(JSON.stringify(generatePlanOSM(OSM_GOLDEN.seed)))() >>> 0; - ok(hash === OSM_GOLDEN.hash, `osm ${OSM_GOLDEN.seed}: fingerprint 0x${hash.toString(16)} matches golden 0x${(OSM_GOLDEN.hash >>> 0).toString(16)}`); + const hash = xmur3(JSON.stringify(generatePlanOSM(20261990, town)))() >>> 0; + const want = OSM_GOLDENS[town]; + ok(want !== undefined && hash === (want >>> 0), `osm/${town}: golden 0x${hash.toString(16)} matches 0x${((want ?? 0) >>> 0).toString(16)}`); + console.log(` ${town}: ${report.shops} shops · normalized {typesRemapped:${report.typesRemapped}, hoursClamped:${report.hoursClamped}, openLate:${report.openLate}}`); } // ── 4. every facade skin referenced by the registry exists on disk ────────────────── diff --git a/web/map.html b/web/map.html index 3d81644..c7b00e0 100644 --- a/web/map.html +++ b/web/map.html @@ -87,8 +87,10 @@ use THREE, but Lane A's 2D map doesn't need it). It imports only Lane A's pure-data modules. -->