#!/usr/bin/env python3 """PROCITY Lane F — R42 §42.6 THE PROOF SHOTS. This round is judged by eye, so these ARE the round. tools/.venv/bin/python tools/qa/r42_tour.py [--only NAME[,NAME…]] [--outdir DIR] R42 is a LOOK round: three faults were visible in R41's own committed proof shots, and the deliverable is the same frames taken again on the integrated tree, **paired against those committed originals**. The R41 files in `docs/shots/laneF_r41/` are the BEFORE — they are in git, nobody can re-shoot them, and that is exactly what makes them evidence. pub_furnished R41's hero. The before holds TWO purple `EL CHUPACABRA` luchadors and an UNCLOTHED body at the stage edge, in a room of white plastic-looking props. Same recipe, same seed, same room, same segment. → pub_furnished_ab.jpg record_dj_booth R41's booth: "a plain black box". Same recipe. → record_dj_booth_ab.jpg record_dj_booth_door …and the frame R41 never took: the booth from where a player actually stands, at Lane C's own `room.spawn` — the door. A composition claim is a claim about the entry view, so it has to be shot from the entry. street_postures R41's street, from R41's OWN published camera — parsed out of its committed sidecar, because re-running R41's live-crowd cluster search lands somewhere different every time. → street_postures_ab.jpg street_bench the bench claim on its own, shot SIDE-ON (a person facing the camera looks the same whichever way the bench under them is turned), with the yaw MEASURED in the frame from the plan's road geometry and the scene's instance matrices — not read back off B's placement rule. credits_panel the F2 surface with the ped rows filled: ruling 3 on screen, no `unverified`. TWO LINES ON EVERY HUMANOID FRAME · the R10 human-sized line (stature feet→crown off the POSED skeleton, as a ratio of that figure's own nominal height — a giant >2.0 m or a fold <55% fails). · the CAST line (Lane D's §42.4 idea, adopted): every figure resolved back to the ped GLB it is wearing via `fleet.all[pedIndex].pedName`, with an explicit assertion that none of the five retired bodies is in the picture. "The luchador is gone" is a claim about the cast, and a JPEG cannot carry it alone. Fresh headless context per frame, own no-store server, seed 20261990, 1280x720. Exit 0 = every frame captured, every human-sized line clean, and no retired body in any cast. """ import sys, os, re, json, time, socket, subprocess, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent.parent PORT = int(os.environ.get('PROCITY_R42_TOUR_PORT', '8984')) HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'laneF_r42' BEFORE = ROOT / 'docs' / 'shots' / 'laneF_r41' # the committed R41 originals — the BEFORE arm LO, HI = 1.4, 2.0 if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1]) ONLY = None if '--only' in sys.argv: ONLY = set(sys.argv[sys.argv.index('--only') + 1].split(',')) # Lane E §42.3 / Lane D §42.4 — the five bodies that must never be in a shipped frame again. RETIRED = ['comical_luchador_01', 'comical_boy_01', 'man_elder_01', 'man_soldier_ww2_01', 'dj_phrtt_01'] fails = [] def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}") def OK(m): print(f" \033[32m✓\033[0m {m}") def head(m): print(f"\n\033[1m{m}\033[0m") def note(m): print(f" \033[33m·\033[0m {m}") NOSTORE = r''' import sys, http.server, functools class H(http.server.SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate') super().end_headers() def log_message(self, *a): pass http.server.HTTPServer(('127.0.0.1', int(sys.argv[1])), functools.partial(H, directory=sys.argv[2])).serve_forever() ''' # ── the street: R41's own cluster search, plus the CAST term ────────────────────────────────────── # Unchanged in method from r41_tour.py on purpose — a before/after pair whose AFTER was framed by a # different algorithm is not a pair. The one addition is `ped`: the frame this round is about WHO is # in it, so the scorer also rewards distinct BODIES, and the sidecar can name them. STREET_LOOK = r""" () => { const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor; const cam = P.camera; const names = (P.fleet.all || []).map(r => r.pedName); const people = []; for (const c of C.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const posedNow = c.loiter > 0; people.push({ id: c.id, x: c.x, z: c.z, ped: names[c.pedIndex] || '?', state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk') }); } const anchors = people.filter(p => p.state !== 'walk'); if (!anchors.length) return { ok: false, why: 'no non-walking rig on the street yet', n: people.length }; const targets = anchors.map(a => ({ x: a.x, z: a.z, state: a.state })); for (let i = 0; i < anchors.length; i++) for (let j = i + 1; j < anchors.length; j++) if (anchors[i].state !== anchors[j].state && Math.hypot(anchors[i].x - anchors[j].x, anchors[i].z - anchors[j].z) < 34) targets.push({ x: (anchors[i].x + anchors[j].x) / 2, z: (anchors[i].z + anchors[j].z) / 2, state: anchors[i].state + '+' + anchors[j].state }); let best = null; for (const a of targets) { for (let i = 0; i < 12; i++) { const th = i * Math.PI / 6; for (const r of [7, 9.5, 12, 15, 18]) { const px = a.x + Math.cos(th) * r, pz = a.z + Math.sin(th) * r; cam.position.set(px, 1.62, pz); cam.lookAt(new V3(a.x, 1.25, a.z)); cam.updateMatrixWorld(true); const v = new V3(); const seen = []; for (const p of people) { const d = Math.hypot(p.x - px, p.z - pz); if (d < 2.4 || d > 26) continue; v.set(p.x, 1.0, p.z).project(cam); if (v.z > 1 || Math.abs(v.x) > 0.9 || Math.abs(v.y) > 0.9) continue; seen.push(p); } const states = new Set(seen.map(s => s.state)); const bodies = new Set(seen.map(s => s.ped)); const score = states.size * 100 + bodies.size * 40 + seen.length * 10 - r * 4; if (!best || score > best.score) best = { score, px, pz, ax: a.x, az: a.z, states: [...states], bodies: [...bodies], inFrame: seen.length, anchor: a.state }; } } } cam.position.set(best.px, 1.62, best.pz); cam.lookAt(new V3(best.ax, 1.25, best.az)); cam.updateMatrixWorld(true); // MEASURE IN THE SAME BEAT — searching in one evaluate and measuring in the next lets the dwell // clock run between them, and a bench somebody has just got up from is the frame you shoot. const v2 = new V3(); const rows = []; for (const c of C.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z); if (d < 2.2 || d > 26) continue; v2.set(c.x, 1.0, c.z).project(cam); if (v2.z > 1 || Math.abs(v2.x) > 0.92 || Math.abs(v2.y) > 0.92) continue; let lo = 1e9, hi = -1e9; c.actor.inner.updateWorldMatrix(true, true); c.actor.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } }); const posedNow = c.loiter > 0; // the clip is read off the MIXER, not off `c.posture` — posture is the ASSIGNMENT and exists on a // ?clips=0 boot that has fetched nothing (R41's lesson, kept). const playing = ((c.actor.mixer && c.actor.mixer._actions) || []) .filter(x => x.getEffectiveWeight() > 0.5).map(x => x.getClip().name); rows.push({ id: c.id, d: +d.toFixed(1), ped: names[c.pedIndex] || '?', state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'), clip: playing[0] || null, h: +c.height.toFixed(3), stature: +(hi - lo).toFixed(3) }); } rows.sort((a, b) => a.d - b.d); const states = [...new Set(rows.map(r => r.state))].sort(); const clips = [...new Set(rows.map(r => r.clip).filter(Boolean))].sort(); const bodies = [...new Set(rows.map(r => r.ped))].sort(); // …and a BENCH-SIT BONUS, stated rather than hidden: R41's committed `street_postures.jpg` is a // bench-sit frame, and a pair whose halves show different states is not a pair. The bonus biases // the AFTER toward the comparable frame; it does not manufacture one, because a bench-sit anchor // only exists if a citizen is actually sitting on a Lane B bench in this town at this moment. const benchBonus = rows.some(r => r.state === 'bench-sit') ? 150 : 0; return { ok: true, ...best, rows, states, clips, bodies, inFrame: rows.length, benchBonus, score2: states.length * 100 + bodies.length * 50 + clips.length * 40 + rows.length * 10 + benchBonus, info: window.DBG.info(), clipStats: C.clipStats() }; } """ # ── THE BENCH, measured in the frame and NOT from Lane B's rule ─────────────────────────────────── # `benchStops(plan)` is used for POSITIONS ONLY — to say "this instance is a bench" — which is the # same non-circularity B used. The yaw verdict is then computed from two things B's placer never # touches: the PLAN's road centreline (Lane A's graph) and the instance's own world matrix. BENCH_MEASURE = r""" async () => { const P = window.PROCITY, V3 = P.scene.position.constructor; const THREE = await import('three'); const F = await import('./js/world/furniture.js'); const stops = F.benchStops(P.plan); // road geometry, straight from Lane A's graph — not from the furniture module const nodes = new Map(P.plan.streets.nodes.map(n => [n.id, n])); const edges = P.plan.streets.edges.map(e => { const a = nodes.get(e.a), b = nodes.get(e.b); return { id: e.id, ax: a.x, az: a.z, bx: b.x, bz: b.z, kind: e.kind, width: e.width }; }); const byId = new Map(edges.map(e => [e.id, e])); const foot = (e, x, z) => { const dx = e.bx - e.ax, dz = e.bz - e.az, L2 = dx * dx + dz * dz || 1, L = Math.sqrt(L2); let t = ((x - e.ax) * dx + (z - e.az) * dz) / L2; t = Math.max(0, Math.min(1, t)); const cx = e.ax + dx * t, cz = e.az + dz * t; return { d: Math.hypot(x - cx, z - cz), cx, cz, ux: dx / L, uz: dz / L, e }; }; // THE EDGE A BENCH BELONGS TO IS THE ONE ITS STATION NAMES, not the geometrically nearest one. // Measured the hard way: nearest-edge attribution reported one bench at 174.4 degrees off "the // road" — a bench standing near a corner is often closer to the CROSS street's centreline than to // its own, so it correctly faces its own street and the instrument calls that a 180-degree error. // `edgeId` is metadata of the station this instance was matched to BY POSITION; the yaw verdict is // still computed from Lane A's road geometry and the instance's own matrix, so it stays // non-circular. Both readings are returned so the disagreement is visible rather than tuned away. const nearestEdgePoint = (x, z) => { let best = null; for (const e of edges) { const f = foot(e, x, z); if (!best || f.d < best.d) best = f; } return best; }; const cam = P.camera; cam.updateMatrixWorld(true); const m = new THREE.Matrix4(), pos = new V3(), q = new THREE.Quaternion(), sc = new V3(); const fwd = new V3(), v = new V3(); const rows = []; P.scene.traverse(o => { if (!o.isInstancedMesh) return; o.updateWorldMatrix(true, false); for (let i = 0; i < o.count; i++) { o.getMatrixAt(i, m); m.premultiply(o.matrixWorld); m.decompose(pos, q, sc); // POSITION-ONLY identification: is there a bench station under this instance? let hit = null; for (const s of stops) { const d = Math.hypot(s.x - pos.x, s.z - pos.z); if (d < 0.02) { hit = s; break; } } if (!hit) continue; fwd.set(0, 0, 1).applyQuaternion(q).normalize(); // the bench FRONT in world space const own = byId.has(hit.edgeId) ? foot(byId.get(hit.edgeId), pos.x, pos.z) : nearestEdgePoint(pos.x, pos.z); const ne = nearestEdgePoint(pos.x, pos.z); const ang = (f) => { const toRoad = new V3(f.cx - pos.x, 0, f.cz - pos.z).normalize(); return Math.acos(Math.max(-1, Math.min(1, fwd.dot(toRoad)))) * 180 / Math.PI; }; const along = new V3(own.ux, 0, own.uz); const degTo = ang(own), degNear = ang(ne); const degAlong = Math.acos(Math.max(-1, Math.min(1, Math.abs(fwd.dot(along))))) * 180 / Math.PI; v.copy(pos); v.y = 1.0; v.project(cam); rows.push({ x: +pos.x.toFixed(2), z: +pos.z.toFixed(2), edgeId: hit.edgeId, side: hit.side, offset: +own.d.toFixed(2), nearOffset: +ne.d.toFixed(2), nearIsOwn: ne.e.id === hit.edgeId, toRoadDeg: +degTo.toFixed(1), nearDeg: +degNear.toFixed(1), alongDeg: +degAlong.toFixed(1), camDist: +Math.hypot(pos.x - cam.position.x, pos.z - cam.position.z).toFixed(1), inFrame: !(v.z > 1 || Math.abs(v.x) > 0.95 || Math.abs(v.y) > 0.95) }); } }); // THE CONTROL, in the same evaluate: shift every station 3 m and re-identify. "coincides" has to // be a measurement, or the 2 cm match above is just "there was an instanced mesh somewhere". let ctrl = 0; P.scene.traverse(o => { if (!o.isInstancedMesh) return; o.updateWorldMatrix(true, false); for (let i = 0; i < o.count; i++) { o.getMatrixAt(i, m); m.premultiply(o.matrixWorld); m.decompose(pos, q, sc); for (const s of stops) if (Math.hypot(s.x + 3 - pos.x, s.z + 3 - pos.z) < 0.02) { ctrl++; break; } } }); // …and choose the camera for the DEDICATED bench frame while the geometry is still in hand. // A SIDE-ON view is the one that reads. Standing on the bench's own front line (which is what the // street cluster search happens to pick) shows a person facing the camera, and a person facing the // camera looks identical whichever way the bench underneath them is turned. In profile, with the // road on one side and the shopfront on the other, "the sitter faces the road" is visible. let pick = null; for (const c of P.citizens.activeCitizens()) { if (!c.bench || !c.actor || c.actorKind !== 'rig') continue; for (const r of rows) { const d = Math.hypot(r.x - c.x, r.z - c.z); if (d < 1.2 && (!pick || d < pick.d)) pick = { d, r }; } } let shot = null; if (pick) { const e = byId.get(pick.r.edgeId); const f = e ? foot(e, pick.r.x, pick.r.z) : null; const toRoad = f ? new V3(f.cx - pick.r.x, 0, f.cz - pick.r.z).normalize() : new V3(0, 0, 1); const side = new V3(-toRoad.z, 0, toRoad.x); // 90 deg to the bench front shot = { px: +(pick.r.x + side.x * 5.0 + toRoad.x * 2.4).toFixed(3), pz: +(pick.r.z + side.z * 5.0 + toRoad.z * 2.4).toFixed(3), ax: +(pick.r.x + toRoad.x * 0.3).toFixed(3), az: +(pick.r.z + toRoad.z * 0.3).toFixed(3), bench: pick.r, sitterDist: +pick.d.toFixed(2) }; } return { stations: stops.length, matched: rows.length, control: ctrl, rows, shot }; } """ # Place the camera the BENCH_MEASURE pick chose, then measure the figures that land in it — in the # same beat, for the same reason the street search does: a bench somebody has just got up from is the # frame you actually shoot otherwise. POSE = r""" ({ px, pz, ax, az, eye, ly }) => { const P = window.PROCITY, V3 = P.scene.position.constructor; P.player.teleport(px, pz, 0); P.camera.position.set(px, eye == null ? 1.55 : eye, pz); P.camera.lookAt(new V3(ax, ly == null ? 1.05 : ly, az)); P.chunks.warmup(P.camera.position); P.camera.updateMatrixWorld(true); const names = (P.fleet.all || []).map(r => r.pedName); const cam = P.camera, v = new V3(), rows = []; for (const c of P.citizens.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z); if (d < 1.0 || d > 26) continue; v.set(c.x, 1.0, c.z).project(cam); if (v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) continue; let lo = 1e9, hi = -1e9; c.actor.inner.updateWorldMatrix(true, true); c.actor.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } }); const posedNow = c.loiter > 0; rows.push({ id: c.id, d: +d.toFixed(1), ped: names[c.pedIndex] || '?', state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow ? 'idle' : 'walk'), clip: ((c.actor.mixer && c.actor.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5) .map(x => x.getClip().name)[0] || null, h: +c.height.toFixed(3), stature: +(hi - lo).toFixed(3) }); } rows.sort((a, b) => a.d - b.d); return { rows, info: window.DBG.info(), sitters: rows.filter(r => r.state === 'bench-sit').length }; } """ # ── interiors ──────────────────────────────────────────────────────────────────────────────────── # Same as R41's INT_FRAME, plus a `from: 'door'` arm: stand at Lane C's own `room.spawn` — the point # the player is put down on when the door opens — and look at the target from there. R41 shot the # booth from 2.5 m at eye 1.45 (a product photo); the fault John reported was about the READ from # where you walk in, so the after has to answer that question in the frame that asks it. INT_FRAME = r""" async ({ target, back, eye, from }) => { const P = window.PROCITY, V3 = P.scene.position.constructor; const room = P.interiorMode.current; if (!room) return { ok: false, why: 'no room open' }; const sp = room.spawn || { x: 0, z: 0 }; let tx, tz, ty = 1.15; if (target === 'people') { const figs = ((P.interiorMode.keepers || {}).keepers || []).map(k => k.actor.fig); if (!figs.length) return { ok: false, why: 'no figures in the room' }; tx = 0; tz = 0; for (const f of figs) { tx += f.position.x; tz += f.position.z; } tx /= figs.length; tz /= figs.length; } else if (target === 'centre') { tx = 0; tz = -room.dims.D * 0.12; ty = 1.05; } else { let hit = null; room.group.traverse(o => { if (hit) return; const u = o.userData || {}; if (target === 'djBooth' && u.djBooth) hit = o; }); if (!hit) return { ok: false, why: `no ${target} in this room` }; const THREE = await import('three'); const c = new THREE.Box3().setFromObject(hit).getCenter(new V3()); tx = c.x; tz = c.z; ty = Math.max(0.95, c.y); } let px, pz; if (from === 'door') { px = sp.x; pz = sp.z; } // the player's real view else { let dx = sp.x - tx, dz = sp.z - tz; const L = Math.hypot(dx, dz) || 1; dx /= L; dz /= L; const d = Math.min(Math.max(back, 1.9), Math.max(L, back)); px = tx + dx * d; pz = tz + dz * d; } P.camera.position.set(px, eye, pz); P.camera.lookAt(new V3(tx, ty, tz)); P.camera.updateMatrixWorld(true); P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); return { ok: true, cam: [+px.toFixed(2), +pz.toFixed(2)], target: [+tx.toFixed(2), +tz.toFixed(2)], spawn: [+sp.x.toFixed(2), +sp.z.toFixed(2)], standoff: +Math.hypot(px - tx, pz - tz).toFixed(2), draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles, shop: P.interiorMode.shop, dims: room.dims }; } """ # Lane D's §42.4 measure, adopted whole: the gig crew AND the keepers, each resolved to its body. ROOM_MEASURE = r""" () => { const P = window.PROCITY, V3 = P.scene.position.constructor; const names = (P.fleet.all || []).map(r => r.pedName); const cam = P.camera; cam.updateMatrixWorld(true); const v = new V3(); const rows = []; const add = (a, kind, role, pedIndex) => { if (!a || !a.inner) { rows.push({ state: kind, role, ped: 'placeholder', h: 0, stature: 0, inFrame: false }); return; } let lo = 1e9, hi = -1e9; a.inner.updateWorldMatrix(true, true); a.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } }); v.set(a.fig.position.x, 1.0, a.fig.position.z).project(cam); rows.push({ state: kind, role, ped: (pedIndex != null && names[pedIndex]) || '?', h: +(a.height || a.nominalHeight || 0).toFixed(3), stature: +(hi - lo).toFixed(3), clip: ((a.mixer && a.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5) .map(x => x.getClip().name)[0] || null, inFrame: !(v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) }); }; const crew = P.interiorMode.crew; if (crew) for (const m of crew.members) add(m.actor, m.part === 'band' ? 'band' : 'crowd', m.role, m.pedIndex); for (const k of ((P.interiorMode.keepers || {}).keepers || [])) add(k.actor, k.browse ? 'browser' : 'keeper', k.type, k.pedIndex); return { rows, info: window.DBG.info() }; } """ def port_up(port): with socket.socket() as s: s.settimeout(0.4); return s.connect_ex(('127.0.0.1', port)) == 0 def serve(): p = subprocess.Popen([sys.executable, '-c', NOSTORE, str(PORT), str(ROOT / 'web')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(80): if port_up(PORT): return p time.sleep(0.1) p.terminate(); raise SystemExit(f'could not serve on :{PORT}') def human_line(rows): """The R10 line — height-RELATIVE (D's R41 finding: a 1.32 m wall-leaner is not a defect, a 2.4 m anything is, and a body folded to hip height is the R16 failure this exists to catch).""" out, bad = [], [] for r in rows: if r.get('ped') == 'placeholder': continue h = r.get('h') or 0 ratio = (r['stature'] / h) if h else None who = r.get('id', r.get('state')) if r['stature'] > HI: bad.append(f'{who} GIANT {r["stature"]}m') elif h and r['stature'] < 0.55 * h: bad.append(f'{who} FOLDED {r["stature"]}m of {h}m') elif h and not (LO <= h <= HI): bad.append(f'{who} nominal {h}m out of [{LO},{HI}]') out.append(f"{r['state']}" + (f"[{r.get('clip') or r.get('ped')}]" if (r.get('clip') or r.get('ped')) else '') + f" {r['stature']}m" + (f" ({ratio:.0%} of its {h}m nominal)" if ratio else '')) verdict = ('ALL HUMAN-SIZED — no giant (>2.0 m), no fold (<55% of nominal)' if not bad else 'FAIL: ' + ', '.join(bad)) return (f"HUMAN-SIZED LINE (R10) — {len(out)} figure(s), stature (feet→crown) measured off the " f"posed skeleton: {' · '.join(out)}. {verdict}."), not bad def cast_line(rows, label): """Lane D's §42.4 idea, adopted. A JPEG cannot say 'the luchador is gone'; this can.""" peds = [r['ped'] for r in rows if r.get('ped') and r['ped'] != 'placeholder'] seen = {} for p in peds: seen[p] = seen.get(p, 0) + 1 banned = sorted(set(peds) & set(RETIRED)) return (f"CAST ({label}) — {len(peds)} figure(s) wearing {len(seen)} distinct bodies: " + ', '.join(f'{k}x{v}' for k, v in sorted(seen.items())) + '. ' + ('PERIOD LAW (ruling 1): none of the five retired bodies ' f'({", ".join(RETIRED)}) is in this frame.' if not banned else f'*** RETIRED BODY IN FRAME: {banned} ***')), not banned def write_shot(pg, name, caption, rows=None, cast_label=None, capture=True): OUTDIR.mkdir(parents=True, exist_ok=True) if capture: pg.screenshot(path=str(OUTDIR / f'{name}.jpg'), type='jpeg', quality=92) txt = caption if rows is not None: line, ok = human_line(rows) txt += '\n\n' + line if not ok: FAIL(f'{name}: {line}') cl, cok = cast_line(rows, cast_label or name) txt += '\n\n' + cl if not cok: FAIL(f'{name}: {cl}') (OUTDIR / f'{name}.txt').write_text(txt + '\n') OK(f'{name}.jpg + .txt') def pair(name, before_name, title, before_cap, after_cap): """Composite the committed R41 original beside today's frame. `pipeline/montage.py` is Lane E's tool and this is exactly what it was written for (§42 notes) — no second copy of it here.""" b = BEFORE / f'{before_name}.jpg' a = OUTDIR / f'{name}.jpg' if not b.exists(): FAIL(f'{name}_ab: the committed R41 before is missing at {b}'); return if not a.exists(): FAIL(f'{name}_ab: no after frame to pair'); return out = OUTDIR / f'{name}_ab.jpg' r = subprocess.run([sys.executable, str(ROOT / 'pipeline' / 'montage.py'), str(out), '--cols', '2', '--width', '640', '--title', title, f'{b}:{before_cap}', f'{a}:{after_cap}'], capture_output=True, text=True) if r.returncode or not out.exists(): FAIL(f'{name}_ab: montage failed — {r.stdout.strip()} {r.stderr.strip()[:200]}') else: OK(f'{name}_ab.jpg (R41 committed original | R42 same recipe)') def new_page(p): b = p.chromium.launch() pg = b.new_page(viewport={'width': 1280, 'height': 720}) errs = [] pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None) pg.on('pageerror', lambda e: errs.append(str(e))) return b, pg, errs def boot(pg, query): pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else '')) pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=45000) pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") pg.wait_for_function('() => window.PROCITY.fleet && window.PROCITY.fleet.ready', timeout=25000) def enter_best(pg, want_type=None, need=None, tries=10): js = r""" async ({ want, need, tries }) => { const P = window.PROCITY, D = window.DBG, C = P.citizens; const PREF = ['record', 'opshop', 'book', 'video', 'stall', 'pawn']; const shops = (P.plan.shops || []).filter(s => !want || s.type === want) .map(s => ({ id: s.id, name: s.name, type: s.type, occ: (C.occupancyOf(s.id) || {}).count || 0 })) .sort((a, b) => (b.occ - a.occ) || ((PREF.indexOf(a.type) + 9) % 9) - ((PREF.indexOf(b.type) + 9) % 9)); const has = (room) => { if (!room) return false; if (need === 'djBooth') { let f = false; room.group.traverse(o => { if ((o.userData || {}).djBooth) f = true; }); return f; } return true; }; let best = null; for (const s of shops.slice(0, tries)) { D.enterShop(s.id); await new Promise(r => setTimeout(r, 2600)); const room = P.interiorMode.current; const ks = ((P.interiorMode.keepers || {}).keepers) || []; if (room && has(room)) { best = { ok: true, shop: s, tried: shops.length, keepers: ks.length, fittings: (room.placement || []).length }; return best; } D.exitShop(); await new Promise(r => setTimeout(r, 350)); } return { ok: false, why: `no ${want || 'shop'} satisfied "${need}"`, tried: shops.length }; } """ return pg.evaluate(js, dict(want=want_type, need=need, tries=tries)) # ── 1. THE PUB ──────────────────────────────────────────────────────────────────────────────────── def shot_pub(p): head('1. pub_furnished — R41\'s hero, re-shot: dressed props, and a cast with no luchador and no nude') b, pg, errs = new_page(p) try: boot(pg, 'gigs=1&weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(5)") pg.wait_for_timeout(1500) r = enter_best(pg, 'pub', None) if not r.get('ok'): FAIL(f"pub_furnished: {r.get('why')}"); return pg.wait_for_timeout(1800) # let the gig crew resolve its bodies before the cast is read fr = pg.evaluate(INT_FRAME, {'target': 'centre', 'back': 5.2, 'eye': 1.62, 'from': 'standoff'}) if not fr.get('ok'): FAIL(f"pub_furnished: {fr.get('why')}"); return m = pg.evaluate(ROOM_MEASURE) inframe = [x for x in m['rows'] if x.get('inFrame')] note(f"{r['shop']['name']} · {r['fittings']} fittings · {fr['draws']} draws / {fr['tris']} tris · " f"{len(m['rows'])} figures ({len(inframe)} in frame)") cap = (f"PROCITY R42 §42.6 — pub_furnished (AFTER)\n" f"seed {SEED}, {r['shop']['name']} (pub), segment 5, ?gigs=1 — the SAME recipe as " f"docs/shots/laneF_r41/pub_furnished.jpg (Lane C's room.spawn line, back 5.2 m, eye 1.62 m), " f"so the pair differs only by what landed this round.\n" f"WHAT CHANGED: Lane E §42.1 repainted 46 props in place via a COLOR_0 rewrite (draw delta " f"+0, tri delta +0, every POSITION/NORMAL/index accessor sha1-identical to R41) — the white " f"blobs are brown vinyl and beer-carpet red because they always carried vertex colour and it " f"was bleached to 0.02 saturation, not missing. Lane E §42.3 + Lane D §42.4 swapped five " f"bodies at their own index: the purple EL CHUPACABRA luchador and the UNCLOTHED dj_phrtt_01 " f"that stood at the stage edge in the before are both gone from the fleet.\n" f"{r['fittings']} fittings · {fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris) " f"· camera {fr['cam']} → {fr['target']}.") write_shot(pg, 'pub_furnished', cap, m['rows'], cast_label='the whole room') if errs: FAIL(f'pub_furnished: {len(errs)} console error(s): {errs[:2]}') finally: b.close() pair('pub_furnished', 'pub_furnished', 'PROCITY R42 §42.6 — THE PUB: R41 as committed, and the same frame today', 'BEFORE (R41, committed): white plastic-looking props; TWO purple EL CHUPACABRA luchadors; ' 'an unclothed body at the stage edge.', 'AFTER (R42): the props are painted (E §42.1, +0 draws), and every body in the room is a ' 'person who could stand on a 1996 Sydney street (E §42.3 + D §42.4).') # ── 2. THE BOOTH ────────────────────────────────────────────────────────────────────────────────── def shot_booth(p): head('2. record_dj_booth — R41\'s recipe re-shot, AND the frame R41 never took (from the door)') b, pg, errs = new_page(p) try: boot(pg, 'weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(2)") pg.wait_for_timeout(1200) r = enter_best(pg, 'record', 'djBooth') if not r.get('ok'): FAIL(f"record_dj_booth: {r.get('why')}"); return # a) the PAIR arm — R41's exact camera recipe, so the before/after is a pair fr = pg.evaluate(INT_FRAME, {'target': 'djBooth', 'back': 2.5, 'eye': 1.45, 'from': 'standoff'}) if not fr.get('ok'): FAIL(f"record_dj_booth: {fr.get('why')}"); return m = pg.evaluate(ROOM_MEASURE) note(f"pair arm: {r['shop']['name']} · {fr['draws']} draws / {fr['tris']} tris · " f"camera {fr['cam']} → {fr['target']} (standoff {fr['standoff']} m)") write_shot(pg, 'record_dj_booth', f"PROCITY R42 §42.6 — record_dj_booth (AFTER, R41's own camera recipe)\n" f"seed {SEED}, {r['shop']['name']} (record), midday. Camera derived exactly as in R41: " f"2.5 m off the booth group's bounding-box centre along Lane C's room.spawn line, " f"eye 1.45 m — so this is a PAIR with docs/shots/laneF_r41/record_dj_booth.jpg.\n" f"WHAT CHANGED: Lane E §42.2a repainted the deck chassis Technics silver (#b9bdc2) — " f"contrast against the booth bench went 1.21:1 → 6.74:1 — and Lane C §42.2b composed the " f"booth (bench 3 → 7 boxes, a record bay, headphones, and the booth turned to face the " f"door: entry-line alignment 49% → 92%). Near-black pixels inside the booth's own screen " f"rect: 57.5% → 26.7%.\n" f"{fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris) · camera {fr['cam']} " f"→ {fr['target']}.", m['rows'] if m['rows'] else None, cast_label='the record shop') # b) the DOOR arm — the frame the fault was actually reported about fr2 = pg.evaluate(INT_FRAME, {'target': 'djBooth', 'back': 0, 'eye': 1.62, 'from': 'door'}) if not fr2.get('ok'): FAIL(f"record_dj_booth_door: {fr2.get('why')}"); return m2 = pg.evaluate(ROOM_MEASURE) note(f"door arm: spawn {fr2['spawn']} → booth {fr2['target']} ({fr2['standoff']} m) · " f"{fr2['draws']} draws / {fr2['tris']} tris") write_shot(pg, 'record_dj_booth_door', f"PROCITY R42 §42.6 — record_dj_booth_door (THE PLAYER'S REAL VIEW)\n" f"seed {SEED}, {r['shop']['name']} (record), midday. The camera stands at Lane C's own " f"`room.spawn` — the exact spot the player is put down on when the door opens — at eye " f"height 1.62 m, looking at the booth: {fr2['standoff']} m away, spawn {fr2['spawn']} → " f"booth {fr2['target']}.\n" f"R41 shot this booth from 2.5 m at eye 1.45 m, which is a product photograph. The fault " f"John reported — 'it reads as a plain black box' — is a claim about the ENTRY view, so " f"this is the frame that answers it. C §42.2b moved the booth to face this line " f"(alignment 49% → 92%).\n" f"{fr2['draws']} draws of the ≤350 interior law ({fr2['tris']} tris).", m2['rows'] if m2['rows'] else None, cast_label='the record shop') if errs: FAIL(f'record_dj_booth: {len(errs)} console error(s): {errs[:2]}') finally: b.close() pair('record_dj_booth', 'record_dj_booth', 'PROCITY R42 §42.6 — THE DJ BOOTH: R41 as committed, and the same camera today', 'BEFORE (R41, committed): the rigged 1200 and the TTM-54i are in there, and at normal ' 'viewing distance you cannot tell it is a turntable setup.', 'AFTER (R42): silver chassis (E §42.2a, 1.21:1 → 6.74:1) in a composed booth (C §42.2b) — ' 'near-black pixels in the booth crop 57.5% → 26.7%.') # ── 3. THE STREET ───────────────────────────────────────────────────────────────────────────────── def shot_street(p): head('3. street_postures — the recast crowd, and Lane B\'s benches now facing the road') b, pg, errs = new_page(p) try: boot(pg, 'weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(2)") pg.evaluate("() => window.DBG.shot('street_noon')") # ── THE PAIR FRAME: stand on the BEFORE's own published camera ────────────────────────────── # The first cut re-ran R41's cluster search and got a different spot each time, because the # search scores a LIVE crowd — one run landed within 40 cm of R41's camera and the next landed # 30 m away behind a wall. Re-rolling until it looks right is not a method. The pose is parsed # out of the committed R41 sidecar (a magic number I typed would be a different claim), so the # AFTER stands exactly where the BEFORE stood and the only variable in the pair is the tree. m41 = re.search(r'camera \(([-\d.]+), ([-\d.]+)\) looking at \(([-\d.]+), ([-\d.]+)\)', (BEFORE / 'street_postures.txt').read_text()) if not m41: FAIL('street_postures: could not parse the R41 camera out of its committed sidecar'); return px, pz, ax, az = (float(g) for g in m41.groups()) note(f'R41 published camera, parsed from its sidecar: ({px}, {pz}) → ({ax}, {az})') sp = pg.evaluate(POSE, dict(px=px, pz=pz, ax=ax, az=az, eye=1.62, ly=1.25)) pg.wait_for_timeout(1500) sp = pg.evaluate(POSE, dict(px=px, pz=pz, ax=ax, az=az, eye=1.62, ly=1.25)) OUTDIR.mkdir(parents=True, exist_ok=True) pg.screenshot(path=str(OUTDIR / 'street_postures.jpg'), type='jpeg', quality=92) states = sorted({r['state'] for r in sp['rows']}) clips = sorted({r['clip'] for r in sp['rows'] if r['clip']}) bodies = sorted({r['ped'] for r in sp['rows']}) cs = pg.evaluate("() => window.PROCITY.citizens.clipStats()") note(f"{len(sp['rows'])} rigs in frame · states {states} · clips {clips} · bodies {bodies} · " f"{sp['info']['drawCalls']} draws / {sp['info']['tris']} tris") write_shot(pg, 'street_postures', f"PROCITY R42 §42.6 — street_postures (AFTER)\n" f"seed {SEED}, midday (seg 2). Camera ({px}, {pz}) → ({ax}, {az}) — **R41's own published " f"pose**, parsed out of docs/shots/laneF_r41/street_postures.txt rather than re-derived. " f"R41 chose it with a live-crowd cluster search; re-running that search on a live crowd " f"lands somewhere different every time, so the AFTER stands where the BEFORE stood and the " f"only variable in the pair is the tree.\n" f"{len(sp['rows'])} rigs in frame · posture states {states} · {len(clips)} distinct clips " f"({', '.join(clips)}) · {len(bodies)} distinct bodies · {sp['info']['drawCalls']} draws of " f"the ≤300 street law ({sp['info']['tris']} tris) · clip bank {cs}.\n" f"R41 read 173 draws / 43,562 tris at this pose. The recast is draw-neutral by " f"construction (one primitive, one material per ped — Lane D §42.4); the 802-station pin " f"reads 0/0 and a single bookmark moves ±1 run to run.\n" f"NB the rows below count NEAR-TIER RIGS only (R41's convention, kept). A citizen far " f"enough out to be drawn as a mid-tier billboard has no skeleton and no posture to read, " f"so a seated figure visible in the picture can be absent from the list — that is what " f"street_bench.jpg is for.", sp['rows'], cast_label='the street', capture=False) # ── THE BENCH FRAME: found by polling, shot in the same beat ──────────────────────────────── STOPS = ['crossroads_busy', 'street_noon', 'crossroads_busy', 'patronage_door', 'night_crowd'] best, bench_shot = None, None for i in range(72): if i % 12 == 0: pg.evaluate(f"() => window.DBG.shot('{STOPS[(i // 12) % len(STOPS)]}')") pg.evaluate("() => window.DBG.setSegment(2)") pg.wait_for_timeout(900) r = pg.evaluate(STREET_LOOK) if not r.get('ok'): pg.wait_for_timeout(600); continue if not best or r['score2'] > best['score2']: best = r # THE BENCH FRAME, TAKEN IN THE SAME BEAT IT IS FOUND. The first cut computed the pose # here and shot it after the loop, and came back with 0 figures in frame: the dwell clock # runs, the sitter gets up, and you photograph an empty bench with a caption about a # sitter. Measure, pose, capture, re-measure — then move on. (This is the R41 tour's own # lesson applied one level down; it cost that harness two frames before it was learned.) if not bench_shot and r['anchor'].startswith('bench-sit'): bm = pg.evaluate(BENCH_MEASURE) if bm.get('shot'): bp = pg.evaluate(POSE, dict(bm['shot'], eye=1.55, ly=1.05)) pg.wait_for_timeout(350) if bp['sitters']: pg.screenshot(path=str(OUTDIR / 'street_bench.jpg'), type='jpeg', quality=92) bench_shot = dict(bm=bm, bp=bp, sh=bm['shot']) # whether or not it worked, the pose moved the player — put the search back where # it was so the next poll is not measuring a different street. pg.evaluate(f"() => window.DBG.shot('{STOPS[(i // 12) % len(STOPS)]}')") pg.evaluate("() => window.DBG.setSegment(2)") pg.wait_for_timeout(700) if len(r['states']) >= 3 and len(r['clips']) >= 3 and r['inFrame'] >= 4 and bench_shot: break pg.wait_for_timeout(450) if not bench_shot: FAIL('street_bench: no citizen sat on a real Lane B bench in 72 polls — no bench frame') else: bm, bp, sh = bench_shot['bm'], bench_shot['bp'], bench_shot['sh'] inf = [x for x in bm['rows'] if x['inFrame']] deg = sorted(x['toRoadDeg'] for x in bm['rows']) along = sorted(x['alongDeg'] for x in bm['rows']) if not bm['rows']: FAIL('street_bench: no bench instance matched a benchStops() station within 2 cm') elif bm['control'] != 0: FAIL(f"street_bench: the 3 m-offset control matched {bm['control']} instance(s) — " f"the 2 cm identification is not a measurement") else: mid = deg[len(deg) // 2] corners = [x for x in bm['rows'] if not x['nearIsOwn']] note(f"bench yaw: {len(bm['rows'])} of {bm['stations']} stations matched · front-vs-OWN-road " f"min {deg[0]}° / med {mid}° / max {deg[-1]}° · front-vs-street med " f"{along[len(along) // 2]}° · control 0 · {len(corners)} bench(es) sit nearer a CROSS " f"street than their own (max nearest-edge reading {max((x['nearDeg'] for x in corners), default=0)}°)") if deg[-1] > 5.0: FAIL(f'street_bench: a bench front is {deg[-1]}° off its own road — B\'s §42.5-A fix is ' f'not what this scene is drawing') note(f"bench frame: camera ({sh['px']}, {sh['pz']}) → ({sh['ax']}, {sh['az']}) — side-on, " f"{bp['sitters']} sitter(s) and {len(bp['rows'])} figure(s) in frame · " f"{bp['info']['drawCalls']} draws") write_shot(pg, 'street_bench', f"PROCITY R42 §42.6 — street_bench (Lane B §42.5-A, measured in this frame)\n" f"seed {SEED}, midday. Camera ({sh['px']}, {sh['pz']}) → ({sh['ax']}, {sh['az']}) — " f"5 m to the SIDE of the bench and 2.4 m out into the carriageway, deliberately NOT the " f"cluster search's pose: that one stands on the bench's own front line, and a person " f"facing the camera looks identical whichever way the bench under them is turned. In " f"profile, 'the sitter faces the road' is something you can see.\n" f"THE MEASUREMENT, and it is not B's arithmetic read back: bench instances are " f"identified by POSITION ONLY (a `benchStops(plan)` station within 2 cm — CONTROL: the " f"same stations offset 3 m match {bm['control']} of {bm['matched']}), and then the " f"angle is computed between the instance's own world +Z and the foot of the " f"perpendicular onto Lane A's road centreline for the edge that station names. " f"Neither input is the placement rule.\n" f"{bm['matched']} of {bm['stations']} town-wide stations are standing in the streamed " f"window ({len(inf)} in this frame). Bench FRONT vs the direction to the road: " f"min {deg[0]}° · median {deg[len(deg) // 2]}° · max {deg[-1]}°. Front vs the street's " f"own direction: median {along[len(along) // 2]}°. In R41 those two were 90° and 0°.\n" f"ONE INSTRUMENT NOTE, recorded rather than tuned away: {len([x for x in bm['rows'] if not x['nearIsOwn']])} " f"of {bm['matched']} benches stand nearer a CROSS street's centreline than their own, " f"because a station 14 m from a node is close to the corner. Attributing those to the " f"geometrically nearest edge reads up to " f"{max((x['nearDeg'] for x in bm['rows'] if not x['nearIsOwn']), default=0)}° and is " f"WRONG — the bench correctly faces the street it belongs to. This is the same trap " f"Lane B named in §42.5-A when they matched instances by expected perpendicular offset.\n" f"IN THIS FRAME: {bp['sitters']} citizen(s) on a bench, {len(bp['rows'])} figure(s) " f"total, {bp['info']['drawCalls']} draws of the ≤300 street law ({bp['info']['tris']} tris).", bp['rows'], cast_label='the bench frame', capture=False) if errs: FAIL(f'street: {len(errs)} console error(s): {errs[:2]}') finally: b.close() pair('street_postures', 'street_postures', "PROCITY R42 §42.6 — THE STREET: R41 as committed, and its OWN camera today", 'BEFORE (R41, committed): the R41 roster, and benches standing side-on to the road.', "AFTER (R42): the same pose, parsed out of the R41 sidecar. Five bodies recast (E §42.3 / " "D §42.4), bench + streetlight yaw fixed (B §42.5-A, 60/60). See street_bench.jpg for the " "yaw in profile.") # ── 4. THE CREDITS ──────────────────────────────────────────────────────────────────────────────── def shot_credits(p): head('4. credits_panel — ruling 3 on screen: the ped rows filled, no `unverified` left on a body') b, pg, errs = new_page(p) try: boot(pg, 'plansrc=osm&town=katoomba&weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.shot('street_noon')") pg.wait_for_timeout(600) link = pg.evaluate("() => { const el = document.getElementById('pc-creditlink'); return el ? el.textContent.trim() : null; }") pg.keyboard.press('F2') pg.wait_for_timeout(1200) # scroll to the ped rows — the panel is long and the rows this round filled are the point pg.evaluate(r"""() => { const el = document.getElementById('pc-credits'); if (!el) return; const box = el.querySelector('*') && el; const walk = (n) => { for (const c of n.querySelectorAll('*')) if (/character roster/i.test(c.textContent || '') && c.children.length < 6) return c; return null; }; const t = walk(el); if (t) t.scrollIntoView({ block: 'center' }); }""") pg.wait_for_timeout(400) st = pg.evaluate(r"""() => { const el = document.getElementById('pc-credits'); const t = el ? el.innerText : ''; return { open: !!(el && getComputedStyle(el).display !== 'none'), entries: (window.PROCITY.credits && window.PROCITY.credits.count) || null, chars: t.length, osm: /OpenStreetMap contributors/.test(t), odbl: /ODbL/.test(t), roster: /character roster/i.test(t), withdrawn: /withdrawn from the roster/i.test(t), chIds: /Ch01|Ch49|Ch23|Ch31/.test(t), unverifiedCount: (t.match(/unverified/gi) || []).length, pedUnverified: /(character roster|DJ five)[^]{0,600}?unverified/i.test(t) }; }""") note(f"HUD link {link!r} · panel open {st['open']} · roster row {st['roster']} · withdrawn row " f"{st['withdrawn']} · Mixamo Ch ids {st['chIds']} · 'unverified' appears {st['unverifiedCount']}× " f"· {st['chars']} chars") cap = (f"PROCITY R42 §42.6 — credits_panel (RULING 3, on screen)\n" f"seed {SEED}, ?plansrc=osm&town=katoomba — an OSM boot, so the ODbL obligation is live and " f"the HUD link carries it: {link!r}. F2 opens Lane B's §41.5 surface.\n" f"WHAT CHANGED THIS ROUND: R41 shipped the ped roster as amber `unverified` — B could not " f"establish the bodies' provenance from anything in this repo. Lane E §42.3-A answered it " f"from INSIDE the assets: all 24 source FBX carry Original|ApplicationVendor = 'Mixamo, Inc.' " f"in their own headers, and the four unmerged peds kept Mixamo's numbered character ids in " f"their material names (Ch43_Body = the luchador), which recovers the id for the other 20. " f"So the row now reads a real licence with per-asset evidence, `required: false` is correct, " f"and no attribution is owed. A second row records the four bodies WITHDRAWN on ruling 1 — " f"'why is that gone' is a question a credits panel should be able to answer.\n" f"On screen: the roster row ({st['roster']}), the withdrawn row ({st['withdrawn']}), the " f"Mixamo Ch ids ({st['chIds']}), the © OpenStreetMap contributors credit ({st['osm']}) and " f"ODbL 1.0 ({st['odbl']}). 'unverified' still appears {st['unverifiedCount']}x in the panel — " f"on the PROPS row (props-house-library, 18 house props), which is a different, still-open " f"debt and is deliberately not hidden. NO ped row is unverified: {not st['pedUnverified']}.\n" f"Zero draws by construction (DOM); credits.json is fetched on first open, not at boot.") write_shot(pg, 'credits_panel', cap) if not st['open']: FAIL('credits_panel: the panel did not open on F2') if not st['roster']: FAIL('credits_panel: the character-roster row is not on screen') if not st['withdrawn']: FAIL('credits_panel: the withdrawn-bodies row is not on screen') if st['pedUnverified']: FAIL('credits_panel: a ped row still reads `unverified` — ruling 3 is not met') if errs: FAIL(f'credits_panel: {len(errs)} console error(s): {errs[:2]}') finally: b.close() def main(): srv = serve() try: from playwright.sync_api import sync_playwright with sync_playwright() as p: if not ONLY or 'pub' in ONLY: shot_pub(p) if not ONLY or 'booth' in ONLY: shot_booth(p) if not ONLY or 'street' in ONLY: shot_street(p) if not ONLY or 'credits' in ONLY: shot_credits(p) finally: srv.terminate() print('') if fails: print(f'\033[31m● FAIL\033[0m — {len(fails)} problem(s):') for f in fails: print(f' · {f}') return 1 print(f'\033[32m● PASS\033[0m — the tour is in {OUTDIR}, every humanoid frame carrying its R10 line ' f'and its CAST line, every pair composited against the committed R41 original') return 0 if __name__ == '__main__': sys.exit(main())