FIVE GATES, FIVE CONTROLS, each demonstrated in the same run. New F-owned tools wired into qa.sh as Gate 8: r39_address.mjs · r39_transmission.py · r39_runtime.py · r39_shots.py (--no-r39 skips the browser half). Lane A's selfcheck reproduces exactly: 157,647/157,647 ALL GREEN, fingerprint 0x5f76e76 — ZERO GOLDENS MOVED, in a round that added an address layer, a fog ledger, a spawn move and a new interior fitting. THE ROUND'S BIGGEST NUMBER IS NOT THE ONE THE LEDGER ASKED FOR. Two port-isolated no-store roots, the same inode for every byte except one file — the shipped bookshelf GLB vs a copy with transmissionFactor:1 re-inserted (BIN sha1 IDENTICAL, only the JSON chunk moved) — both through interior_test.html's own drawSweep: GLB-on worst 329 (opshop/hall) -> 188 (dept/auto) -141 GLB-off worst 162 (opshop/hall) -> 162 (opshop/hall) 0 — the control's own control margin vs <=350 21 -> 162 +141 opshop 329->174 (-47%) · book 247->125 (-49%) · every other type 0, exactly the shape Lane C predicted from an isolated room, at sweep scale, by a harness C did not write. The worst interior room in the game is now dept/auto — a room with no bookshelf in it. The default boot fetches these GLBs from the public depot, so all four R39 GLBs were fetched off digalot.fyi and scanned: sha1 identical to the local tree, 0 transmissive materials, 4/4. Binding fact #3's third and final value. 39.1 ADDRESS — GREEN. 1,192/1,219 shops (97.8%), 23/23 'ways', 857/857 membership agree / 0 wrong (and the control is asserted to have CHECKED 857 edges). No cache -> supplier 'district', 0 names, still labels 72/72 from district+block. RED ARM: a 3 m shift error RAISES adelaide 89 -> 98 resolved shops (Lane A's number to the digit) while INVENTING 9 names; stats().shiftCheck reads DISAGREE on all 72 perturbations. And the honest limit: at 3 m the membership control reads 0 disagreements, because those shops front ways OSM leaves unnamed — only the shift cross-derivation catches it. 39.2 FOG — GREEN, both arms on one boot. HIDES 4/72 at spawn, 0 of 61 distant shops known. REVEALS 2,125 m -> 58 shops. FRONT TEST: probe from 5 m BEHIND each of 59 unknown shopfronts, 0 learned. +0 DRAWS AND +0 TRIS, four freshly launched browsers, both orders, drained, 3 reads each, with the arms asserted distinguishable first. 8/8 corrupt saves rejected, state untouched. Null store on classic/game=0/fog=0; game=0 and fog=0 pixel-identical, default differs. 39.3 THE SIGN — GREEN. Shops in front of the player at spawn: 4/4 · 4/4 · 4/4 · 1/1, and ZERO on all four with the pre-R39 sign re-derived on the same tree. The live spawn is asserted equal to the +sin derivation, so the shell is what is scored. Re-baselined WITHOUT falsifying a release record: the v7 tour and v7-beta frames are kept as history; docs/shots/laneF_r39/ ships the new pair with its own BEFORE. The R35 gate itself is pose-independent and was green either side. 39.4 RUMMAGE BIN — GREEN. 120 rooms: tub 60/60 both arms, kind:'bin' 0/60 default vs 60/60 armed, 0 carves, 60/60 deterministic, reachable 0.80-1.03 m by a flood fill written here. THE CONTENTS DID NOT SHIP: on the default street boot the tub is kind:'prop', 0 dig targets, E opens nothing. 39.5 TRANSMISSION VALIDATOR — GREEN and it FIRES: hard-fails rc 1 on the fabricated glass asset AND passes on the shipped tree. THREE CORRECTIONS, ALL MEASURED. A's "a 30 m shift returns 0 street names" — it returns 40/12/47 wrong ones; a big shift lands you on the next street, it does not degrade to null. B's "a fresh export has no `known` key" — true of ?fog=0, no longer of a default boot, because B's own +sin fix means you spawn in front of four shopfronts. And my own: I first aimed the transmission control at web/assets/models/ when validate_manifest.py reads pipeline/_normalized/, and nearly filed a defect against Lane E on the strength of it. Aim the control at the code path, not the plausible path. HELD -> LANE E: check_transmission() skips any GLB with no local _normalized copy and never scans web/assets/models/. Demonstrated with a live control — remove the local copy and the same glass asset passes rc 0. A depot-only transmissive GLB (the exact shape of longbench and streetlight) would ship unseen. No tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
6.6 KiB
Python
127 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""PROCITY Lane F — R39: RE-BASELINE the two frames that photograph the spawn pose.
|
|
|
|
Lane B's `+sin` fix moved the non-classic spawn (`index.html:500`, the R32 cluster-pose). Two of this
|
|
lane's picture baselines were taken at the OLD pose and are therefore photographs of the defect:
|
|
|
|
· `docs/shots/v7_tour/02_the_strip.png` — "the cluster-pose spawn: midday on the main street"
|
|
· `docs/shots/v7_beta/first_five_splash.png` — the R35 first-five-minutes splash, shot at the spawn
|
|
|
|
Those two files are **not overwritten**: they are artefacts of the tagged v7.0 epoch and re-shooting
|
|
them on a v9 tree would falsify a release record. The re-baseline lands in `docs/shots/laneF_r39/`
|
|
with its own BEFORE (the same tree, camera parked at the old `-sin` pose), so the pair is a
|
|
comparison rather than a claim.
|
|
|
|
Run: tools/.venv/bin/python tools/qa/r39_shots.py
|
|
"""
|
|
import sys, os, time, socket, subprocess, pathlib
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
|
OUT = ROOT / 'docs' / 'shots' / 'laneF_r39'
|
|
PORT = int(os.environ.get('PROCITY_R39_SHOT_PORT', '8761'))
|
|
HOST = f'http://127.0.0.1:{PORT}'
|
|
SEED = 20261990
|
|
|
|
NOSTORE = r'''
|
|
import sys, http.server, functools
|
|
class H(http.server.SimpleHTTPRequestHandler):
|
|
def end_headers(self):
|
|
self.send_header('Cache-Control', 'no-store'); 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()
|
|
'''
|
|
|
|
# both spawn derivations, from the live plan — identical to tools/qa/r39_runtime.py's JS_SIGN
|
|
JS_POSES = r"""
|
|
() => {
|
|
const P = window.PROCITY, plan = P.plan;
|
|
const lotById = new Map(plan.lots.map(l => [l.id, l]));
|
|
const spawnWith = (sgn) => {
|
|
const ms = new Set((plan.blocks || []).filter(b => b.kind === 'mainstreet').map(b => b.id));
|
|
const doorOf = (s) => { const l = lotById.get(s.lot); if (!l) return null;
|
|
const ry = l.ry || 0;
|
|
return { x: l.x + sgn * Math.sin(ry) * (l.d / 2 + 2.6), z: l.z + sgn * Math.cos(ry) * (l.d / 2 + 2.6), block: l.block }; };
|
|
let doors = plan.shops.map(doorOf).filter(Boolean);
|
|
const m = doors.filter(d => ms.has(d.block)); if (m.length >= 2) doors = m;
|
|
if (doors.length < 2) return null;
|
|
let cx = 0, cz = 0; for (const d of doors) { cx += d.x; cz += d.z; } cx /= doors.length; cz /= doors.length;
|
|
let vx = 0, vz = 0; for (const d of doors) { vx += (d.x - cx) ** 2; vz += (d.z - cz) ** 2; }
|
|
const ax = vx >= vz ? 'x' : 'z';
|
|
let stand = doors[0], bd = Infinity;
|
|
for (const d of doors) { const dd = (d.x - cx) ** 2 + (d.z - cz) ** 2; if (dd < bd) { bd = dd; stand = d; } }
|
|
let lo = Infinity, hi = -Infinity;
|
|
for (const d of doors) { lo = Math.min(lo, d[ax]); hi = Math.max(hi, d[ax]); }
|
|
const dir = (hi - stand[ax] >= stand[ax] - lo) ? 1 : -1;
|
|
return { x: stand.x, z: stand.z, yaw: ax === 'x' ? Math.atan2(-dir, 0) : Math.atan2(0, -dir) };
|
|
};
|
|
return { fixed: spawnWith(1), old: spawnWith(-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 main():
|
|
from playwright.sync_api import sync_playwright
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
proc = 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): break
|
|
time.sleep(0.1)
|
|
shots = []
|
|
try:
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
|
for town, q, seg, tag in (('synthetic', '', 2, 'the_strip'),
|
|
('katoomba_real', 'plansrc=osm&town=katoomba_real', 2, 'the_strip_katoomba')):
|
|
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&roster=v1&pop=0' + (('&' + q) if q else ''))
|
|
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=30000)
|
|
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
|
pg.wait_for_timeout(2500)
|
|
poses = pg.evaluate(JS_POSES)
|
|
pg.evaluate('(s) => window.DBG.setSegment(s)', seg)
|
|
for which in ('old', 'fixed'):
|
|
pz = poses[which]
|
|
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
|
|
try: pg.wait_for_function('() => window.PROCITY.chunks.pending === 0', timeout=20000)
|
|
except Exception: pass
|
|
pg.wait_for_timeout(2000)
|
|
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
|
|
name = f"{tag}-{'BEFORE-minus-sin' if which == 'old' else 'AFTER-plus-sin'}.png"
|
|
pg.screenshot(path=str(OUT / name))
|
|
shots.append(name)
|
|
print(f" {name} @ ({pz['x']:.1f}, {pz['z']:.1f}) yaw {pz['yaw']:.3f}")
|
|
# the R35 first-five frame: a FRESH game at the fixed spawn, splash + hunt line visible
|
|
b2 = p.chromium.launch()
|
|
pg2 = b2.new_page(viewport={'width': 1280, 'height': 720})
|
|
pg2.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&roster=v1&pop=0')
|
|
pg2.wait_for_function('window.DBG && window.DBG.ready === true', timeout=30000)
|
|
pg2.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
|
pg2.wait_for_timeout(2500)
|
|
pz = pg2.evaluate(JS_POSES)['fixed']
|
|
pg2.evaluate('() => window.DBG.setSegment(2)')
|
|
pg2.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
|
|
try: pg2.wait_for_function('() => window.PROCITY.chunks.pending === 0', timeout=20000)
|
|
except Exception: pass
|
|
pg2.wait_for_timeout(2000)
|
|
pg2.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
|
|
hunt = pg2.evaluate("""() => { const h = document.getElementById('pc-hunt');
|
|
return h ? { shown: h.style.display !== 'none', text: h.textContent } : null; }""")
|
|
pg2.screenshot(path=str(OUT / 'first_five-AFTER-plus-sin.png'))
|
|
shots.append('first_five-AFTER-plus-sin.png')
|
|
print(f" first_five-AFTER-plus-sin.png hunt line: {hunt}")
|
|
b2.close(); b.close()
|
|
finally:
|
|
proc.terminate()
|
|
print(f"\n{len(shots)} frames → {OUT}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|