306 lines
35 KiB
Markdown
306 lines
35 KiB
Markdown
> **FABLE'S VERIFICATION (2026-07-26).** John's reframe, designed by a 3-take panel (take-the-map-
|
||
> away / go-up-and-out-the-back / the-unmarked-door), each adversarially judged, then synthesised.
|
||
> **I re-measured the two load-bearing claims myself before circulating this, and both hold exactly:**
|
||
>
|
||
> 1. **The street names.** Counted independently across every shipped cache: **19,132 road ways,
|
||
> 17,835 carrying a `name` — 93.2%**, matching the synthesis to the digit. And the drop is real:
|
||
> `plan_osm.js:363` builds each way as `rawWays.push({ kind: roadEdgeKind(rd.kind), pl })` —
|
||
> it keeps the kind and the geometry and **discards `rd.name`**. Every real street in twenty-three
|
||
> Australian towns is sitting in this repo with its name on it, and the game has never said one
|
||
> out loud.
|
||
> 2. **The interior budget correction, which contradicts our own depth map and is the more urgent
|
||
> of the two.** `index.html:349` ships `useGLB: !NOASSETS`, so **the default boot is the GLB
|
||
> path**, and `C-progress.md:841` records that sweep at **GLB-on 344** against the ≤350 interior
|
||
> cap. **The interior margin is SIX draws, not 191.** The depth map's "draw budget is not the
|
||
> problem either" cited the GLB-*off* 168 and is wrong. This goes into the house record today
|
||
> regardless of what happens to the rest of this document — any interior work, in any epoch,
|
||
> is spending against six.
|
||
>
|
||
> **Scope honesty, stated up front:** enterable upper floors are an EPOCH, not a wave.
|
||
> `player.js:64` and `interior_mode.js:485` both write `y = EYE` unconditionally every frame on
|
||
> every path — there is no gravity, no floor query, no current-storey state, and no data structure
|
||
> anywhere carries a floor height. Anything containing "levels" or "stairs you walk up" is
|
||
> proposing a locomotion rewrite plus a new ground truth. Everything else John asked for is
|
||
> affordable now.
|
||
>
|
||
> Companion: [SALE_RED_TICKET_DAY.md](SALE_RED_TICKET_DAY.md) owns the ROUND and the LAWS (the
|
||
> Band Floor Law); this document owns the HUNT and the CITY.
|
||
|
||
# THE CITY IS THE GAME
|
||
### Synthesis — the v8 city-patterns review, three takes reconciled
|
||
**Fable → John.** Measurements in this document were re-run against the tree at `d59df8e`, seed 20261990, all 23 shipped caches + the synthetic. Where I contradict a judge or the depth map, I say so and show the number.
|
||
|
||
---
|
||
|
||
## 1. THE ANSWER
|
||
|
||
**Yes. And the reason it's the right move is that the city is already deeper than the game knows.**
|
||
|
||
Your reframe — *"you have to even know where to find the store first to get the sale item"* — asks for a city with places in it. PROCITY generates places. It has generated them since v2. Nothing reads them:
|
||
|
||
| Generated | Where | Consumed by |
|
||
|---|---|---|
|
||
| Six `DISTRICT_KINDS`, 7 districts on the synthetic (mainstreet 332 shops · backstreets 95 · market 41 · arcade 17 · warehouse 4 · residential 4) | `registry.js:8`, `plan.js:160-292` | grass verges + one plaza quad (`ground.js:58,165`), ambient anchors (`audio.js:170`). Nothing else. |
|
||
| A 42 m covered arcade, 17 shops, 2 blocks, a measured 4.99 m walkable lane, already awning-roofed by accident | `plan.js:270-283`, `buildings.js:634-647` | paved (`ground.js:140-147`). No props, no signage, no roof, no reverb. |
|
||
| Two 280 m service laneways behind the main-street blocks, 36 yard lots | `plan.js:328-339` | gum trees, magpie song. Zero shops. |
|
||
| `storeys` on **every** shop — 566 of 1,345 corpus shops are 2–3 storeys | `plan.js:89`, `plan_osm.js:275` | box height (`buildings.js:567`) and one ambient. **The facade stops at 3.0 m.** No upper window exists anywhere in the codebase. |
|
||
| `lot.frontEdge` — which street every shop is on | `plan.js:78` | one consumer, `gigs.js:82`. |
|
||
| **`roads[].name` on every shipped cache — 17,835 named ways of 19,132 (93.2%)** | the caches themselves | **nothing. `plan_osm.js:362-366` keeps `kind` and drops the name.** |
|
||
|
||
That last row is the finding of this review and I re-derived it independently: adelaide 1191/1099/319 distinct, bendigo 1552/1408/322, fitzroy 1571/1437/190, katoomba 484/457/166, castlemaine 397/370/141. **Every real street in twenty-three Australian towns is already sitting in the repo with its name on it, and the game has never once said one out loud.**
|
||
|
||
And it is recoverable without touching citygen. Plan nodes *are* snapped projected way points (`plan_osm.js:361-372`, `SNAP=3`), so plan edges lie **on** the named ways. Measured, all 23 caches, median distance from a plan-edge midpoint to its nearest named way:
|
||
|
||
> **0.03 m – 0.90 m.** Worst town katoomba at 0.90 m. **97.8% of corpus shops (1,192 / 1,219) resolve to a street name.**
|
||
|
||
That is not an inference. I recovered the plan's internal shift by centroid-align + nearest-waypoint vote and re-measured; the numbers above are mine. There is a cleaner one-line upgrade — write `norm.shift = {shx, shz}` at `plan_osm.js:510`, which lands in the **caller-supplied `opts.report` sink, never on the plan object** — so it moves no golden and breaks no classic byte. Take the one line. But the design does not depend on it.
|
||
|
||
**So: the city is the game is cheap, because the city is already built and only the exploration layer is missing.** The expensive part is exactly one thing, and I'll name it now rather than bury it:
|
||
|
||
> **Vertical movement is fiction until someone rewrites locomotion in both modes.** `player.js:64` and `interior_mode.js:485` both write `y = EYE` unconditionally, every frame, on every path. There is no gravity, no floor query, no current-storey state, and no data structure anywhere carries a floor height. Any proposal containing "levels", "stairs" or "upper floors as places you enter" is proposing that rewrite plus a new ground truth. **And the interior draw margin is six, not one-nine-one** — `C-progress.md:841` reads *"full drawSweep unchanged (GLB-off 168 · GLB-on 344, both pass)"*, and `index.html:349` ships `useGLB: !NOASSETS`, so the **default boot is the GLB path at 344/350.** The depth map's line *"DRAW BUDGET IS NOT THE PROBLEM EITHER"* cites the GLB-off sweep and is wrong. That correction goes into the house record today regardless of what happens to the rest of this.
|
||
|
||
Enterable upper floors are an epoch. They are not this epoch. Everything else you asked for is affordable now.
|
||
|
||
---
|
||
|
||
## 2. THE SHAPE — "KNOW YOUR TOWN"
|
||
|
||
One proposal, four layers, in dependency order. Layers 1–3 are information design and cost **zero draws**. Layer 4 is the only geometry and it rides meshes that already exist.
|
||
|
||
### Layer 1 — THE ADDRESS LAYER *(the vocabulary)*
|
||
A new pure module, `address.js`. No THREE, no fetch, no plan mutation.
|
||
|
||
```
|
||
createAddresses(plan, cache|null) → {
|
||
streetOf(edgeId) → "Templeton Street" | null
|
||
localityOf(shopId) → { street, district, block, side, label }
|
||
cohort(predicate) → shopId[] // the clue sizer — see Layer 3
|
||
}
|
||
```
|
||
|
||
- **23 cache towns:** street name, resolved by nearest named way to the edge midpoint (≤8 m tolerance).
|
||
- **Synthetic:** no roads cache and only 22 edges, so the label comes from `district.kind` + block — *"the market end"*, *"the arcade"*, *"the backstreets"*. Same field, different supplier, one consumer contract.
|
||
|
||
Consumers, all cheap: the HUD tooltip (`hud.js`, one template literal), the map's street labels, the notebook, the morning paper, keeper rumours.
|
||
|
||
**This is the prerequisite, not the bonus.** VERTICALITY's judge marked it as such and was right: without it, CLEARANCE collapses to one type-cell on 21 of 24 towns. UNMARKED needs it harder still — 7 of the synthetic's 12 hidden shops share a name with 1–10 others (`Rotation ×10`, `Quick Cash Pawn ×11`), so a ledger entry that says only a name points at ten buildings. **An address is the only way a shop with no sign can be written down at all.**
|
||
|
||
### Layer 2 — THE FOG *(the keystone)*
|
||
`minimap.js` draws only shops the player has walked past. Discovery is player state, saved.
|
||
|
||
Today `minimap.js:52-63` draws **every** lot in the plan, coloured by shop type, from the first frame, with no gating. One M keypress renders all 493 synthetic shops — all 77 record shops, all 92 op shops — before the player has walked a metre. Measured against what that is worth: passing every shopfront once costs **5,330 m on the synthetic, 9,622 m on fitzroy, 5,797 m on northbridge** — at WALK 4.6 m/s against `lighting.js:91`'s 240-second day, **4.8 / 8.7 / 5.3 game-days of walking.** The map hands over five to nine game-days of legwork for one keystroke, on all 24 towns, forever.
|
||
|
||
And the payoff is bigger than the cost saving. Replicating `minimap.js`'s own transform, a shop rectangle renders at:
|
||
|
||
| | bowral | castlemaine | katoomba | adelaide | fitzroy | synthetic |
|
||
|---|---|---|---|---|---|---|
|
||
| full-extent (today) | 0.8×1.1 px | 1.4×1.8 | 1.4×1.9 | 1.7×2.3 | 2.7×3.5 | 5.2×10.2 |
|
||
| framed to a 200 m known window | 29×39 px | 29×39 | 29×39 | 29×39 | 29×39 | 21×41 |
|
||
| **gain** | **35×** | **21×** | **21×** | **17×** | **11×** | 4× |
|
||
|
||
**The same line of code that hides what you don't know makes legible what you do.** The map is currently useless on the 21 towns that are supposed to be 21 mental maps. Fog fixes that as a side effect.
|
||
|
||
Cost: zero draws, zero triangles — the map is a 2D canvas overlay and `hud.js:361`'s counter (`renderer.info.render.calls`) never sees it. Zero new plan fields. Classic-gatable by construction: `createMinimap(plan)` at `index.html:204` becomes `createMinimap(plan, game ? knownProvider : null)`, and a null provider is today's exact behaviour — the same pattern `ground.js` already uses at `index.html:178-185`, so `minimap.js` needs no flag test at all.
|
||
|
||
**Two corrections to the depth map's own fog proposal:**
|
||
|
||
1. **Discovery is a proximity probe, not `chunks.onChunkBuilt`.** Real towns are all BIG_CITY (`index.html:151`) so R=2 — the outermost live chunk centre is 128 m out, its far corner 181 m. Chunk-built discovery would hand you shops three blocks away through a wall. Use a 25 m squared-distance probe over the live `doorRects` on the existing `frame % 6` cadence (`hud.js:337`). Same cost class, honest semantics.
|
||
2. **Key the fog by TOWN, not by `townKey`.** I verified: on all 23 cache towns, `shops[].id / lot / type / name` and every lot geometry are **byte-identical across seeds** — only `storeys` and `hours` re-jitter. So a re-seed is *the same Katoomba with different secrets*, and the map you learned should survive it. Keying by town also bounds the save by construction (≤ 25 registered keys, no LRU, no cap needed), which kills NOMAP's K5 and UNMARKED's cap problem in one move. The **synthetic is the exception** — its shop ids are seed-dependent (493 shops at 20261990, 479 at seed 7) — so it keys `default@<seed>`. One rule, one stated exception.
|
||
|
||
### Layer 3 — THE HUNT *(what the fog is for)*
|
||
A once-per-day sale, announced at dawn in the existing morning paper, addressed rather than pinned. Full round in §3.
|
||
|
||
The director's job is one thing: **size the clue.** It picks a target from the set of shops that some clue cell resolves to **≤4 doors within a ≤350 m span**, and emits the smallest cell that works. Measured share of a town's shops that are clueable under that rule:
|
||
|
||
| bendigo · braddon · darwin | launceston · castlemaine · marrickville · bowral | newtown · redhill · newcastle · northbridge | glebe · westend · hobart | adelaide · fitzroy · geelong · fremantle · katoomba | brunswick | **synthetic** |
|
||
|---|---|---|---|---|---|---|
|
||
| **100%** | 90–93% | 83–89% | 77–78% | 69–74% | 51% | **32%** |
|
||
|
||
Median real town: **~85% of shops are pointable-at in four doors or fewer.** Median best clue cell on a real town: **1 door.** On the synthetic: **9 doors, p90 20.** That difference is structural and I'll deal with it in §5 and §8.
|
||
|
||
### Layer 4 — THE UNMARKED *(the reward)*
|
||
Roughly 6% of a town's shops ship with **no sign** — 89 across the corpus, 12 on the synthetic, 2–12 per town by `clamp(round(shops×0.06), 2, 12)`. You find them by cue: a crate of records on the footpath, an A-frame, a roller door half up, a bloke in the doorway. They enter the notebook **by address**, because they have no name.
|
||
|
||
**The seam that makes this free: on a fogged map, an unmarked shop's absence is indistinguishable from an undiscovered shop's absence.** UNMARKED's own judge killed the map-omission idea because on the synthetic — the only town where the map is legible — omitting one lot from a terrace punches a **16.8 px hole, 6.5× the lot width**, and you can count the notches without walking. With Layer 2 in place there is nothing to count: every undiscovered lot is a hole. The fog doesn't just permit the hidden layer, it is the only thing that makes it honest.
|
||
|
||
The same seam retires VERTICALITY's KILL 2 (*"the minimap already hands over the clue"*) and NOMAP's Gate 1 problem. **Every one of the three takes has a kill that fog closes.** That is the argument for this shape over any of the three alone.
|
||
|
||
### Layer 5 — UPSTAIRS, VISIBLE ONLY *(the bridge to the next epoch)*
|
||
Give the 566 multi-storey shops a **first-floor window band**, drawn into the existing merged `windowGeos` mesh with the existing per-vertex `aHours` shader. **+0 draws, ~24 triangles per shop.** `makeInstanced` sizes to `list.length` with no cap (`buildings.js:749-753`); `windowGeos` merges to one mesh per chunk (`:800-806`). Today `buildings.js:581` caps the facade skin at `FACADE_H = 3.0` and puts both windows at y=1.55, so storeys 2 and 3 are a flat tinted box with a parapet — while `audio.js:190-193` already seeds a radio-through-a-window ambient onto every shop with `storeys ≥ 2`. **The game already sounds like someone lives above the shop and looks like nobody does.**
|
||
|
||
No stair. No enterable room. Nothing that touches the two Y-clamps. This is v8's own name — SOMEONE LIVES HERE — paid for at zero draws.
|
||
|
||
### The seams, stated
|
||
```
|
||
address ──(labels)──▶ hud tooltip · map · paper · keeper rumour · notebook
|
||
address ──(cohort sizer)──▶ director [the clue can't be written without it]
|
||
fog ──(known set)──▶ map draw filter + map framing + notebook
|
||
fog ──(structural)──▶ unmarked [no hole to count]
|
||
unmarked──(no name)──▶ address [the only identifier it has]
|
||
upstairs──(address)──▶ "above 42 Templeton St" [free, later]
|
||
```
|
||
One save field (`known`), one new pure module (`address.js`), one canvas rewrite (`minimap.js`), one template literal (`hud.js`).
|
||
|
||
---
|
||
|
||
## 3. THE ROUND
|
||
|
||
**Castlemaine. 22 shops. 8 shop-bearing streets. Day 14. You have $180 and a wantlist with three things on it.**
|
||
|
||
**The clock, from the code.** `lighting.js:91` `DAY_SECONDS = 240` over 6 segments = **40 real seconds per segment**. `lighting.js:22` `HOUR_NUM = [6.5, 9, 12.5, 15.5, 18.5, 22]`. Registry trade hours are 10–18 (`registry.js:40`), so on 20 of 24 towns a record shop is open in **segments 3 and 4 only — 80 real seconds of trading.** That is the hard constraint every hunt design in this review either respected or died on.
|
||
|
||
**06:30 — DAWN, segment 1.** The paper lands. One line:
|
||
|
||
> **CLOSING DOWN — EVERYTHING HALF PRICE — THE RECORD SHOP ON TEMPLETON STREET**
|
||
|
||
Two terms: type + street. Measured on castlemaine, that cell holds **1 shop** (town median 2, town max 3). The clue is exact. The question is whether you know where Templeton Street is.
|
||
|
||
**06:30–12:30 — segments 1 and 2, 80 real seconds, the doors are shut.** This is the hunt, and it is free time you already spend walking. At WALK 4.6 m/s that's **368 m; at RUN 8.8 it's 704 m.** Measured, distance from the town's shop-centroid to a given shop: on castlemaine 45% of shops are inside 368 m, 73% inside 704 m. Corpus range 11–85% inside 368 m.
|
||
|
||
- **You've walked Templeton before.** It's on your fogged map, labelled. You go. 40–90 seconds. You're standing outside at 11:50 with a segment in hand.
|
||
- **You haven't.** The map shows the streets you've learned and a name you've never seen. Three outs, in order of cost:
|
||
1. **Ask a keeper.** Any open shop, any keeper: *"Templeton? Off the north end of Barker, past the hall."* Free, rides the existing rumour seam, and it is the most SOMEONE-LIVES-HERE thing in the whole design.
|
||
2. **The paper's second line** — a landmark from the address layer's nearest named cross-street: *"opposite the Town Hall"*. Costs nothing; the director emits it only when the target street is undiscovered.
|
||
3. **Walk.** Every street you learn is banked forever, on every future visit, at every seed. The loss is a day, not a save.
|
||
- **You get it wrong.** The clue cell is ≤4 doors by gate, so worst case you check four shopfronts. Measured p90 spread inside a clue cohort: 0 m on eleven towns, 105–466 m on nine, and the gate rejects anything over 350 m.
|
||
|
||
**12:30 — MIDDAY, segment 3. Doors open.** You're in. The sale stock is the shop's normal seeded rotation at half band. You dig — riffle, pull, read the sleeve, check it against THE BIBLE. Under band is a gem; that skill is unchanged and it is still the core.
|
||
|
||
**Scarcity is the stock, not a stopwatch.** `dig.js:417-421` reads `opts.gone` once and computes offers once, at card-open — so "half the good ones are already gone" works evaluated **when you walk in**, and does not work as a timer ticking while the card is open. Arrive in segment 3 with a full sale; arrive in segment 4 and a seeded fraction is gone. That is the whole pressure and it costs one union into an existing set (`interior_mode.js:187`).
|
||
|
||
**18:00 — the shop shuts.** If you never found it, you lost the day. Tomorrow's paper has a different sale, in a different part of town, and the streets you burned today are permanently on your map. **You did not lose a run. You bought a map with a day.**
|
||
|
||
**Total round: 240 real seconds. Clue resolves to ≤4 doors. 368–704 m of pre-open walking. 80 seconds of trading. Win = you knew the town, or you learned it.**
|
||
|
||
**The two numbers that constrain everything above** — and that killed two of the three takes' round designs:
|
||
- NOMAP's director radius (`remaining_segments × 40 s × 4.6 m/s × 0.7`) leaves a median of **1 shop reachable on marrickville, 2–3 on braddon and redhill.** The radius narrows the pool below the clue's cohort before the clue speaks. Cut; announce at dawn instead, which is what §7 already did, and let the whole town be live.
|
||
- UNMARKED's BLIND DOCKET puts the target **2,217 m from the roster on hobart, 1,971 m on fremantle** — 3.1× the entire round's RUN budget in a straight line, on a mode with an 80-second window. Impossible on 7 towns, and on 9 more the "unnamed" shop is already printed in the paper. Cut entirely.
|
||
|
||
---
|
||
|
||
## 4. WHAT IT COSTS
|
||
|
||
### THE MINIMUM VERSION — one round, no geometry, proves the fun
|
||
|
||
| Lane | File | Change |
|
||
|---|---|---|
|
||
| **F** | `address.js` **(new)** | pure; edge→named-way resolution + shift recovery; synthetic district/block fallback; `cohort()` sizer. Zero fetch beyond the cache the shell already holds — needs one additive export, `getTownCache(key)`, because `index.html:130` hands the JSON to `registerTownCache` and drops its own reference. |
|
||
| **A** | `plan_osm.js:510` | **one line**: `norm.shift = { shx, shz }` into the caller-supplied `opts.report`. Not on the plan. No golden moves. Optional — the vote recovers it without this. |
|
||
| **B** | `minimap.js` | fog filter + known-extent framing + street labels + an **offscreen static cache** (today `drawStatic()` re-fills every lot every frame; adding up to 322 `fillText` calls per frame would turn a free overlay into a CPU text pass — invalidate on discovery change only). ~40 lines. |
|
||
| **B** | `hud.js` | tooltip prints `NAME — Templeton Street`. One template literal. Plus the 25 m discovery probe on the existing `frame % 6` cadence. |
|
||
| **B** | `buildings.js:453-478` | **the type label into the sign atlas.** The cell is 512×168; the name renders at ≤74 px centred at `py+84`, and `drawCrateMark` owns `py+128…py+152`. There is ~42 px of clear canvas in the **top** band (`py+13 … py+55`). Draw the registry `label` there at ~30 px. **+0 draws, +0 tris.** Without it, "the op shop on Barker Street" is unfalsifiable from the footpath: `drawSign` renders `shop.name` only, `opshop` and `stall` sign colours are byte-identical (`fixture_plan.js:29-37`), and only **5–18% of real-town shop names contain a type word** (median 13%). Every later clue term depends on this. |
|
||
| **F** | `save.js` | `known` optional block, town-keyed, exact-and-loud validator matching `save.js:72-99`, `.slice()` in `adopt()`. |
|
||
| **F** | the game layer | the dawn director + the paper line + the notebook DOM. |
|
||
|
||
**Zero new draws. Zero new triangles. Zero plan fields. No Lane C, D or E.** One round.
|
||
|
||
### Then, in order
|
||
|
||
| Round | What | Lanes | Why then |
|
||
|---|---|---|---|
|
||
| **+1** | The unmarked layer — UNMARKED §1–§5 with all nine corrections (`TYPE_W.stall = 0`; hoist the atlas alloc to `ensureAtlas()` — `buildings.js:452-462` allocates lazily inside `drawSign`, so **49 chunks across the corpus** where every shop is hidden would hit `new THREE.CanvasTexture(null)`; crate to `d/2 + 2.4`, outboard of the awning post line at `d/2 + 2.0`; discovery hook **inside** the successful-entry branch, after `index.html:491` and `:503`, not on the `:489` dispatch) | B + F | Needs the address layer to have a name for the nameless, and the fog to hide the hole. |
|
||
| **+2** | The first-floor window band on 566 multi-storey shops | B | +0 draws, ~24 tris/shop. This is v8's own headline, paid. |
|
||
| **+3** | The arcade, treated as an arcade — district-keyed, **synthetic only** (roof lights, tiled floor, reverb, E's 5.86 m `longbench`) + a mouth so it stops being a graph orphan (measured: arcade edge 14's endpoints are degree 1 and 1; the announcement is 14.7 m of bare ground) | B + E | 17 shops, one town, but it is the **only** place on the synthetic where a type-plus-place clue resolves to a set you can scan: `record\|arcade` = 9 hosts, 32 m spread, 7 s end to end. |
|
||
| **+4… (a different epoch)** | Enterable upstairs | C + F + B | Priced in §5. |
|
||
|
||
---
|
||
|
||
## 5. THE HARD PROBLEMS, ANSWERED
|
||
|
||
### Replayability in a deterministic world
|
||
**The town is the constant; the contents are the variable.** Verified across katoomba, fitzroy and castlemaine: re-seeding a cache town leaves shop ids, lots, types and names **byte-identical** — only `storeys` and `hours` re-jitter (`plan_osm.js:274`, `seedFor(citySeed,'osmshop',id)`). And `save.js:172` already treats a town change as travel, so you keep cash and collection.
|
||
|
||
So `?seed=N` is *the same Katoomba, different secrets*: the same streets, the same shopfronts, a different unmarked set, a different sale roster, different upstairs. **Your map survives; your knowledge of what's behind the doors does not.** That is exactly the right split for a game about knowing your town, and it is why the fog keys by town.
|
||
|
||
Two honest limits. On small towns the hidden-set reshuffle is weak — 13 of 24 towns pick from N=2 with one dominant candidate, so a Jaccard-based "different secrets" gate goes red on bendigo, braddon, launceston, redhill and glebe. That gate must be size-aware or the reserved slot must take a seeded draw. And the synthetic re-rolls everything including shop ids, so it is a different town at every seed, not the same one.
|
||
|
||
### Findable vs frustrating
|
||
Answered by arithmetic, not by feel. **The director sizes the clue; the gate enforces the size.**
|
||
|
||
- Two terms (street + type) on a real town: **median cohort 1–4**, p90 2–18, **19–89% uniquely identified**. Without the street term: median 3–49.
|
||
- Adding side-of-street: median 1–4, **27–89% unique**, worst tails cut (katoomba 18→15, fitzroy 10→7, brunswick 12→9).
|
||
- **51–100% of a real town's shops (median ~85%) are clueable at ≤4 doors within ≤350 m.**
|
||
|
||
The gate must assert the **search**, not the answer. VERTICALITY's G7 (*"the clue's candidate set has ≥2 members"*) is a floor on the wrong quantity and goes green on an 87-door clue — measured, `toy|mainstreet` on the synthetic sends you to 87 doors over a 770 m spread, 167 seconds of walking. Rewrite as a **ceiling on doors-the-clue-sends-you-to, in [1, 4], with span ≤350 m, over 200 seeded days × 24 towns.** As corrected it fails on today's data, which is the point.
|
||
|
||
The failure mode is then never *"I wandered for four minutes and lost."* It is *"I haven't learned that part of town yet"* — with three outs and a permanent gain either way.
|
||
|
||
### The interiors machinery's one-room assumption
|
||
**Don't touch it this epoch.** The minimum version's interior delta is exactly zero: a hidden shop, a sale shop and a normal shop all call the same `buildInterior(shop)`.
|
||
|
||
When it does come due, the price is known and it is not what the depth map said:
|
||
- **The blocker is not dispose** — `ctx.disposeAll()` (`interiors.js:242-248`) tracks every geometry/material/texture regardless of sub-group, so N rooms through one Ctx dispose correctly today. `binUnderAim` and `stockInfo` both traverse, so a second room's crates are diggable for free.
|
||
- **The blocker is not draws — but the margin is six, not one-nine-one.** `C-progress.md:841`: GLB-off 168, **GLB-on 344**, both against ≤350. `index.html:349` ships `useGLB: !NOASSETS`. The 159 the depth map cites is the GLB-off sweep. **Correct the house record.**
|
||
- **The blocker is the grid.** `layout.js:337` makes ONE `makeGrid(W,D)`; `interior_mode.js:322-330` rejects any position with `|x| > W/2` or `|z| > D/2` **before consulting occupancy**. A fully built, fully lit second room outside that rectangle is unreachable.
|
||
- Full bill: one merged grid with a doorway cell the Manhattan carve at `layout.js:408` may not close · a second path guarantee (`:391-408` flood-fills one spawn to one counter) · per-room keeper resolution (`interior_mode.js:396` is `.find`, not `.filter`) · a second `exits[]` entry with `toStreet:false` **and an opener** — the contract is already honoured at `:335`, which makes it a vacuous-gate trap: ship the door without the opener and a naive "the room has 2 exits" gate passes while nothing on earth opens it · a shell variant, because `shell.js:113-114` calls `buildShopfront` unconditionally and would give a first-floor room ground-floor glazing, an EXIT plate, and a backdrop canvas painting a kerb at row 74 · plus, for anything called "stairs", the two Y-clamps and a floor-height ground truth no structure carries, and the seven module singletons `enter()`/`exit()` own.
|
||
|
||
**≈ one Lane C round + one Lane F round for a second room. An epoch for stairs.**
|
||
|
||
### The 8-draw margin
|
||
The pin is **292 draws at night, 282/283 at noon** (`F-progress.md:9,962`). Everything in layers 1–3 is DOM, 2D canvas, or data: **zero draws, and the count should be *identical*, not merely under budget.** Gate on `renderer.info.render.calls` matched exactly, **at night and at noon** — the window band is emissive at night and lands on the worst frame.
|
||
|
||
The standing refusal holds and should be written as law: **no new per-chunk mesh class, ever.** Measured live chunks: 25 live / 49 in the dispose window on every real town (all BIG_CITY, R=2); 23/42 on the synthetic. At 8 draws of margin, +1 draw × 49 chunks is not a cost, it is a breach. All depth geometry rides the existing merged lists — `boxMats`, `awnings`, `footGeos`, `windowGeos`, `doorGeos`, `signQuads` — the way the R38 yards change did. `makeInstanced` sizes to `list.length` with no cap; that mechanism is verified and it is what makes layers 4 and 5 free.
|
||
|
||
---
|
||
|
||
## 6. THE CUT LIST
|
||
|
||
| Cut | Measured reason |
|
||
|---|---|
|
||
| **CLEARANCE** (VERTICALITY R4) | The clue sends you to **18 doors at the median, 87 at the max** — the design counted correct answers, not searches, because the player can't see which hosts have an upstairs. And `minimap.js:52-58` already hands over the clue's three attributes. Re-found it or leave it out. |
|
||
| **THE BLIND DOCKET** (UNMARKED §6) | Blind shop to nearest roster shop: **2,217 m hobart, 1,971 m fremantle, 1,538 m newtown** against a 704 m RUN budget. Impossible on 7 towns. On 9 others the blind shop is already **inside** the named clique, so the paper names the shop it says has no name. |
|
||
| **The laneway leg** | **2 shops in the entire corpus front a `lane` edge** (both launceston), 0 on the synthetic. The synthetic's two laneways are graph orphans (endpoint degrees 1 and 1) and the east lane runs **under seven arcade lots** that overhang its carriageway by up to 1.63 m — `lotCollider` would eject the player from the lane. |
|
||
| **The merged-grid back room** (VERTICALITY R5) | Surplus depth behind a max-depth room: **p50 = 0.10 m**, p75 1.96, p90 3.22. Only **31 of 493 shops (6.3%)** clear 4 m. It is a lottery on the archetype roll, and it is the only piece that touches the path guarantee. |
|
||
| **Real-town arcade and lane flavour** | `'arcade'` on a real town is OSM `pedestrian\|footway\|path\|steps\|cycleway` — **990 edges, 39,276 m of ordinary suburban footpath.** Roofing it roofs 39 km of pavement. Arcade treatment keys off `district.kind`, synthetic only. Make it a code rule. |
|
||
| **District-keyed skins on real towns** | `plan_osm.js:305` is one unconditional `addDistrict('mainstreet')`. All 24 caches: 1 district, all lots `use:'shop'`. Code rule, not a caveat. |
|
||
| **The facade-skin clue term** | 47 files in `web/assets/gen/`, no colour or material name anywhere in the codebase, and skins are shared across types (`facade-corrugated` in pawn/stall/band_room). Leaks type without adding information. |
|
||
| **Physical corner street signs** | +1 draw × up to 49 live chunks against 8 of margin. Already refused; endorsed. |
|
||
| **Map omission for unmarked shops** | Punches a hole up to **16.8 px wide — 6.5× a lot width** — in a contiguous terrace on the synthetic, the one town where the map is legible and the default and classic boot. Fog handles it structurally; omission does not. |
|
||
| **The segments-remaining director radius** | Median shops reachable from a random shopfront: **marrickville 1, braddon 2, redhill 3.** Narrows the pool below the clue before the clue speaks. |
|
||
| **The window card as a *cue*** | 28 px in a 168 px cell on a 0.25 m quad = **0.042 m of letter, legible to ~3.8 m** — inside the free 6 m tooltip at `hud.js:302`. Demote to dressing. One ranged cue exists: the crate. |
|
||
|
||
---
|
||
|
||
## 7. WHERE IT SITS
|
||
|
||
**This is v9 — THE CITY IS THE GAME — with a one-round probe landing inside v8.**
|
||
|
||
Three reasons.
|
||
|
||
1. **v8 is texture; this is mechanic.** Ambient audio, per-town character, the magpie, backyards — every v8 item makes the city *feel* more like a place. This makes it *function* as one. Folding a new win condition into an epoch about atmosphere leaves the epoch un-nameable, and PROCITY names its epochs for a reason.
|
||
2. **The probe belongs in v8 because it decides whether v9 exists.** Fog + the address layer + the type label is one round, zero draws, zero geometry, and it is falsifiable on its own: does a fogged, street-named town make you want to walk it? If the answer is no, v9 doesn't happen and we spent one round. If the answer is yes, v9 has a proven core before anyone models a polygon. **Never build the geometry first.**
|
||
3. **v8's own headline is already the bridge.** SOMEONE LIVES HERE is answered — literally, on 566 buildings, at +0 draws — by the first-floor window band. `audio.js:190-193` has been asserting that upper floors are inhabited since it shipped. Land the band in v8 and the epoch keeps its name. Then v9 makes the city knowable. Then, if it earns it, a later epoch makes those upper floors enterable, and by then the window is already there to walk up to.
|
||
|
||
**v8 wave 2:** the address layer · the fog · the type label · the first-floor window band.
|
||
**v9 — THE CITY IS THE GAME:** the notebook · the unmarked · the sale director · the arcade.
|
||
|
||
---
|
||
|
||
## 8. WHAT NEEDS JOHN
|
||
|
||
Six taste calls. Nobody else can make them.
|
||
|
||
**1. Does the default boot become a real town?**
|
||
This is the big one. The synthetic is 493 shops on **22 streets**, with 304 distinct names and a 17-way collision (`The Toy Nook` ×17, `Marbles & Kites` ×14, `The Book Cellar` ×12). Measured: **32% of its shops are clueable at ≤4 doors, against 51–100% (median ~85%) on the real towns.** Its best clue cell is a median of 9 doors. It is a magnificent strip and it is not a town you can learn. A bare `index.html` boots it today; `?classic=1` freezes it byte-identical forever regardless, so the covenant is not at risk. But if the win condition is knowing your town, the town the game opens on should be one that can be known. *(A smaller version of the same call: let the synthetic's hunt be district-scale — "somewhere in the backstreets" — an honest browse rather than a broken precision hunt.)*
|
||
|
||
**2. How harsh is the fog?**
|
||
Three settings, in ascending mercy: **(a)** shops you've walked within 25 m of · **(b)** shops you've actually entered · **(c)** *streets* you've walked, with every shop on a walked street revealed. (c) is the most map-like and the most forgiving — you learn a town in streets, not in shopfronts — and it makes the address layer the unit of knowledge, which is tidy. (b) is brutal and would take a very long time to fill in. My instinct is (c); it's your call.
|
||
|
||
**3. Does the clue name the type or the shop?**
|
||
*"the record shop on Templeton Street"* teaches you the city. *"Rotation, somewhere up Templeton"* teaches you the shops. Type-clues make the town the thing you learn; name-clues make the roster the thing you learn. They are different games and the arithmetic supports either.
|
||
|
||
**4. Does an unmarked shop ever get a name?**
|
||
Or does it stay, forever, *"the one with the crate out front, Lyttleton Street"* — a place you know by where it is and not by what it's called. The second is better writing and it is also the harder sell to a player who wants a list.
|
||
|
||
**5. Is being late a discount or a loss?**
|
||
Arrive in segment 4 and the good ones are gone (`gone` grows with arrival time, seeded) — or the sale is simply over and the door is locked. The first is a difficulty curve; the second is a deadline. The code supports both at the same cost.
|
||
|
||
**6. Do keepers give directions?**
|
||
It is the anti-frustration valve, it costs almost nothing, it rides the existing rumour seam — and it is the single most *someone lives here* thing in this entire document. It is also the moment the city stops being an opponent and becomes a place with people in it who are on your side. That's a tonal decision, not an engineering one.
|
||
|
||
---
|
||
|
||
### Two things for the house record, regardless of what you decide
|
||
|
||
1. **The interior draw margin is six, not one-nine-one.** `C-progress.md:841` — GLB-off 168, GLB-on **344**, budget ≤350, and `index.html:349` makes GLB-on the default boot. The depth map's "draw budget is not the problem" line is measured off the wrong sweep and has to come out before it authorises a room somebody can't afford.
|
||
2. **Street names are already in the repo.** 17,835 named ways of 19,132, 93.2%, on 23 towns, recoverable to a median of 0.03–0.90 m with no citygen change and no golden move. Whatever the shape ends up being, that data should stop being dropped on the floor. |