From 22f5f11dde7f7c968f676a1efadfad67fbb194ff Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 20 Jul 2026 06:06:42 +1000 Subject: [PATCH] Fix card/judge disagreement under text lag; session handover Co-Authored-By: Claude Fable 5 --- LANEHANDOVER.md | 70 ++++++++++++++++++++++++++++++++++++ src/scenes/door/DoorScene.ts | 9 +++-- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/LANEHANDOVER.md b/LANEHANDOVER.md index ba5d653..d1aa15f 100644 --- a/LANEHANDOVER.md +++ b/LANEHANDOVER.md @@ -932,3 +932,73 @@ the literal back), and (2) the ECONOMY TUNING PASS (baselines: content's 3-night run data + door's Phase-1 measurements + floor's idle/attentive table). After those land, the next lanes are content-expansion (encounters, repeat gating), venue 2, and the v0.4 art pass per docs/ASSETS.md. + +--- +### SESSION — SOLO BUILD-OUT (Fable) — 2026-07-19 23:30 +**Branch/commits:** main (three commits this session; deployed live) +**The headline: THE ECONOMY TUNING PASS IS DONE — against bots, held to bands.** +`tests/sim/economy.ts` is a full node-side bot night (QueueManager + judge + +Meters + deferred pipeline + churn model, no Phaser); `economy.test.ts` runs +careful/sloppy/dawdler bots over 5 seeds and asserts NINE feel-bands. Those +tests are now the economy's contract — a knob change that kills the feel fails +CI, not a playtest three weeks later. Where the numbers landed (careful bot): +admits 87 > denies 69 (was 48/68 — the room fills now), vibe pinned-share 0.34 +(was 0.99 — the meter is alive), aggro peaks ~36 (was 6), slam queue avg 7 +peak 16+ (was 1.0/8 — the 10:30 slam is REAL, verified live: 22 in the queue +at 12:33 AM), hype decays while you work (was a permanent x3 ratchet), sloppy +dies of vibe with 3 strikes, extreme dawdling riots before it profits. +**What the tuning changed (all commented at the definition):** +- judge: denyViolator 6→2, denyClean 2→1, admitClean 3→2, admitBadId 2→1. +- queue: calmPerSec 0.9→0.35, NEW hypeDecayPerSec 0.045 (theatre fades while + you serve — kills the ratchet). +- arrivals: slam peak 0.42→0.72/min, night total ~70→~120. +- generator: logo rate 0.35→0.18; sunnies/bucketHat/blazer weights halved. +- NEW `core/meters.vibeCoolingPerMin`: the room cools above 50 (-0.4) and + faster above 70 (-0.8), emitted per clock-minute — nobody stays impressed. +- **Dazza's attention span (`ACTIVE_RULE_CAP = 4`)**: only his last four rules + are live; older ones get greyed on the card + a retraction toast ("dazza: + forget the 'NO THONGS' thing. new priorities"). With all 8 cumulative, ~55% + of the late crowd was deniable and rule-following EMPTIED the room. +- **Natural churn**: patrons go home after 50-140 clock-min (`floor:leave`, + new contract event, CrowdSim emits, NightScene frees capacity). Without it + capacity was a one-way ratchet and the door locked shut from 1 AM. +- **Capacity strike bug FIXED**: the strike reason embedded the patron id, so + the reason-dedupe never collapsed and every over-80 admit filed its own + strike — 3 admits = licence gone. Now one stable reason per night. (This is + what was really killing the long Kayden nights in Phase 2.) +**Also this session:** +- Floor SFX wired: stall bangs (doorBang), pat-down bin drops (clickerClunk), + via optional sfx handles on the overlay constructors. +- Cut-off dialogue now wears its blood-alcohol: `drunkify` on the patron's + reply, deterministic per dollSeed. +- **Encounters 4 → 8** (eighteenToday, kaydensMate, bigNightOut, twoOfThem — + register per the §4.3 rules, humane never secretly optimal) + cross-night + repeat gating (`GameState.seenEncounters`, recorded off the encounter + incident, excluded by the scheduler until everyone's been met). Scheduler + reworked: end-order greedy + backward latest-safe pass — a random draw can + no longer strand a pick; overflow drops gracefully. +- nightShift window tightened to [150,195] (its noLogos dilemma dies when the + rule cap rescinds noLogos at 200). +- **Bug found & fixed in browser verification**: Dazza's texts lag the clock + (send cooldown), so a rule could be rescinded before its card row existed — + the one-shot guard then swallowed the retirement forever and the card LIED + about what was enforceable. Card sync is idempotent now; verified converged + under worst-case lag. +- **The Kayden contradiction has now fired in real play** (content's open + item): floor stint → his 9:15 PM influencer admit → lied on the report → + Friday's paperwork: "kayden wrote his version of p2 up too. it does not + match urs. he is very proud of the log". Screenshot in session notes. +**Tests:** 632 passing / 0 failing (was 621). Gate clean. **Deployed to +https://monsterrobot.games/not-tonight/ (bundle index-W0Kf_Kh7.js, verified 200).** +**Broke / known-wonky:** +- The sim bot cheats (perfect information, no inspection time beyond a fixed + cost) — bands are calibrated to it, not to humans. John's playtest may want + the arrival curve ±15%; the bands make retunes safe to try. +- Dawdler test covers EXTREME stalling (6s); mild-stall dominance is killed by + hype decay but has no dedicated band. +- Rule retractions land as toasts; a phone text would be richer (needs a dazza + schedule slot — next content session). +- Sloppy bot dies by VIBE before aggro ever moves — arguably right (admit- + everything tanks the room via laps/ripens) but worth a human sanity-check. +**Next:** John's ear pass (M in a live night) and a human feel-pass over the +new economy — the bands make it safe to turn knobs. Then venue 2. diff --git a/src/scenes/door/DoorScene.ts b/src/scenes/door/DoorScene.ts index 67df893..0fc75cc 100644 --- a/src/scenes/door/DoorScene.ts +++ b/src/scenes/door/DoorScene.ts @@ -669,9 +669,14 @@ export class DoorScene extends Phaser.Scene { private retireStaleRules(): void { const live = new Set(activeRules(this.night.state.clockMin).map((r) => r.id)); for (const r of announcedRules(this.night.state.clockMin)) { - if (live.has(r.id) || this.retiredRules.has(r.id)) continue; - this.retiredRules.add(r.id); + if (live.has(r.id)) continue; + // Always re-sync the card: Dazza's texts can LAG the clock (the send + // cooldown queues them), so a rule can be rescinded before its card row + // has even landed. retire() is an idempotent no-op until the row exists, + // and every later text re-runs this sync — the card converges. this.codeCard.retire(r.id); + if (this.retiredRules.has(r.id)) continue; // toast once per rule, though + this.retiredRules.add(r.id); const line = RULE_RETRACTIONS[this.retiredRules.size % RULE_RETRACTIONS.length]!; this.showToast(line.replace('{rule}', r.short)); }