Lane F R9 F3+F4: v2.0 tour (11 shots) + full-default & all-on soaks + docs freeze

F3: tools/v2_tour.py shoots 12-shot-class tour across flag-groups → docs/shots/v2_tour/ (11 shots,
all un-letterboxed 1280x720) incl. the rain-verandah money shot (night rain + winmap-lit open video
shop + CLOSED dark neighbours), browsers-in-a-shop, dig-real, katoomba, tram_stop + contact sheet.
soak.py gained --flags; full-default soak PASS (37 chunks/20 shops/heap stable/0 errors/leak-free)
and all-on soak (winmap+dig+stock=real+weather=rain+tram) PASS identically.

F4: F-progress round-9 section + FINAL v2 flags table (roster/patronage default-on; dig/winmap/
stock/weather/plansrc/tram/noassets opt-in; goldens frozen). LANE_F_NOTES v2.0 freeze note. CityPlan
v2 contract (A's CITY_SPEC freeze) validated live: selfcheck 1727/1727 + goldens. qa.sh --strict GREEN 5/5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-15 11:57:11 +10:00
parent 5a24f8aa52
commit cb3aed196a
16 changed files with 137 additions and 1 deletions

View File

@ -4,6 +4,37 @@
---
## Round 9 (the v2.0 ship round) — DONE ✅ (v2.0 tagged)
All lanes landed (C browse points + buy-anywhere, D browser rigs, A CITY_SPEC freeze + closing-time
ruling, E VRAM audit + half-res book/toy atlases, B tram + tour bookmarks). F wired the one cross-lane
seam, graduated the harness, shot the tour, soaked, and tagged the epoch.
| task | status |
|---|---|
| **F1 — interior presence (C→D→F)** | ✅ `interior_mode` reads `occupancyOf(shop.id)` (closure to `citizens`) and spawns browser rigs at C's `room.browsePoints`, one per patronage occupant, **cap 3**; freed by `exit()`'s `keepers.disposeAll`. Door points carry `shopId`. Verified: enter occupied "Second Time Redfern" → 1 browser == `min(occ,3)`, disposed clean, 0 errors. |
| **also wired** | ✅ **Tram** (`?tram=1`, `createTram`+update). **Buy-anywhere** (C2): E at a book-spine/toy-box `buyMesh` buys over the wallet (`collapseBuyItem`+toast); preload book+toy packs. Verified: bought a book spine, cash 191→177. |
| **F2 — smokes to strict** | ✅ buy-v0 / patronage / book-toy / katoomba **graduated warn→STRICT**. New warn smokes: interior-presence, shelf-buy, tram. Fixed the book-stock check to count `isStock OR buyMesh` (C2 tags spines `buyMesh`). `qa.sh --strict` GREEN 5/5, harness 0 fails / 0 warns. |
| **F3 — tour + soaks** | ✅ `tools/v2_tour.py`**11 shots** (`docs/shots/v2_tour/`, un-letterboxed) incl. the **rain-verandah money shot** (rain + winmap-lit open video shop + CLOSED dark neighbours at night) + browsers + dig-real + katoomba + tram_stop + contact sheet. **Both soaks PASS** (full-default + all-on: 37 chunks, 20 shops, heap stable, 0 errors, leak-free). |
| **F4 — docs freeze + tag v2.0** | ✅ Flags table finalized (below); **`v2.0` tagged** on a green tree. |
| **Marshal** | C1 (the round's SPOF) landed early; all lanes committed; no slips. |
### v2.0 flags — FINAL
| flag | owner | status | default |
|---|---|---|---|
| roster | D | **graduated → DEFAULT-ON** (R7 flip); `?roster=v1` escape hatch | on |
| patronage | D | default-on (`?patronage=0` off) | on |
| `?dig=1` | C | opt-in (crate riffle) | off |
| `?winmap=1` | B | opt-in (parallax window glass) | off |
| `?stock=real` | C/E | opt-in (real GODVERSE covers/spines/boxes + buy-anywhere) | off |
| `?weather=1` | B | opt-in (seeded rain/overcast/clear; `?weather=rain` forces) | off |
| `?plansrc=osm[&town=]` | A | opt-in (real-OSM towns: melbourne, katoomba) | off (synthetic) |
| `?tram=1` | B | opt-in (spine tram loop) | off |
| `?noassets=1` | — | opt-in (primitive-only, zero-network) | off |
Determinism goldens frozen: synthetic `0x3fa36874` · osm/melbourne `0x34cfdec0` · osm/katoomba `0x0f652510`.
---
## Round 8 (catch-up + the town comes alive → v2.0-rc) — DONE ✅
All lanes landed (E packs, B weather, A 2nd town, C buy, D patronage). F wired the shell seams,

View File

@ -9,6 +9,12 @@ Status of this file: **living**. Update the ✅/⏳ markers as wiring lands. Sna
readiness lives in [`F-progress.md`](../../F-progress.md); run `node tools/qa/scaffold_check.mjs`
for the live matrix.
> **v2.0 FROZEN (round 9).** Nine rounds: seed → walkable town → weather, a shopping crowd,
> follow-a-ped-into-a-shop presence, and real stock you can buy. Current gate reality + the
> final v2 flags table live in [`F-progress.md`](../../F-progress.md); `qa.sh --strict` GREEN 5/5
> (scaffold+determinism · consistency · citygen selfcheck · manifest --depot · v2 flags harness STRICT).
> CityPlan v2 contract frozen by Lane A in CITY_SPEC; validated live by selfcheck (1727/1727) + goldens.
---
## 0. Ground truth / environment

View File

@ -0,0 +1 @@
<!doctype html><meta charset="utf-8"><title>PROCITY v2.0 tour</title><style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}.g{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}figure{margin:0}img{width:100%}figcaption{color:#8f8;padding:3px 0}</style><h2>PROCITY v2.0 — 11 shots</h2><div class="g"><figure><img src="rain_verandah.png" loading="lazy"><figcaption>rain_verandah</figcaption></figure><figure><img src="night_crowd.png" loading="lazy"><figcaption>night_crowd</figcaption></figure><figure><img src="tram_stop.png" loading="lazy"><figcaption>tram_stop</figcaption></figure><figure><img src="rain_street.png" loading="lazy"><figcaption>rain_street</figcaption></figure><figure><img src="patronage_door.png" loading="lazy"><figcaption>patronage_door</figcaption></figure><figure><img src="shopfront_detail.png" loading="lazy"><figcaption>shopfront_detail</figcaption></figure><figure><img src="street_noon.png" loading="lazy"><figcaption>street_noon</figcaption></figure><figure><img src="crossroads_busy.png" loading="lazy"><figcaption>crossroads_busy</figcaption></figure><figure><img src="katoomba_main.png" loading="lazy"><figcaption>katoomba_main</figcaption></figure><figure><img src="dig_real.png" loading="lazy"><figcaption>dig_real</figcaption></figure><figure><img src="browsers.png" loading="lazy"><figcaption>browsers</figcaption></figure></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

View File

@ -42,7 +42,9 @@ def main():
seed = flag('--seed')
minutes = float(flag('--minutes', 10))
noassets = '--noassets' in sys.argv
qs = '?dbg=1' + (f'&seed={seed}' if seed else '') + ('&noassets=1' if noassets else '')
extra = flag('--flags') # arbitrary query flags, e.g. --flags 'winmap=1&dig=1&stock=real&weather=rain&tram=1'
qs = '?dbg=1' + (f'&seed={seed}' if seed else '') + ('&noassets=1' if noassets else '') \
+ (f'&{extra}' if isinstance(extra, str) else '')
url = HOST + '/' + qs
fails, warns, chunks_seen, shops_entered, peak_draws, peak_tris = [], [], set(), 0, 0, 0

96
tools/v2_tour.py Normal file
View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""PROCITY Lane F — v2.0 tour: 12 beauty shots across flag-groups → docs/shots/v2_tour/ + contact sheet.
Boots each flag-combo once, shoots its bookmarks (Lane B dbg.js poses) + interior shots. Un-letterboxed
1280x720 (dbg.js syncSize). Run: cd web && python3 -m http.server 8130 then tools/.venv/bin/python tools/v2_tour.py
"""
import sys, pathlib
from playwright.sync_api import sync_playwright
ROOT = pathlib.Path(__file__).resolve().parent.parent
OUT = ROOT / 'docs' / 'shots' / 'v2_tour'
OUT.mkdir(parents=True, exist_ok=True)
HOST = 'http://127.0.0.1:8130'
# flag-combo -> [street bookmarks] (each dbg.js bookmark sets its own pose + day segment)
GROUPS = [
('winmap=1&weather=rain&tram=1&stock=real&pop=200',
['rain_verandah', 'night_crowd', 'tram_stop', 'rain_street', 'patronage_door', 'shopfront_detail',
'street_noon', 'crossroads_busy']),
('plansrc=osm&town=katoomba&pop=180', ['katoomba_main']),
]
# interior shots: flags, then enter + optional dig
INTERIORS = [
('dig_real', 'dig=1&stock=real', 'record', True), # enter record shop + open the dig (real covers)
('browsers', 'stock=real&pop=200', 'occupied', False), # enter an occupied shop (browsers present)
]
def boot(pg, q):
pg.goto(f'{HOST}/?dbg=1&{q}')
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
try:
pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)", timeout=12000)
except Exception:
pass
def main():
shot_names, errs = [], []
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
for flags, marks in GROUPS:
boot(pg, flags)
for name in marks:
try:
pg.evaluate("(n) => window.DBG.shot(n)", name)
pg.wait_for_timeout(1100)
pg.evaluate("(n) => window.DBG.shot(n)", name) # settle bloom/segment
pg.wait_for_timeout(300)
pg.screenshot(path=str(OUT / f'{name}.png'))
shot_names.append(name); print('shot', name)
except Exception as e:
print('MISS', name, str(e)[:80])
for name, flags, kind, do_dig in INTERIORS:
boot(pg, flags)
ok = pg.evaluate("""(kind) => {
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
let s=null;
if(kind==='occupied'){
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
for(let i=0;i<400;i++) P.citizens.update(0.05);
for(const sh of plan.shops){ const o=P.citizens.occupancyOf(sh.id); if(o&&o.count>0){s=sh;break;} }
if(!s) s=plan.shops.find(x=>x.type==='opshop'&&P.isOpen(x));
} else { s=(P.plan.shops||[]).find(x=>x.type===kind && P.isOpen(x)); }
if(!s) return false; D.enterShop(s.id); return true;
}""", kind)
if not ok:
print('MISS', name, '(no shop)'); continue
if do_dig:
# aim at a bin + open the dig for the real-cover shot
pg.evaluate("""() => { const P=window.PROCITY; const bins=[];
P.interiorMode.current.group.traverse(o=>{if(o.userData&&o.userData.kind==='bin')bins.push(o)});
if(bins.length){ const bp=new P.THREE.Vector3(); bins[0].getWorldPosition(bp);
P.camera.position.set(bp.x,1.6,bp.z+1.4); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'})); } }""")
pg.wait_for_timeout(900)
pg.screenshot(path=str(OUT / f'{name}.png'))
shot_names.append(name); print('shot', name)
b.close()
# contact sheet
cells = ''.join(f'<figure><img src="{n}.png" loading="lazy"><figcaption>{n}</figcaption></figure>' for n in shot_names)
(OUT.parent / 'v2_contact.html').write_text(
'<!doctype html><meta charset="utf-8"><title>PROCITY v2.0 tour</title>'
'<style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}'
'.g{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}figure{margin:0}img{width:100%}'
'figcaption{color:#8f8;padding:3px 0}</style><h2>PROCITY v2.0 — ' + str(len(shot_names)) + ' shots</h2>'
'<div class="g">' + cells + '</div>')
print(f'\n{len(shot_names)} shots → docs/shots/v2_tour/ · contact: docs/shots/v2_contact.html')
if errs:
print(f'{len(errs)} console error(s); first: {errs[0][:120]}')
if __name__ == '__main__':
main()