Fix card/judge disagreement under text lag; session handover

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 06:06:42 +10:00
parent 7b44e01333
commit 22f5f11dde
2 changed files with 77 additions and 2 deletions

View File

@ -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). 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 After those land, the next lanes are content-expansion (encounters, repeat
gating), venue 2, and the v0.4 art pass per docs/ASSETS.md. 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.

View File

@ -669,9 +669,14 @@ export class DoorScene extends Phaser.Scene {
private retireStaleRules(): void { private retireStaleRules(): void {
const live = new Set(activeRules(this.night.state.clockMin).map((r) => r.id)); const live = new Set(activeRules(this.night.state.clockMin).map((r) => r.id));
for (const r of announcedRules(this.night.state.clockMin)) { for (const r of announcedRules(this.night.state.clockMin)) {
if (live.has(r.id) || this.retiredRules.has(r.id)) continue; if (live.has(r.id)) continue;
this.retiredRules.add(r.id); // 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); 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]!; const line = RULE_RETRACTIONS[this.retiredRules.size % RULE_RETRACTIONS.length]!;
this.showToast(line.replace('{rule}', r.short)); this.showToast(line.replace('{rule}', r.short));
} }