qa.sh --strict --matrix: 7 passed / 0 failed / 0 warn. selfcheck ALL GREEN 156,352/156,352 ·
MATRIX GREEN 10 towns x 7 gates · scaffold_check GREEN (F's re-pins took) · flags harness green,
carrying the kill-the-server gate and the beta crate gate.
F RE-PINNED ITS OWN FILES per A's handoff (A doesn't edit other lanes' files, correctly):
GOLDEN_HASH 0x3fa36874 -> 0x5f76e76 · GIG_GOLDEN 0xb1d48ea1 -> 0xec7a2d39 · melbourne · katoomba
· the captions. AND A CAUGHT A LIVE HAZARD IN F'S GATE: hash_ok carried `or ('3fa36874' in
r.stdout)` — a bare-string OR-clause, dead the moment the covenant moved and worse than dead,
because it would go green off a SENTENCE ABOUT the old covenant appearing in stdout. Deleted.
The check derives from GOLDEN_HASH or it doesn't exist.
JOHN'S RULING IS THE EPOCH'S BEST LINE: the covenant is anti-drift machinery, not a defect
preservative. 0x3fa36874 did its job for five epochs — it never drifted. It just was never right,
and no hash can catch what was wrong in the first commit. A golden proves NOTHING CHANGED; it
cannot prove anything is CORRECT. Every gate we own was green over a world of back walls, and the
only thing that ever saw it was a human standing in the street looking at a wall.
THE FRONTAGE FRAME TOOK FIVE ATTEMPTS, FOUR OF THEM F'S FAULT:
1-3. cluster pose (the town's heart isn't this shop) / lot-anchored facing along the edge /
lot-normal facing — three photographs of empty road. Also never winnable: the shops faced
backwards the whole time. F was posing a camera at a wall and tuning the geometry.
4. B's helper — and F THREW THE RESULT AWAY. poseForShopFront RETURNS a pose; it does not
apply one. F called it, error-checked it, and never touched the camera, so the frame was
the spawn paddock — and F's error-check PASSED, because the call succeeded perfectly. The
vacuous check one last time, in the last frame of the epoch.
5. Applied it -> MONSTER ROBOT PARTY. Sign, door, awning, a ped walking in off Musgrave Road.
The measurement that would have found the facade bug in five minutes — compare the two
conventions against each other — is the one nobody ran for nine rounds, F included, right up to
the vacuous 72/72 F nearly filed as "B is wrong".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
201 lines
11 KiB
Python
201 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""PROCITY v5.0 RELEASE TOUR — THE LIVE CRATE (Lane F, R27 ledger #5)
|
|
|
|
The crate walk, in the order you'd actually do it: Musgrave Road → Monster Robot Party's frontage (B's
|
|
stocked-shopfront mark) → inside, riffling ITS OWN records → a mint shop, captioned honestly → the
|
|
`?classic=1` control that still boots the exact v2 town.
|
|
|
|
Posing: the street frames go through F's R21 cluster primitive (reused from tour_shots.py). Per-EDGE shop
|
|
counts are ~1 on a real graph, so "the busiest edge" poses you at bare bitumen — the Fitzroy trap, twice
|
|
learned. The interior frames pose themselves: the room builds you a metre inside its own door.
|
|
|
|
Honesty rules baked in, not bolted on:
|
|
· the mint frame says MINT in its caption. A minted crate is plausible, not real, and a tour is exactly
|
|
where that distinction gets quietly lost.
|
|
· the real frame's caption carries the shop's actual name and address, because it IS one.
|
|
· nothing here boots ?live=1. A tour frame must be reproducible by anyone who clones this repo, and
|
|
tier 2 needs a POS none of them have. The live tier is proven by the gate, which owns its own server;
|
|
the tour shows tier 1, which is what ships in the box.
|
|
"""
|
|
import json, subprocess, sys, time, pathlib
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / 'tools' / 'qa'))
|
|
PORT = 8131
|
|
HOST = f'http://127.0.0.1:{PORT}'
|
|
SEED = 20261990
|
|
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v5'
|
|
SHOP = 3962749
|
|
|
|
# name, query, segment, mode, caption
|
|
V5_TOUR = [
|
|
('musgrave_road', f'plansrc=osm&town=redhill_godverse&stock=real&seed={SEED}', 3, f'frontage:{SHOP}',
|
|
"the frontage — Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the "
|
|
"game because it is on this road"),
|
|
('the_real_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, f'shop:{SHOP}',
|
|
"THE REAL CRATE — Monster Robot Party (147 Musgrave Rd): 120 records off its own till, real covers, "
|
|
"real titles, real prices. Sell one in the shop and it leaves this crate"),
|
|
('a_mint_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, 'mint',
|
|
"a MINT crate — plausible, NOT real: a seeded sample standing in for a shop whose till we don't hold. "
|
|
"Same tier, same offline guarantee, different provenance — and the atlas says so in its own index"),
|
|
('classic_covenant', f'classic=1&seed={SEED}', 2, 'street',
|
|
"?classic=1 — the exact v2 town, byte-identical forever (0x5f76e76). Five epochs, one covenant"),
|
|
]
|
|
|
|
SHOT_JS = r"""
|
|
async ({ mode, seg, shop }) => {
|
|
const P = window.PROCITY, D = window.DBG, T = P.THREE;
|
|
D.setSegment(seg);
|
|
const plan = P.plan, shops = plan.shops || [];
|
|
|
|
if (mode.startsWith('shop:') || mode === 'mint') {
|
|
let target;
|
|
if (mode === 'mint') {
|
|
const man = await (await fetch('assets/stock_godverse/index.json')).json();
|
|
const mint = new Set((man.shops||[]).filter(s => s.sourcing === 'mint').map(s => s.godverseShopId));
|
|
const byId = new Map((man.shops||[]).map(s => [s.godverseShopId, s]));
|
|
target = shops.find(s => mint.has(s.godverseShopId) &&
|
|
(byId.get(s.godverseShopId).types||[]).includes(s.type));
|
|
} else {
|
|
target = shops.find(s => s.godverseShopId === shop);
|
|
}
|
|
if (!target) return { err: 'no target shop for ' + mode };
|
|
D.enterShop('g:' + target.godverseShopId);
|
|
await new Promise(r => setTimeout(r, 1500));
|
|
const si = D.stockInfo();
|
|
// pose: stand back from the spawn looking into the room, so the crates are in frame
|
|
const im = P.interiorMode, sp = im.current.spawn;
|
|
P.camera.position.set(sp.x, 1.6, sp.z + 0.2);
|
|
P.camera.rotation.set(0, sp.ry || 0, 0);
|
|
P.camera.updateMatrixWorld();
|
|
// Beauty shot: hide EVERY overlay, not just the street HUD. R21 taught this on the street frames
|
|
// and F only fixed it there — the first v5 interior frame came back with "press Esc to leave"
|
|
// across the top of the money shot. Same lesson, second surface.
|
|
// Hide EVERY DOM overlay rather than guessing ids — the interior banner is created at runtime, so a
|
|
// hardcoded selector list is exactly how the HUD sneaks back into the next money shot.
|
|
for (const el of document.body.children) if (el.tagName !== 'CANVAS') el.style.display = 'none';
|
|
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
|
|
const info = D.info();
|
|
return { name: si.name, gid: si.godverseShopId, items: si.packItems, base: si.base,
|
|
shops: shops.length, edges: (plan.streets.edges||[]).length,
|
|
draws: info.drawCalls, tris: info.tris };
|
|
}
|
|
|
|
// ── street ──
|
|
// [R27w5] The frontage frame now goes through B's `poseForShopFront` — B's helper, B's geometry, B's
|
|
// shop. F re-derived this three times and produced three photographs of empty road; the fourth attempt
|
|
// would have been the same mistake with more arithmetic. The lane that owns the frontage owns the pose.
|
|
// (And the empty road was never the pose at all — the shops faced backwards. Fixed by A, wave 5.)
|
|
const nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
|
|
const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
|
|
let hub = -1, best = null;
|
|
if (mode.startsWith('frontage:')) {
|
|
const r = D.poseForShopFront('g:' + mode.split(':')[1], 8);
|
|
if (!r || r.error) return { err: 'poseForShopFront: ' + JSON.stringify(r) };
|
|
// poseForShopFront RETURNS a pose; it does not apply one. F called it, error-checked it, and threw
|
|
// the result away — so the camera never moved and the frame was the spawn paddock. A helper that
|
|
// hands you a value is not a helper that does the thing.
|
|
P.camera.position.set(r.pos[0], r.pos[1], r.pos[2]);
|
|
P.camera.lookAt(r.look[0], r.look[1], r.look[2]);
|
|
P.camera.updateMatrixWorld();
|
|
const sh = shops.find(x => x.godverseShopId === +mode.split(':')[1]);
|
|
const lot = sh ? (plan.lots || []).find(l => l.id === sh.lot) : null;
|
|
hub = lot ? lots.filter(o => Math.hypot(o.x - lot.x, o.z - lot.z) < 60).length : -1;
|
|
} else {
|
|
for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++;
|
|
if (c > hub) { hub = c; best = l; } }
|
|
if (best) {
|
|
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
|
|
const a = nById.get(e.a), b2 = nById.get(e.b);
|
|
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
|
|
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
|
|
t = Math.max(6, Math.min(L - 6, t));
|
|
const back = Math.min(26, t);
|
|
D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
|
|
} else { D.shot('street_noon'); }
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 1800));
|
|
for (const el of document.body.children) if (el.tagName !== 'CANVAS') el.style.display = 'none';
|
|
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
|
|
const info = D.info();
|
|
return { name: plan.name, shops: shops.length, edges: (plan.streets.edges || []).length,
|
|
hub, draws: info.drawCalls, tris: info.tris };
|
|
}
|
|
"""
|
|
|
|
|
|
def ensure_server():
|
|
import socket
|
|
s = socket.socket()
|
|
try:
|
|
s.connect(('127.0.0.1', PORT)); s.close(); return None
|
|
except Exception:
|
|
pass
|
|
srv = subprocess.Popen([sys.executable, '-m', 'http.server', str(PORT)],
|
|
cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
time.sleep(1.2)
|
|
return srv
|
|
|
|
|
|
def main():
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError:
|
|
sys.exit('playwright not installed')
|
|
print(f"\033[1mv5.0 RELEASE TOUR — THE LIVE CRATE\033[0m ({len(V5_TOUR)} frames → docs/shots/release_v5/)")
|
|
OUTDIR.mkdir(parents=True, exist_ok=True)
|
|
srv = ensure_server()
|
|
shots, errs_total = [], 0
|
|
try:
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
for name, query, seg, mode, cap in V5_TOUR:
|
|
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
|
errs = []
|
|
pg.on('pageerror', lambda e: errs.append(str(e)))
|
|
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
|
pg.goto(f'{HOST}/index.html?{query}&dbg=1')
|
|
pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready',
|
|
timeout=25000)
|
|
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
|
try:
|
|
pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000)
|
|
except Exception:
|
|
pass
|
|
r = pg.evaluate(SHOT_JS, {'mode': mode, 'seg': seg, 'shop': SHOP})
|
|
if r.get('err'):
|
|
print(f" \033[31m✗\033[0m {name}: {r['err']}"); errs_total += 1; pg.close(); continue
|
|
pg.screenshot(path=str(OUTDIR / f'{name}.png'))
|
|
extra = (f" · {r['items']} records from {r['base']}" if r.get('items')
|
|
else (f" · retail heart: {r['hub']} shops/60m" if r.get('hub') else ''))
|
|
caption = (f"{cap} · {r['name']}{extra} · {r['draws']} draws · {r['tris']:,} tris")
|
|
shots.append({'name': name, 'file': f'{name}.png', 'caption': caption})
|
|
print(f" \033[32m●\033[0m {name}: {caption}")
|
|
errs_total += len(errs)
|
|
if errs:
|
|
print(f" \033[31m{len(errs)} page error(s)\033[0m: {errs[0][:110]}")
|
|
pg.close()
|
|
b.close()
|
|
finally:
|
|
if srv:
|
|
srv.terminate()
|
|
|
|
html = ["<!doctype html><meta charset=utf-8><title>PROCITY v5.0 — THE LIVE CRATE</title>",
|
|
"<style>body{background:#111;color:#ddd;font:15px/1.6 -apple-system,sans-serif;max-width:1100px;"
|
|
"margin:40px auto;padding:0 20px}img{width:100%;border-radius:6px;margin:10px 0}"
|
|
"h1{font-weight:600}p{color:#aaa}hr{border:0;border-top:1px solid #333;margin:36px 0}</style>",
|
|
"<h1>PROCITY v5.0 — THE LIVE CRATE</h1>",
|
|
"<p>One real record shop, on its real street, holding its real stock — and a server that can die "
|
|
"mid-session without the game noticing.</p>"]
|
|
for s in shots:
|
|
html.append(f"<hr><h3>{s['name']}</h3><img src='{s['file']}'><p>{s['caption']}</p>")
|
|
(OUTDIR / 'contact.html').write_text('\n'.join(html))
|
|
(OUTDIR / 'shots.json').write_text(json.dumps(shots, indent=1))
|
|
print(f"\n → {OUTDIR}/contact.html ({len(shots)} frames, {errs_total} page error(s))")
|
|
return 0 if (len(shots) == len(V5_TOUR) and errs_total == 0) else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|