diff --git a/B-progress.md b/B-progress.md index 1968bfd..374b553 100644 --- a/B-progress.md +++ b/B-progress.md @@ -4,6 +4,57 @@ plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's `generatePlan` (which landed mid-session). Everything below was verified live in a browser. +## Round 40 (§40.3) — the ?r= breach GATED at its true number, classic's selector ratified, the fog signed — plus §40.4's arcade kit (Fable-routed) + +Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §40. Everything below: today's tree, +headless fresh contexts, no-store servers, pin method (stepwalk 2 m · yaw 0 · laps 2 · 802 stations). + +**1. THE ?r=3 BREACH — re-measured, then GATED (option b), because the carried 307 was fiction.** + +| mode | before (carried) | re-measured (pin method, final tree) | law now | +|---|---|---|---| +| default noon / night | 282 / 292 (R38 pin) | **282 / 291** ✅ pin reproduced | ≤300 (unchanged) | +| `?classic=1` night | 269 | **269 / 90,580 tris — byte-exact** | ≤300 (unchanged) | +| `?r=3` noon / night | "307 / 317" (stale bookmark) | **382 / 391** at 45–48 live chunks | **≤420, its OWN declared law** | + +Decision: **gate, not shave** — the 91-draw excess is the R+1 live-chunk window (48 vs 31) on a +per-chunk cost already collapsed to ~1 draw/kind/chunk; a shave that size is a content cut to the +DEFAULT town to legalise a diagnostic. So: `?r=` above auto now declares +`PROCITY.budget = {draws: 420, tris: 200k, diagnostic: true}`, console-warns its law, the HUD warn +threshold reads it, `DBG.info().budget` carries it, and **NEW `tools/qa/r40_lane_b.py`** asserts it +with both arms non-vacuous (measures 386 ≤ 420 at 8 m stations AND 386 > 300, so the exemption is +load-bearing; ≤300 someday ⇒ "retire the exemption", printed not failed). Ceiling 420 = 391 + ~7% +jitter margin; growth past it goes red and is re-pinned consciously. → F: one qa.sh line; README +`?r=N` row still says 307. + +**2. ?classic=1 TOWN SELECTOR (Fable ruled yes) — verified true by construction, made deliberate.** +Classic boots with the selector present + visible, **all 27 options** (3 fixtures + 23 real/godverse +towns), one towns-fetch = exactly the named POST_V2_EXCEPTION (`assets/towns/index.json`), picks +FROM classic stay classic (query string preserved), zero draws (DOM chrome), 0 errors. Ruling +written into the selector block (hud.js) and asserted in r40_lane_b.py. → F: the flags_check +POST_V2_EXCEPTIONS label still calls this a debt-ledger question; it's ruled now. + +**3. FOG SIGNAGE — three surfaces that said nothing now consume A's layer** (`street || label`, +null handled once, no town-type branch): **HUD `street` row** ("Lurline Street" on katoomba_real, +supplier `ways`; "the south end of the main street" on the synthetic) · **door tooltip** +("🚪 Little Paris Cafe · Katoomba Street — click to enter") · **fog-map caption** +("… 2 streets walked · Lurline Street · press M to close"). New `createStreetLocator(plan)` in +discovery.js (the probe's hash, read-only; signage ≠ discovery, so not fog-keyed); rides the +existing frame%6 throttles; zero draws. **Classic-gated by construction**: locator built only +`!CLASSIC`, so `#pc-street` doesn't exist in classic's DOM and the tooltip is the pre-R40 byte — +A/B'd on the same door, asserted both arms in r40_lane_b.py. + +**4. §40.4 ARCADE KIT (routed mid-round by Fable, E's spec applied verbatim):** roof spans (depth +2.5 = half-lane, slabs meet flush), **a spanned roof has no posts**; `ctx.arcadeBlocks` keyed on +`district.kind` never `edge.kind`; explicit `ARCADEKIT` gate (classic forces off — mandatory; +`?arcadekit=0` = falsifiability control). Measured: **−34 post instances exactly, lane draws +120 → 120 (+0)**, shot pair `docs/shots/laneB_r40/`, classic pin cell byte-exact after the change. + +Goldens **157,647/157,647, 0x5f76e76 unmoved** (selfcheck after every wave). r40_lane_b gate: +**all assertions green**. Files: `web/index.html` (`[Lane B R40]` marks), `hud.js`, `minimap.js`, +`discovery.js`, `dbg.js`, `chunks.js`, `buildings.js`, new `tools/qa/r40_lane_b.py`, +`docs/shots/laneB_r40/`, these docs. **No git commands run** (lane treaty; Fable commits). + ## Round 39 (v9 Layer 2 — THE FOG, item 39.2) — **zero draws, and the map is finally readable** Full detail + every number in LANE_B_NOTES §39. `minimap.js` drew all 493 synthetic shops from the diff --git a/docs/LANES/LANE_B_NOTES.md b/docs/LANES/LANE_B_NOTES.md index 476ad09..ee00063 100644 --- a/docs/LANES/LANE_B_NOTES.md +++ b/docs/LANES/LANE_B_NOTES.md @@ -1,5 +1,127 @@ # LANE B — NOTES (measured) +## Round 40 (§40.3 + routed §40.4) — the ?r= flag learns its own law, classic keeps its shop window, and the fog gets its signage + +All numbers this section: today's tree, headless Chromium, fresh contexts, port-isolated no-store +servers, 1280×720, seed 20261990 unless a town is named. Budget cells use **the pin method of +record** (R37/R38: `budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 +stations down the main spine at yaw 0, the worst bearing). + +### §40.3-1 — THE ?r=3 BREACH: re-measured 391, and the ruling is GATE, not shave + +**The carried "307" was a stale bookmark, as the round suspected — the true number is 391.** The +R37-era 307/317 pin predates five rounds of default-on content; nobody had re-measured the override +since. On today's tree, same method as the budget of record: + +| synthetic, pin method | noon (seg 2) | NIGHT (seg 5) | live chunks max | tris worst | +|---|---|---|---|---| +| default (`r` auto = 2) | **282** | **291** | 32 | 120,093 ✅ pin reproduced (R38: 282/292) | +| `?classic=1` | — | **269** | 32 | 90,580 ✅ **byte-exact to the R38 pin** | +| **`?r=3`** | **382** | **391** | **48** | 132,829 (tris legal) | + +**Why shave was refused, with the numbers:** the breach is structural, not fat. `?r=3` carries +45–48 live chunks against the default's 31 (R+1 dispose window over a 7×7 vs 5×5 set), and the +excess is 91–100 draws. Lane B's per-chunk cost is already collapsed to ~1 draw per kind per chunk +(facade atlas, merged instancing — R2 close-out); there is no 91-draw shave available in a +diagnostic boot that is not a content cut to the DEFAULT town. Shaving a diagnostic override by +making the shipped town poorer is backwards. + +**So the flag now documents its own budget law and qa asserts it (the round's option b):** + +- `index.html` (`[Lane B R40]`, the RADIUS block): any `?r=` **above** the auto radius declares + `PROCITY.budget = { draws: 420, tris: 200000, radius, auto, diagnostic: true }` and prints a + `console.warn` naming the law ("DIAGNOSTIC boot … ≤420 draws … not a supported boot"). At or + below auto: `{ draws: 300, diagnostic: false }` — the street law, unchanged. +- **The ceiling is 420** = 391 measured + ~7% for crowd/streamer jitter (the R37 lesson: settle + policy and station phase move a walked worst by single digits). If the town grows past it, the + gate goes red and the ceiling is re-pinned CONSCIOUSLY — that is what a pinned law is for. +- `hud.js` warn threshold reads the boot's own law (`budget.draws`), so the readout stops shaming a + documented diagnostic at 300 and starts telling the truth at 420. Default boots render identical + DOM (same 300). +- `dbg.js` `info().budget` carries the law, so **a harness asserts against the boot's declared + ceiling, never a hardcoded 300**. +- **`tools/qa/r40_lane_b.py` (NEW file — races no lane) is the enforcing gate**, all arms + non-vacuous: default declares 300/not-diagnostic · `?r=3` declares 420/diagnostic + the warn + fired · a night stepwalk under `?r=3` measures ≤ its own ceiling (read 386 ≤ 420 at 8 m + stations) **and** > 300 (the exemption is load-bearing, not vacuous — if it ever reads ≤300 the + gate prints "retire the exemption" rather than failing) · tri law unchanged (131,893 ≤ 200k) · + 0 console errors. **→ Lane F: one line wires it into qa.sh** — and README.md's `?r=N` row still + quotes 307; please update it to the law (391 measured R40, ceiling 420, gated + warned). + +### §40.3-2 — CLASSIC'S TOWN SELECTOR: the ruling was already true by construction; now it is deliberate and gated + +Fable ruled classic SHOWS the selector ("information, not gameplay state"). Verified on today's +tree: `?classic=1` boots with the selector **present and visible, all 27 options** (synthetic + 3 +OSM fixtures + the 23-town real/godverse pack), its towns-fetch surface is **exactly the one named +POST_V2_EXCEPTION** (`assets/towns/index.json`, Fable's R37 ruling), navigation preserves the query +string so a town picked FROM classic STAYS classic, **zero draws** (a ` the renderer + // never sees), its one fetch (assets/towns/index.json) is the named POST_V2_EXCEPTION Fable's R37 + // ruling carries for classic, and navigation preserves the full query string — so a town picked + // FROM classic stays classic. Verified R40: classic boots with all 27 options, 1 towns-fetch + // (index.json only), 0 console errors. qa asserts it: tools/qa/r40_lane_b.py. // Navigates to the matching URL params, preserving everything else (seed, flags). Lives in the HUD // overlay, so it hides with the HUD for money shots; usable when the pointer is free (Esc). No // index.html seam. The real-town list is built SYNCHRONOUSLY from the fallback (so the picker always @@ -304,6 +316,49 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } let hovered = null; // { shopId, name } let frame = 0, fpsAcc = 0, fpsCount = 0, fpsShown = 0; + // ── [Lane B R40 §40.3 — FOG SIGNAGE] Lane A's address layer on the HUD ──────────────────────── + // GATED BY CONSTRUCTION: none of this DOM exists until the shell calls setAddresses(), and the + // shell only does that on non-classic boots — so ?classic=1's HUD is byte-identical with no flag + // test here (the R19 selector / R30 game-UI pattern). Two surfaces, both zero-draw DOM: + // • a "street" row in the debug readout — the street you are standing on (locator.edgeAt → + // addr.streetOf), falling back to the nearest shopfront's locality label (addr.localityOf → + // street || label, LANE_A_NOTES §39: print `label`, never branch on town type); + // • the door tooltip carries the shop's street/locality between name and action. + // Resolution rides the existing frame%6 throttle (the door-raycast cadence); the address calls + // are wrapped once — a throwing layer disables itself rather than spamming (minimap.js's rule). + let addresses = null, locator = null, elStreet = null, shownStreet = null; + const whereCache = new Map(); // shopId → street/label string | null (localityOf is pure per boot) + function whereOf(shopId) { + if (!addresses || typeof addresses.localityOf !== 'function') return null; + if (whereCache.has(shopId)) return whereCache.get(shopId); + let text = null; + try { const loc = addresses.localityOf(shopId); text = (loc && (loc.street || loc.label)) || null; } + catch (err) { addresses = null; return null; } + whereCache.set(shopId, text); + return text; + } + function setAddresses(a, loc) { + addresses = a || null; locator = loc || null; + whereCache.clear(); shownStreet = null; + if (addresses && locator && !elStreet) { + const row = document.createElement('div'); + row.innerHTML = 'street '; + $('pc-town').parentElement.insertAdjacentElement('afterend', row); + elStreet = row.querySelector('#pc-street'); + } + if (elStreet) elStreet.textContent = '—'; + } + function resolveStreet() { + if (!addresses || !locator || !elStreet) return; + let text = null; + try { + const eid = locator.edgeAt(camera.position.x, camera.position.z); + if (eid != null && typeof addresses.streetOf === 'function') text = addresses.streetOf(eid); + if (!text) { const sid = locator.shopNear(camera.position.x, camera.position.z); if (sid != null) text = whereOf(sid); } + } catch (err) { addresses = null; return; } + if (text !== shownStreet) { shownStreet = text; elStreet.textContent = text || '—'; } + } + function nearestDoor(rects, point) { let best = null, bd = Infinity; for (const r of rects) { @@ -334,7 +389,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } if (fpsAcc >= 0.4) { fpsShown = Math.round(fpsCount / fpsAcc); fpsAcc = 0; fpsCount = 0; } frame++; - if (frame % 6 === 0) raycastDoor(); + if (frame % 6 === 0) { raycastDoor(); resolveStreet(); } // [Lane B R40] signage rides the raycast cadence // v7 game surface — lazy construction + refresh both live in tickGame() (R33 fix: the build used // to happen ONLY here in street frames, so a door entered fast after boot had no crate/bar inside @@ -347,9 +402,12 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } const h = hovered.hours; const now = window.PROCITY && window.PROCITY.currentHour ? window.PROCITY.currentHour() : null; const closed = h && now != null && !(now >= h[0] && now < h[1]); + // [Lane B R40 §40.3] the shop's street/locality on the tooltip (null when unresolved or classic + // — setAddresses never called ⇒ `where` is always null ⇒ the pre-R40 string, by construction) + const where = hovered.shopId != null ? whereOf(hovered.shopId) : null; elTip.textContent = closed ? `🔒 ${hovered.name} — CLOSED · opens ${String(h[0]).padStart(2, '0')}:00` - : `🚪 ${hovered.name} — click to enter`; + : `🚪 ${hovered.name}${where ? ' · ' + where : ''} — click to enter`; elTip.style.opacity = 1; } else elTip.style.opacity = 0; @@ -358,8 +416,8 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } if (info.clock) elClock.textContent = `${info.clock.label} ${info.clock.hour}${info.clock.night ? ' 🌙' : ''}`; elFps.textContent = fpsShown; const draws = renderer.info.render.calls, tris = renderer.info.render.triangles; - elDraws.innerHTML = draws > 300 ? `${draws}` : draws; - elTris.innerHTML = tris > 200000 ? `${(tris / 1000 | 0)}k` : `${(tris / 1000 | 0)}k`; + elDraws.innerHTML = draws > budget.draws ? `${draws}` : draws; // [Lane B R40] the boot's own law + elTris.innerHTML = tris > budget.tris ? `${(tris / 1000 | 0)}k` : `${(tris / 1000 | 0)}k`; if (info.chunks != null) elChunks.textContent = info.chunks; } } @@ -399,5 +457,6 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop } } return { update, tickToast, showToast, setVisible, tickGame, dispose, getHovered: () => hovered, - getFps: () => fpsShown }; + getFps: () => fpsShown, + setAddresses }; // [Lane B R40 §40.3] fog signage — the shell wires it on non-classic boots only } diff --git a/web/js/world/minimap.js b/web/js/world/minimap.js index d9e0b66..73a86ee 100644 --- a/web/js/world/minimap.js +++ b/web/js/world/minimap.js @@ -35,6 +35,12 @@ const MAX_LABELS = 40; export function createMinimap(plan, known = null, opts = {}) { let addresses = opts.addresses || null; // Lane A's createAddresses(): streetOf / localityOf + // [Lane B R40 §40.3 — FOG SIGNAGE] optional street locator (discovery.createStreetLocator): lets + // the fog-arm caption say where YOU are standing ("· Katoomba Street"). Consumed ONLY inside the + // fog arm (`known !== null`), so the classic/null-arm map — caption included — stays byte-for-byte + // pre-R39 with no flag test here, exactly like `known` itself. The shell passes null under classic. + let locator = opts.locator || null; + let capBase = null, capLoc = null; // caption = capBase + (' · ' + where-you-are) + tail const wrap = document.createElement('div'); wrap.id = 'pc-map'; @@ -258,9 +264,30 @@ export function createMinimap(plan, known = null, opts = {}) { for (const id of known.edgeIds()) { let n = null; try { n = addresses.streetOf(id); } catch (e) { names.clear(); break; } if (n) names.add(n); } if (names.size) streets = names.size; } - cap.textContent = `${plan.name} — seed ${plan.citySeed} · ${c.shops} shop${c.shops === 1 ? '' : 's'} known · ` + capBase = `${plan.name} — seed ${plan.citySeed} · ${c.shops} shop${c.shops === 1 ? '' : 's'} known · ` + (streets != null ? `${streets} street${streets === 1 ? '' : 's'}` : `${c.edges} street segment${c.edges === 1 ? '' : 's'}`) - + ` walked · press M to close`; + + ` walked`; + cap.textContent = capBase + (capLoc ? ` · ${capLoc}` : '') + ` · press M to close`; + } + + // [Lane B R40 §40.3] where the player dot IS: the street under your feet by name, else the nearest + // shopfront's locality label — A's contract verbatim (`street || label`, print it, never test the + // town type; null is rendered by omission). Fog arm only; a throwing layer disables itself. + function refreshLocation(px, pz) { + if (!locator || !addresses || capBase == null) return; + let text = null; + try { + const eid = locator.edgeAt(px, pz); + if (eid != null && typeof addresses.streetOf === 'function') text = addresses.streetOf(eid); + if (!text && typeof addresses.localityOf === 'function') { + const sid = locator.shopNear(px, pz); + if (sid != null) { const loc = addresses.localityOf(sid); text = (loc && (loc.street || loc.label)) || null; } + } + } catch (err) { locator = null; return; } + if (text !== capLoc) { + capLoc = text; + cap.textContent = capBase + (capLoc ? ` · ${capLoc}` : '') + ` · press M to close`; + } } function draw(playerPos, fwd) { @@ -277,6 +304,7 @@ export function createMinimap(plan, known = null, opts = {}) { } ctx.clearRect(0, 0, W, H); ctx.drawImage(off, 0, 0); + refreshLocation(playerPos.x, playerPos.z); // [Lane B R40] caption: where the dot is (µs; writes DOM only on change) } const x = T.tx(playerPos.x), z = T.tz(playerPos.z); // heading (world +X→right, +Z→down on the map) @@ -292,8 +320,9 @@ export function createMinimap(plan, known = null, opts = {}) { setVisible: (v) => { wrap.style.display = v ? 'flex' : 'none'; }, dispose: () => wrap.remove(), // [R39] Lane A's address layer, injectable — the shell wires it at boot when citygen exports - // createAddresses, and it can land mid-session without a reload. - setAddresses(a) { addresses = a || null; cacheKey = null; lastCounts = null; }, + // createAddresses, and it can land mid-session without a reload. [R40] + the optional street + // locator (fog-arm caption signage; the shell passes null under classic — see the header note). + setAddresses(a, loc) { addresses = a || null; locator = loc || null; cacheKey = null; lastCounts = null; capLoc = null; }, // what the map is actually showing, for the gates: metres across, and the px a shop lot renders at get view() { const med = (arr) => { const b = arr.slice().sort((p, q) => p - q); return b.length ? b[b.length >> 1] : 0; };