not-tonight/LANEHANDOVER.md
type-two e0b5831166 LANE-DOOR: land the adversarial review findings
The important one: the dress-code card was highlighting every rule a patron
broke the moment they stepped up, so optimal play was reading a red line off the
desk. That deleted the inspection loop the whole scene exists for. The highlight
now fires after the ruling, as feedback.

Second: tonight's date appeared nowhere in the game, so working out whether an ID
was underage — the tell that files a heat strike and costs the licence — was
guesswork. It is now on the card beside the DOB and under the HUD clock, which
finally makes the boundary cases idCheck.ts was written for reachable.

Also:
- determinism: the partner's generatePatron call sat behind an enqueue-success
  check, and enqueue fails at maxQueue, which is player-controlled — so from the
  first queue overflow the rest of the night's patrons diverged. Same class as
  the comeback-stream bug, fixed for couples and missed for patrons.
- the sobriety modal's backdrop wasn't interactive; you could rule straight
  through it
- tooltips read "navy a blazer" — articles baked into the noun tables, then
  colour-prefixed. They are the only detection channel for four rules under
  placeholder art, so they have to read cleanly.
- bumbag and chain are drawn on the body but were described only in the head zone
- NightScene.endNight pushed heat strikes past core/meters.ts
- dropped the diff's one `as never` for a real type guard (CCR-4)
- replaced a vocabulary-coverage test whose string-equality heuristic silently
  stopped working once a legitimate word matched its own type id

231 tests. Balance findings (vibe pins at 100, stalling is dominant, the slam
never materialises) are measured and written up in LANEHANDOVER.md but not
applied — that is a whole-economy retune and it wants a human playtest, not a
bot with perfect information.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:06:31 +10:00

21 KiB
Raw Permalink Blame History

LANE HANDOVER LOG

Coordination file between lanes (Opus 4.8 execution sessions) and the reviewer (Fable). Append-only — never edit or delete previous blocks. Newest block at the BOTTOM. Push after every session so the reviewer can see state remotely.

Protocol

  1. Before starting work: read this whole file + your lane file in lanes/ + docs/CONTRACTS.md. Check for a REVIEW block addressed to your lane — it overrides your lane file where they conflict.
  2. Work only inside your lane's owned directories (see BUILD_PLAN.md §2).
  3. End of session: append a SESSION block (template below), commit, push your lane/<name> branch.
  4. Never merge to main — the reviewer does that.
  5. Blocked on a contract ambiguity? Don't stall and don't silently change the contract: stub locally with // TODO(contract):, log a CONTRACT CHANGE REQUEST below, keep moving.

SESSION block template

---
### SESSION — <LANE-NAME> — <date> <time>
**Branch/commits:** lane/<name> @ <short-sha>..<short-sha>
**Done:** bullet list of what actually works now (be honest — "renders but untested" ≠ "done")
**How to see it:** exact command + what to click (e.g. `npm run dev` → press D → deny 3 patrons)
**Tests:** X passing / Y failing (name the failures)
**Broke / known-wonky:** anything you left imperfect
**Contract change requests:** none | numbered list with justification
**Questions for reviewer:** none | numbered list
**Next session should:** bullet list

REVIEW block template (reviewer only)

---
### REVIEW — Fable — <date>
**Reviewed:** <lanes/commits>
**Merged to main:** <yes/no + sha>
**Contract decisions:** rulings on any open change requests
**Instructions for <LANE>:** what to do next / what to fix first

(log starts below)


SESSION — LANE-0 (executed by Fable, reviewer) — 2026-07-19 16:30

Branch/commits: main (reviewer executed Phase 0 directly; no lane branch) Done:

  • Vite + strict-TS + Phaser 3.90 + Vitest + ESLint scaffold; Math.random banned by lint rule.
  • All CONTRACTS.md types verbatim in src/data/types.ts.
  • core/: SeededRNG (mulberry32, named streams), typed EventBus (snapshot-emit semantics), GameClock (real-ms driven, ageOn birthday math incl. leap-day), Meters (single-writer, hype multiplies positive vibe only, clicker drift), StubBeatClock (128 BPM), versioned corrupt-safe localStorage save.
  • data/: outfit vocabulary (+PALETTE), 9 archetypes, 10 contraband items, 17 Dazza texts (8 rule-bearing with ruleIds).
  • patrons/: generator (fake-ID tell system, DOBs clustered near the 18 boundary, late-arrival drunk drift), pure dollPlan + Phaser renderDoll (3 poses, drunk tells baked), regulars' memory store.
  • rules/drunk.ts: drunkStage thresholds.
  • ParadeScene demo: rainy street, kebab-shop + venue dressing, 24-walker parade with ID cards rendered from the same data, SPACE reseed / D toggle cards.
  • 49 tests green; lint/build/test gate clean. How to see it: npm run devhttp://localhost:5199 (in the Claude preview pane, drive frames per CLAUDE.md "Dev notes"). Watch for an almost18 label walking under an ID card claiming 18+ — that's the whole game in one image. Tests: 49 passing / 0 failing. Broke / known-wonky:
  • ID cards overlap when the footpath is crowded — cosmetic, parade-only.
  • window.game debug handle exposed in main.ts (deliberate, for pane-driving).
  • Spec deviations: doll renderer split into dollPlan.ts (pure) + doll.ts (Phaser) — better than spec'd, plans are pixel-testable without a GPU; added a 5th fake-ID tell (expired real card, no boolean tells); no MainMenu scene (Boot → Parade direct) — menu is Phase 2 shell work. Contract change requests: none (contracts implemented as written). Questions for reviewer: n/a (self-reviewed). Next session should: Phase 1 lanes are OPEN — see REVIEW block below.

REVIEW — Fable — 2026-07-19

Reviewed: Phase 0 (self-executed; verified via test gate + in-browser parade). Merged to main: yes (direct). Contract decisions: CONTRACTS.md now frozen. Extension requests via this file. Instructions for Phase 1 (LANE-DOOR, LANE-FLOOR, LANE-JUICE):

  1. Branch from current main into lane/door / lane/floor / lane/juice (worktrees if sharing a machine). Read your lane file's new "Phase-0 reality" section FIRST — it corrects the original spec where the landed code differs.
  2. LANE-DOOR is the critical path; if only one Opus session is available, run DOOR first, JUICE second, FLOOR third.
  3. Every session ends with the gate + a SESSION block here + a push. Fable reviews on request ("review the lanes").

SESSION — LANE-DOOR — 2026-07-19 18:50

Branch/commits: lane/door @ 9805ff8..(this commit) — branched from main after Phase 0, worked in worktree ../not-tonight-door.

Done (all verified by playing it, not just by tests):

  • Full Door-only night is playable start→summary. npm run dev boots straight into 9:00 PM, patrons arrive on a curve, you work the rope, rule on them, Dazza texts land, meters move, 3:00 AM → summary. Both fail states reachable.
  • Rules engine (src/rules/, pure, no Phaser):
    • dressCode.ts — 8 rules. text/activeFrom are READ OUT of data/strings/dazza.ts by ruleId rather than retyped, and the module throws at load if a rule and its announcing text ever disagree, so the card can't drift from the message.
    • idCheck.ts — all the ID date maths (idVerdict, idDisplayLines). Builds on ageOn, does not reimplement it. Catches the 5th tell (expired-but-otherwise-real).
    • judge.ts — the single scoring function + JUDGE_TUNING (every magic number named).
    • sobriety.ts — challenge content + the drunk-performance model.
    • doorTypes.ts — this lane's contract EXTENSIONS (see change requests below).
  • Scene layer (src/scenes/door/): NightScene (thin night shell — clock, meters, deferred consequences, fail states, marked TODO(integration)), DoorScene (tableau + desk + all input), QueueManager + arrivalCurve (pure, no Phaser), PatronUpView (×3 doll, 5 hover zones), IdCardView (tray thumbnail + half-screen zoom), PhoneWidget (buzz, preview, scrollback thread), DressCodeCard (NEW badges), SobrietyModal, StampFx (marked TODO(juice)), NightSummaryScene, inspect.ts, dazzaSchedule.ts, ui.ts.
  • Tests: 49 → 229 (+180). Every judge branch, every ID boundary (18th birthday tonight, leap-day DOB, expiry today vs tomorrow), every dress-code predicate, the queue economy, the Dazza schedule, and the inspection vocabulary.

How to see it: npm run devhttp://localhost:5199. Click the rope (or NEXT!) → patron steps up ×3 → hover their shoes/torso/face for tooltips → click the ID in the tray to zoom it → ADMIT or NOT TONIGHT. Stamp someone to see the signature interaction. Wait ~30 in-game min for Dazza's first rule text. To fail on purpose: deny everyone (aggro riots by ~10:30 PM), or admit everyone (three heat strikes, licence gone at 3 AM). In the Claude preview pane, drive frames per CLAUDE.md "Dev notes" — document.hidden freezes the Phaser loop at frame 0 there. Drive a whole run inside ONE javascript_exec call: splitting the frame stepping across calls hands Phaser a multi-second delta and its tweens/timers land in a state that looks like a game bug but isn't. Cost me an hour.

Tests: 229 passing / 0 failing.

Bugs found by playing it that the test gate could never have caught (all fixed):

  1. Game opened on ~45 real seconds of empty street — the 9 PM arrival rate was honest and unplayable. Raised the curve, capped the max gap, and the QueueManager now seeds one patron at construction so somebody is always at the rope on the first frame.
  2. noLogos fired on 47.6% of patrons, but dollPlan.ts only draws a logo for the TOP slot — ~28% of all patrons were deniable for a logo that was never on screen. Narrowed to visibly-rendered slots (see CCR-3). Dazza's own text says "no visible logos".
  3. One patron could be ruled up to 11 times. Game state was being mutated inside the stamp's tween callback; if the tween didn't land, the patron stayed at a rope the player could keep scoring. All state now settles synchronously in rule() and the tweens only move pixels. This class of bug is why busy also has a token-guarded watchdog.
  4. A correctly-played night still rioted at 1:37 AM. Aggro relief only fired on a completely empty street, which never happens during the slam. Relief now scales with queue slack — the pressure is "keep it moving", not "never deny".
  5. Admit-everything logged 40 heat strikes (one per patron over capacity). CONTRACTS.md §3 says heatStrikes is "run-scoped, max 3"; now enforced, and a persistent breach is one write-up rather than one per patron.
  6. One stubborn regular reappeared 6 times and ate a third of the night's denials. Comebacks capped at 2.
  7. Dazza sent the identical lap text 5 times in a row. Seven phrasings now, picked by the same roll that sets the delay.
  8. Determinism leak: denying a regular drew from the arrivals stream, so one player decision reshuffled every remaining arrival time. Moved to its own comebacks stream (CONTRACTS.md §6 exists precisely to stop this).
  9. ID card modal covered the patron, making photo-vs-face impossible. Moved right. The genuine-hologram reference swatch now sits on the same cream backing as the card — a translucent teal reads as a different colour over dark laminate than over card stock, so the reference was lying.
  10. Phone preview text spilled off the screen onto the desk; strike reasons on the summary ran off the right edge of the canvas. Both clamped.

Broke / known-wonky:

  • src/main.ts is the one file I touched outside my lane — 8 lines, to register the three door scenes and boot into Night instead of Parade. Unavoidable for the "npm run dev → playable night" deliverable. Boot/Parade are still registered; game.scene.start('Parade') from the console still works. Flagging it explicitly for the merge.
  • src/data/strings/door.ts is a NEW file in data/strings/ — CONTRACTS.md §7 says Aussie strings live there, and it adds no types, but data/ is nominally frozen-ish. Move it if you'd rather.
  • With placeholder art, noThongsSinglets / noWhiteSneakers / noBlazers are not distinguishable by silhouette (a thong and a boot are both a 3×4 rect). They ARE readable via the hover tooltips, which is why I built the inspection surface before the stamp. It costs the player time, which costs aggro — that tradeoff is fine, arguably good. It stops being a compromise at the v0.4 art pass (CCR-3).
  • Vibe pins at 100 for a competent player by ~midnight, so late-night vibe stops being a live pressure. Aggro and heat still are. Worth a tuning pass in Phase 2; I did not want to over-tune against a bot that plays with perfect information and zero deliberation time. A human is much slower and will not see these numbers.
  • NightScene renders nothing and DoorScene draws the HUD. Deliberate, but it means the HUD moves to wherever Phase 2 puts the shell.
  • No audio (LANE-JUICE). StubBeatClock runs and is consumed for nothing yet — no rhythm flourish landed; the stamp is tween-timed, not beat-timed.

Contract change requests:

  1. CCR-1 — PatronFlags.handHolding?: boolean. Needed by noHandHolders, which is one of the 8 already-written Dazza rule texts and otherwise has no data to predicate on. Implemented as a declare module interface augmentation in src/rules/doorTypes.ts, so src/data/types.ts is byte-identical to main. QueueManager sets it on couples and DoorScene draws a linked-hands pixel between them so the rule is visible, not psychic. Request: fold the field into types.ts properly at integration and delete the augmentation.
  2. CCR-2 — VerdictOutcome.hypeDelta?: number. Design §3.1 gives rope theatre a hype reward but VerdictOutcome has no hype channel, so judge() cannot express the 'wait' verdict's payoff. Same augmentation pattern, in src/rules/judge.ts. Same request.
  3. CCR-3 — renderer request, for LANE-0/art rather than contracts. dollPlan.ts draws a logo rect for the TOP slot only, and gives thongs/boots, blazer/bomber, and sneaker/heel identical geometry. Four dress-code rules are therefore silhouette-invisible and I narrowed noLogos to compensate (LOGO_VISIBLE_SLOTS in dressCode.ts, one constant, widen it when the art lands). Requesting per-slot logo marks and distinct shoe/outer silhouettes in the v0.4 art pass.

Questions for reviewer:

  1. Is the declare module augmentation pattern acceptable as the standing mechanism for additive contract extensions, or do you want every extension to go through you and land in types.ts? It kept the frozen files clean and I'd use it again, but it is a precedent and two other lanes will copy it.
  2. src/main.ts and src/data/strings/door.ts — ruling on ownership, please. If either is off-limits I'll move the strings into scenes/door/ and hand you a patch for main.ts.
  3. Should heat strikes end the night immediately at 3, or accumulate to the summary as they do now? Design §2 says "3 strikes = licence pulled, run over" — I read that as run-scoped, so the night still finishes and the summary declares it. Say the word and it becomes a third fail state.
  4. Vibe pinning at 100 (above) — worth retuning now, or leave it until there is a real playtester rather than a bot?

Next session should:

  • Take the review findings from the four-lens pass (lane discipline, scene lifecycle, playability, balance) — I ran it but landed only what I could verify in-session.
  • Swap StampFx for ui/Stamp + SFX once LANE-JUICE lands; hook a beat flourish to beat:tick (currently consumed by nothing).
  • Guest-list clipboard and the UV pass-out stamp are specced in design §3.1 and NOT built — they were the right things to cut to get a full playable night.
  • Sobriety line-walk (the third challenge) is still a stretch item; alphabet + riddle ship.
  • Phase 2 integration should lift clock/meters/fail-state ownership out of NightScene into the real night-flow machine and replace the temporary summary.

SESSION — LANE-DOOR (review-fix pass) — 2026-07-19 19:05

Branch/commits: lane/door @ 30162d3..(this commit)

Ran a four-lens adversarial review over the lane (discipline / scene lifecycle / playability / balance) and landed the findings I could verify in-session.

Fixed:

  1. The game was answering its own question. DoorScene lit up every rule a patron broke on the dress-code card the moment they stepped up. Optimal play was "read the red line", which deletes the inspection loop the entire scene exists for — all 8 dress-code rules were free. The highlight now fires AFTER the ruling, for ~2s, as feedback. This is the single most important fix in this pass.
  2. Tonight's date appeared nowhere in the game. The card showed 04/12/2000 and the HUD showed 11:42 PM; the player was asked to subtract a birthdate from a date they were never given. underage is the tell that files a heat strike and costs the licence, and it was pure guesswork. Now stamped on the card beside the DOB and under the HUD clock. Every boundary case idCheck.ts was carefully written for (turnsEighteenTonight, leap-day DOB) is finally reachable by a player.
  3. Determinism, again, worse than the one I already fixed. In spawnArrival the partner's generatePatron call sat behind if (enqueued), and enqueue fails at maxQueue — which is player-controlled. From the first overflow onward every remaining patron of the night differed. All draws are now unconditional. Same class as the comeback-stream bug; I fixed it for couples and left it in patrons.
  4. You could rule straight through the open sobriety modal. Its backdrop was not interactive, so ADMIT/NOT TONIGHT stayed live underneath. Now eats clicks.
  5. Tooltips read as broken textnavy a blazer, white a singlet, cream a bucket hat. Articles were baked into the noun tables and then colour-prefixed. Since the tooltips are the ONLY detection channel for four rules under placeholder art, the one system the player has to trust looked broken. Nouns are bare now.
  6. bumbag and chain are drawn on the torso/waist but were described only in the head zone — hovering the visible item told you about a jacket. Both body zones now name them.
  7. NightScene.endNight pushed to state.heatStrikes directly, bypassing core/meters.ts fifty lines after correctly emitting heat:strike. Now emits. CONTRACTS.md §4.
  8. Dropped the one as never in the diff (DoorScene's dazza:text handler) for an honest type guard — see CCR-4.
  9. Test quality: tests/inspect.test.ts proved its vocabulary coverage by string-comparing output against the raw type id, which silently stopped working once a legitimate word ('skirt', 'cap') matched its own id. Replaced with an exact hasWordFor check.

Tests: 231 passing / 0 failing (+2). Re-verified in-browser after the changes: competent play still runs to 3:00 AM; deny-everything still riots (aggro 100 @ 10:42 PM); one patron can still only be ruled once, plus up to 2 regular comebacks.

New contract change request: 4. CCR-4 — dazza:text payload should carry the lane's rule type. EventMap types it rule?: DressCodeRule, so a rule this lane emits comes back without the short card label the lane added to it. Currently narrowed with a runtime type guard in DoorScene. Either widen the payload or accept the guard.

Reviewer findings I did NOT land, with reasons:

  • The economy is slack and dawdling is dominant. Measured over 5 seeds: a competent night pours 7491115 raw vibe into 50 points of headroom (≈94% discarded by the clamp), vibe pins at 100 by ~10:00 PM and stays there 8186% of the night, and aggro peaks at 5/100 because relief refunds ~10:1. hype is a pure ratchet — nothing in src/ ever emits a negative hype delta — so stalling is paid twice (vibe multiplier + cash bonus) and a player who plays slower earns 24% more at identical accuracy. Recommended: denyViolatorVibe 6→1, admitCleanVibe 3→1, QUEUE_TUNING.calmPerSec 0.9→0.25. I did not apply these: it is a whole-economy retune, the measurements come from a bot with perfect information and zero deliberation time, and I would rather you tune it against a human than have me chase a number I can't feel. Flagging it as the top item for the next pass.
  • The queue never actually backs up. Player throughput ≈0.83 arrivals/in-game-min against a curve that peaks at 0.42 — the "10:3012:30 slam" is currently fiction (measured avg queue 0.10.2, max 35 against 8 visible slots). Fixing it means roughly doubling the curve to ~170 arrivals a night, which is a big feel change; wants a human playtest first.
  • noSongRequesters (activeFrom 240) and noHandHolders (280) are nearly decorative — they arrive after 84% and 93% of the night's patrons have been served, and fire ~1.3 and ~0 times respectively. Their fromMin values come from data/strings/dazza.ts, which is not mine to edit. Suggest 240→~105 and 280→~135.
  • noHandHolders is still unfair even when it fires. The linked-hands pixel only draws between adjacent QUEUE sprites; once a patron steps up to the rope there is no partner drawn and no tooltip mentions it, so at the moment of judgement the tell is invisible. CCR-1's justification ("rendered as a linked-hands pixel ... so the rule stays one the player can SEE") overclaims. Either draw the partner at the rope, add it to describeZone, or cut the rule.
  • Intoxication and contraband are worth zero points. judge() reads neither. Denying a maggot is scored as denying a clean punter (+7 aggro), and a pat-down that finds a baggie changes nothing. The drunk channel is the most legible signal the game has — sway tween, bloodshot pixels, a whole sobriety minigame — and it does not score. This is the biggest design gap in the lane and I'd want your ruling on the intended numbers before inventing them.
  • The clicker is inert. clickerShown is read only for a text colour and one summary row; judge uses the true insideCount for capacity. The brief calls the divergence "the game", so it needs a consequence — presumably the inspector reads the clicker, not reality, which is Phase-2 inspector work.
  • Minor, unlanded: the ID modal's shade dims the desk hologram swatch but not the card's (the teal/magenta hue gap carries it today, but a subtler fake colour would break); peelingLaminate is cream-on-cream and near-invisible; photoMismatch degrades to a 1/6 coin flip for hatted patrons; three player-facing strings are still inline in judge.ts against CONTRACTS.md §7, one of which ("Good story for them one day") reads as narrator rather than the game's voice.