Playable start to finish: 9 PM to 3 AM, patrons arrive on a curve, you work the rope, inspect them, and rule. Dazza texts escalate the dress code through the night, deferred consequences land when you can no longer argue, and the night ends in a summary or one of two fail states. Rules engine (src/rules/, pure, no Phaser): - dressCode: 8 rules whose text/activeFrom are read out of data/strings/dazza.ts by ruleId, with a load-time throw if a rule and its announcing text disagree - idCheck: all ID date maths, built on core/GameClock's ageOn - judge: the single scoring function, every magnitude named in JUDGE_TUNING - sobriety: challenge content and the drunk-performance model Scene layer (src/scenes/door/): NightScene shell, DoorScene, pure QueueManager + arrivalCurve, patron-up inspection view, ID card, phone, dress-code card, sobriety modal, stamp, summary. Tests 49 -> 229. Ten bugs found by playing it rather than by testing it, including one patron being ruled eleven times (state was mutating inside a tween callback; it now settles synchronously), a correctly-played night still rioting at 1:37 AM, and 28% of patrons being deniable for a logo the renderer never drew. Contract change requests CCR-1..3 are in LANEHANDOVER.md. src/data/types.ts, src/core/ and docs/ are byte-identical to main; the two additive extensions are declare-module augmentations. src/main.ts is the one file touched outside the lane (8 lines, to boot into the night) and is flagged for the reviewer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
236 lines
14 KiB
Markdown
236 lines
14 KiB
Markdown
# 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
|
||
|
||
```markdown
|
||
---
|
||
### 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)
|
||
|
||
```markdown
|
||
---
|
||
### 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 dev` → http://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 dev` → http://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.
|