PROCITY/tools/qa/gig_shot.py
m3ultra 146b5f3d2d Lane F R15 (v3.0): retire the R12 single-venue alias (ledger #2)
The compat alias delegating to the primary venue is gone — every read is now keyed by venueShopId.
B swept the cross-lane readers in R14; this migrates F's own tools + the spec, one atomic commit.

- gig_state.js: delete the alpha-shape alias block + the now-unused primaryId/P; update() just ticks all latches
- flags_check.py + gig_shot.py: .venueShopId->venueShopIds[0], .state->stateOf(id), .cover->coverOf(id), .bandName->bandNameOf(id)
- CITY_SPEC \S v3: delete the two alias sentences, add the omitted nightOf(id) to the accessor list

selfcheck 14264/14264 green (base 0x3fa36874, gig 0x4f4a549d); flags_off + smoke_gigs green, 0 warn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 11:14:19 +10:00

141 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""PROCITY Lane F — v3.0-beta THE DISTRICT acceptance shots (round 13).
The two money shots the round asks for, both reproducible (never a hand-cropped screenshot nobody can
re-take when the gig layer changes):
1. INTERIOR — a gig ON: the 4-piece band (front-line trio + a drummer on the riser) on Lane C's deck
with Lane E's real instruments in hand, a mixed standing/dancing crowd at the watch points,
everything human-sized next to the door / bar / ceiling.
2. STREET (venue_night) — the lit venue frontage at night: marquee + bulbs + window spill + streetlamp
pools on the footpath, Lane D's outdoor QUEUE at the door, town-wide posters on the poles.
Run: tools/.venv/bin/python tools/qa/gig_shot.py [--seed N] [--outdir DIR]
Needs the Playwright venv + Lane B's ?dbg=1 hook (incl. the venue_night bookmark). Writes full-frame
1280x720 PNGs to docs/shots/laneF/.
"""
import sys, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = 8130
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
OUTDIR = ROOT / 'docs' / 'shots' / 'laneF'
if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1])
if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1])
def port_up(port):
with socket.socket() as s:
s.settimeout(0.4)
return s.connect_ex(('127.0.0.1', port)) == 0
def ensure_server():
if port_up(PORT):
return None
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50):
if port_up(PORT): return proc
time.sleep(0.1)
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
# ── Shot 1: INTERIOR. Stand back by the door at eye height and look at the stage over the crowd's heads.
# Forcing one explicit render (rather than trusting rAF) keeps the interior shot deterministic in headless.
INTERIOR_JS = r"""
async () => {
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — read the primary venue by id
D.setSegment(5); // NIGHT — the gig is on
D.enterShop(id);
await new Promise(r => setTimeout(r, 900)); // room build + crew spawn + rig clips settle
const room = P.interiorMode.current;
if (!room) return { ok: false, why: 'not in the venue', state: P.gigs.stateOf(id) };
const st = room.stage;
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2);
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
P.renderer.info.reset();
P.renderer.render(P.interiorMode.scene, P.camera);
const crew = P.interiorMode.crew, mem = crew ? crew.members : [];
const band = mem.filter(m => m.part === 'band');
return { ok: true, venue: (P.plan.shops.find(s => s.id === id) || {}).name,
band: P.gigs.bandNameOf(id), state: P.gigs.stateOf(id), cover: P.gigs.coverOf(id), crew: P.interiorMode.crewInfo,
roles: band.map(m => m.role),
dancers: mem.filter(m => m.part === 'crowd' && m.dance).length,
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
}
"""
# ── Shot 2: STREET (venue_night). Lane B's bookmark poses out in front of the lit pub frontage at NIGHT.
# We exit to the street first, then let the animate loop run several frames at the pose so F's queue spawns
# (it is driven from the street frame), the frontage ramps up, and the venue block streams in — the last
# natural full-frame render is what the screenshot captures.
STREET_JS = r"""
async () => {
const P = window.PROCITY, D = window.DBG;
if (P.mode === 'interior') { D.exitShop(); await new Promise(r => setTimeout(r, 350)); }
D.shot('venue_night'); // B's bookmark: seg 5 + pose at the pub frontage
await new Promise(r => setTimeout(r, 1600)); // frontage ramp + queue spawn + chunk/poster stream settle
const id = P.gigs.venueShopIds[0];
return { ok: P.mode === 'street', venue: (P.plan.shops.find(s => s.id === id) || {}).name,
state: P.gigs.stateOf(id),
queue: P.queueCountOf ? P.queueCountOf(id) : null,
posters: (P.plan.posters || []).length, venues: (P.gigs.venueShopIds || []).length,
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
}
"""
def main():
try:
from playwright.sync_api import sync_playwright
except ImportError:
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
srv = ensure_server()
OUTDIR.mkdir(parents=True, exist_ok=True)
interior_out = OUTDIR / 'r13_v3beta_gig_interior.png'
street_out = OUTDIR / 'r13_v3beta_venue_night.png'
try:
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = []
pg.on('pageerror', lambda e: errs.append(str(e)))
pg.goto(f'{HOST}/index.html?seed={SEED}&gigs=1&dbg=1')
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
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=15000)
except Exception:
print(' ! fleet never readied — shooting the placeholder crew')
# Shot 1 — interior
info = pg.evaluate(INTERIOR_JS)
if not info.get('ok'):
b.close(); sys.exit(f'\033[31m✗ could not stage the interior shot: {info}\033[0m')
pg.screenshot(path=str(interior_out))
print(f"\033[32m● SHOT\033[0m {interior_out.relative_to(ROOT)}")
print(f" interior · {info['venue']} · {info['band']} · state={info['state']} · ${info['cover']} cover")
print(f" band {info['crew']['band']} (roles {info['roles']}) · crowd {info['crew']['crowd']} "
f"({info['dancers']} dancing) · {info['draws']} draws · {info['tris']:,} tris")
# Shot 2 — street venue_night
st = pg.evaluate(STREET_JS)
pg.screenshot(path=str(street_out))
print(f"\033[32m● SHOT\033[0m {street_out.relative_to(ROOT)}")
print(f" street · {st['venue']} · state={st['state']} · queue={st['queue']} · "
f"{st['posters']} posters across {st['venues']} venues · {st['draws']} draws · {st['tris']:,} tris")
b.close()
finally:
if srv: srv.terminate()
if errs:
print(f" ! {len(errs)} page error(s): {errs[0][:120]}")
sys.exit(1)
if __name__ == '__main__':
main()