Lane F R15 (v3.0): identity continuity — wire D's tonight-roster into the crowd (ledger #3)
The ped you followed into a venue now turns up in its crowd. D published the seam in R14; F wires the three touches:
- interior_mode.js: crew.spawn({ roster: rosterOf(shop.id) }) — the front crowd slots become tonight's roster
- index.html: rosterOf=(id)=>citizens.tonightRoster(id) into interior_mode; onAdmit=(e)=>citizens.recordVenueEntry(id,e)
on the queue (each admit relays into the roster); the clear is automatic on the existing setGig(id,false)
- flags_check.py: smoke_continuity — asserts the seam (crowd includes roster-cap, pedIndex in tonightRoster),
empty-roster injects nobody (shell side of D's byte-identical law), + ledger #5 settled-tris honesty
Verified: 2 queue admits carry into the crowd; empty roster 0-injected; settled interior 71k->182k tris
(C's finding — interior gate is draws <=54, no violation). 0 console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
146b5f3d2d
commit
687d23f654
@ -931,6 +931,88 @@ def smoke_gigs(p):
|
|||||||
b.close()
|
b.close()
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_continuity(p):
|
||||||
|
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
|
||||||
|
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
|
||||||
|
tonightRoster(id) → GigCrew.spawn({roster}) → the front crowd slots carry those identities. Asserts the
|
||||||
|
seam on identities (crewInfo.fromRoster + member.pedIndex ∈ tonightRoster), the invariant crowd ⊇
|
||||||
|
roster ∩ cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
|
||||||
|
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
|
||||||
|
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
|
||||||
|
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
|
||||||
|
if not gigs_landed(p):
|
||||||
|
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||||||
|
return
|
||||||
|
b, pg, errs = new_page(p)
|
||||||
|
try:
|
||||||
|
boot(pg, 'gigs=1')
|
||||||
|
pg.keyboard.press('Space')
|
||||||
|
res = pg.evaluate("""async () => {
|
||||||
|
const P = window.PROCITY, D = window.DBG;
|
||||||
|
const id = P.gigs.venueShopIds[0];
|
||||||
|
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
|
||||||
|
// 1) EMPTY-ROSTER PATH — enter before any admit; crew must inject only what's in the roster
|
||||||
|
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
|
||||||
|
D.setSegment(5); // NIGHT — the gig is on
|
||||||
|
const rosterAtEmpty = rosterOf();
|
||||||
|
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
|
||||||
|
const ei = P.interiorMode.crewInfo;
|
||||||
|
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
|
||||||
|
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||||||
|
// 2) POPULATED PATH — let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
|
||||||
|
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
|
||||||
|
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||||||
|
const q = P.venueQueues && P.venueQueues.get(id);
|
||||||
|
const admitted = [];
|
||||||
|
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
|
||||||
|
const roster = rosterOf();
|
||||||
|
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
|
||||||
|
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
|
||||||
|
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
|
||||||
|
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
|
||||||
|
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
|
||||||
|
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||||
|
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
|
||||||
|
await new Promise(r => setTimeout(r, 1600));
|
||||||
|
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||||
|
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
|
||||||
|
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
|
||||||
|
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
|
||||||
|
watchPoints: room ? room.watchPoints.length : 0,
|
||||||
|
trisImmediate, draws, trisSettled, drawsSettled };
|
||||||
|
}""")
|
||||||
|
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
|
||||||
|
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
|
||||||
|
if res['emptyFromRoster'] == want_empty:
|
||||||
|
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
|
||||||
|
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
|
||||||
|
else:
|
||||||
|
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
|
||||||
|
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
|
||||||
|
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
|
||||||
|
if admitted and all(a in roster for a in admitted):
|
||||||
|
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
|
||||||
|
else:
|
||||||
|
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
|
||||||
|
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
|
||||||
|
cap = res['crowd']
|
||||||
|
want = min(len(roster), cap)
|
||||||
|
subset_ok = set(rosterCrowd).issubset(set(roster))
|
||||||
|
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
|
||||||
|
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
|
||||||
|
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
|
||||||
|
else:
|
||||||
|
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
|
||||||
|
f"vs roster {roster} (subset={subset_ok})")
|
||||||
|
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
|
||||||
|
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
|
||||||
|
f"(instrument GLBs; draws {res['draws']}→{res['drawsSettled']} ≤ {GIG_DRAWS_MAX} gate)")
|
||||||
|
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||||
|
else: OK('continuity: 0 console errors')
|
||||||
|
finally:
|
||||||
|
b.close()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
srv = ensure_server()
|
srv = ensure_server()
|
||||||
try:
|
try:
|
||||||
@ -955,7 +1037,8 @@ def main():
|
|||||||
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
||||||
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
|
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
|
||||||
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
|
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
|
||||||
smoke_gigs(p) # R12 — v3.0-alpha gig layer (schedule · band · crowd · budget · cover · noassets)
|
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
|
||||||
|
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
|
||||||
finally:
|
finally:
|
||||||
if srv: srv.terminate()
|
if srv: srv.terminate()
|
||||||
|
|
||||||
|
|||||||
@ -188,7 +188,8 @@ const gigState = (GIGS_ON && plan.gigs && plan.gigs.length) ? createGigState({ p
|
|||||||
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
|
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
|
||||||
// time (door click), long after citizens is initialized — so the forward reference is safe.
|
// time (door click), long after citizens is initialized — so the forward reference is safe.
|
||||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
|
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
|
||||||
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id), gigState });
|
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id),
|
||||||
|
rosterOf: (id) => citizens.tonightRoster(id), gigState }); // [R15] identity continuity: the crowd IS tonight's roster
|
||||||
// [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's
|
// [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's
|
||||||
// open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin
|
// open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin
|
||||||
// doesn't read as "walked out of the shop".
|
// doesn't read as "walked out of the shop".
|
||||||
@ -385,7 +386,10 @@ function frame() {
|
|||||||
if (open && !q) {
|
if (open && !q) {
|
||||||
q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
||||||
q.spawn(scene, { queueZone: venuePresentation && venuePresentation.queueZoneFor(id),
|
q.spawn(scene, { queueZone: venuePresentation && venuePresentation.queueZoneFor(id),
|
||||||
gigId: (gigState.gigOf(id) || {}).gigId | 0 });
|
gigId: (gigState.gigOf(id) || {}).gigId | 0,
|
||||||
|
// [R15] identity continuity: relay each queue admit → D's tonight roster, so the
|
||||||
|
// punter who queued turns up in the interior crowd (clear is automatic at setGig(id,false)).
|
||||||
|
onAdmit: (entry) => citizens.recordVenueEntry(id, entry) });
|
||||||
venueQueues.set(id, q);
|
venueQueues.set(id, q);
|
||||||
} else if (!open && q) { q.disposeAll(); venueQueues.delete(id); }
|
} else if (!open && q) { q.disposeAll(); venueQueues.delete(id); }
|
||||||
if (q) q.update(dt);
|
if (q) q.update(dt);
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const WALLA_KEY = 'crowd-walla'; // [R13] manifest.ambience — crowd murmur l
|
|||||||
|
|
||||||
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
|
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
|
||||||
dig: digEnabled = false, stockReal = false, wallet = null,
|
dig: digEnabled = false, stockReal = false, wallet = null,
|
||||||
occupancyOf = null, gigState = null }) {
|
occupancyOf = null, gigState = null, rosterOf = null }) {
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color('#0e0b07');
|
scene.background = new THREE.Color('#0e0b07');
|
||||||
|
|
||||||
@ -228,15 +228,18 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
|||||||
seedKey: `${shop.id}#${i}`,
|
seedKey: `${shop.id}#${i}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: 3 on the deck, up to 8 at the watch
|
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: a 4-piece on the deck, up to 8/12 at
|
||||||
// points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at watchPoints.length
|
// the watch points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at
|
||||||
// and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these rigs too.
|
// watchPoints.length and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these
|
||||||
// Instruments are D's primitives this round — E's instrument GLBs landed but nothing wires them yet
|
// rigs too; E's instrument GLBs ride in via D's instrumentFor seam.
|
||||||
// (D's `opts.instrumentFor` seam is open); asset law says a primitive band is still a gig. → LANE_F_NOTES §12.
|
// [Lane F R15 — identity continuity (ledger #3)] pass D's TONIGHT ROSTER so the front crowd slots ARE the
|
||||||
|
// punters who came in (surge occupants + queue admits, keyed by venue). D fills the rest with seeded
|
||||||
|
// strangers; an EMPTY roster is byte-identical to R13 (D's law), which is what a fresh/quiet venue gets.
|
||||||
crewInfo = null;
|
crewInfo = null;
|
||||||
if (gigOn && current.stage && current.watchPoints && current.watchPoints.length) {
|
if (gigOn && current.stage && current.watchPoints && current.watchPoints.length) {
|
||||||
if (!crew) crew = new GigCrew({ citySeed: plan.citySeed, fleet });
|
if (!crew) crew = new GigCrew({ citySeed: plan.citySeed, fleet });
|
||||||
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints, gig: tonight });
|
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints,
|
||||||
|
gig: tonight, roster: rosterOf ? rosterOf(shop.id) : [] });
|
||||||
}
|
}
|
||||||
doorReturn = { x: camera.position.x, y: camera.position.y, z: camera.position.z, ry: camera.rotation.y };
|
doorReturn = { x: camera.position.x, y: camera.position.y, z: camera.position.z, ry: camera.rotation.y };
|
||||||
camera.position.set(current.spawn.x, EYE, current.spawn.z);
|
camera.position.set(current.spawn.x, EYE, current.spawn.z);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user