Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Playtest Findings

Playtest findings log

Started 2026-08-08 (first live TV playtest session covering the Upperworld/ rickroll build). Distinct from NEXT.md (forward-looking plan) — this is a log of things actually found by playing, human eyes on the running game, not design proposals. Each entry gets a stable ID so later work (a fix, a harness check) can reference it without re-describing it.

Fields: Status is one of open / dispatched (an agent/task is on it) / fixed (merged) / wontfix (decided not to address, with why). Harness tracks whether this finding’s class of bug has been turned into a durable automated check yet (see the planned mechanics-audit harness) — means not checkable by a test (visual/feel issues), todo means checkable but not yet written, done means a real test exists.

Open

PF-7 — soccerShoes/danceShoes are single-chance, latent risk if ever hardened

Found: 2026-08-08, mechanics audit (MECHANICS-AUDIT-2026-08.md). What: Unlike Tailored Clothes, soccerShoes/danceShoes exist only in their _s2.ts files with no Season 1 twin — no season-pair redundancy. Today both are soft gates (missed score/bonus only, never added to moveSolids), so not currently broken. But if either is ever escalated to a hard block the way the dress-code guard was, it would be a worse, completely unmitigated softlock — no accidental jump-bypass exists for these, because nothing currently needs one. Severity: Low today, flagged as a landmine for future changes. Status: open, informational — no fix needed unless/until either gate becomes a hard block. Harness: todo — same “hard-gate reachability” check the mechanics-audit harness should encode generally would also re-check this if either item’s gating logic ever changes.

Confirmed good (not a bug, don’t touch)

PF-23 — Confirmed good: the Clouds wind gust requires exactly one speed-tier boost to cross

Found: 2026-08-08, live TV playtest, final pre-demo pass on Clouds S2. Torsten’s own words: “The wind gust needs exactly one flash (gained by a cola). without it its impossible to reach the end of the game… I just needed to fly back and collect a cola. Noice.” What: Not a bug — confirmed by design. clouds_s2.ts’s own windZones comment already spells out the exact math: the gust’s pushX=-80 is pinned to exactly cancel RUN_BASE=80 (player.ts) at speedTier=0, so holding run alone only hovers in place — real forward progress needs at least one River Cola’s speed-tier bonus first. Reaching the level’s end without ever collecting a single cola is intentionally impossible, not an oversight; recorded here so it doesn’t get “balanced” away later by someone unaware the wind’s exact strength was tuned specifically to require this. Severity: N/A — positive finding. Status: confirmed-good, no action. Harness: — (the exact push/speed math is already documented inline in clouds_s2.ts’s own comment; a dedicated automated “0 speedTier can’t cross, 1+ can” check could be added later if desired, not required now).

PF-11 — Confirmed good: Soccer Shoes placement + a nearby speed pickup after respawn

Found: 2026-08-08, live TV playtest, Soccer S2. What: Not a bug — a positive confirmation worth recording so it doesn’t get accidentally “fixed” later by someone unaware it was deliberate and liked. Soccer Shoes sits on a platform, and there’s a cola (speed-tier) pickup positioned close enough to help recover pace right after a fatal respawn resets speedTier to 0. Player’s own words: “I like it.” Severity: N/A — positive finding. Status: confirmed-good, no action. Harness: —.

Fixed

PF-22 — Ejected player’s spawn point overlapped a waltzing dancer’s sway

Found: 2026-08-08, live TV playtest, immediately after PF-20’s portal re-entry fix made this observable at all (“player lands just beneath the door and the next dancer who happens to be placed there is gently pushing him into the door again” — noted first as charming, kept for one test (test_ejected_player_gets_waltzed_back_into_the_portal), then reconsidered as a real spawn-point bug: a permanent spawn shouldn’t depend on a moving hazard’s phase). Root cause: exitUnderworld()’s default eject spot (trigger.x - player.width - 4 = 631) sat 1px inside the x=600 dancePartner waltz item’s sway peak (~632) — a genuine takeHit() knockback (not a solid-body push; dancePartner is never in moveSolids) that landed the player right back inside the (now re-enterable, PF-20) portal trigger on a recurring ~2.73s cycle, for as long as the player stood still. Fixed 2026-08-08: PortalDef gained exitSide?: 'left' | 'right' (default 'left', unchanged for every other portal); Tanzstunde’s portal sets exitSide: 'right', landing the player at x=665 — clear of every nearby dancer, with 3px to spare before the x=680 GAPS entry (falling into it hits FALL_OUT_Y, player.ts, and respawns at spawnX=20). That safe strip is only ~17px wide total, tight but real. Harness: done — smoke_test_season2.py gained test_ejected_player_stays_put_not_pushed_into_gap_or_portal (replacing the earlier “keep it” test), asserting an idle ejected player neither gets swept back into underworld_tanzstunde nor falls into the gap across several would-be waltz cycles; test_tanzstunde_portal_reenterable_after_eject updated to walk left (not right) to re-enter, matching the new eject side.

PF-19 — Dance session timeout had no visible countdown or timeout FX

Found: 2026-08-08, manual playtest check (“dance session timeout in Underworld ejects to Tanzstunde… Seems to work. Not clear if the timer from Tanzstunde is counting or if the lesson gets its own counter. Also, there is no visual indication of a timer running out.”). Fixed 2026-08-08: PF-18’s DANCE_SESSION_TIMEOUT_S/sessionElapsed is its own dedicated clock (never shared with the outer level’s levelTime, which is snapshotted/ restored around the Underworld visit by enterUnderworld()/exitUnderworld() unchanged), but it had zero on-screen readout and abandonDanceSequence() ejected the instant it fired, with no message or sound. Added: a live “Xs” countdown (centered under the beat bar in the dance overlay, red under 10s, mirroring hud.ts’s own level-timer convention), and a DANCE_TIMEOUT_HOLD_S (0.8s) hold on timeout that flashes “TIME’S UP!” (reusing the existing danceFeedbackText mechanism) and plays the time_out SFX before the eject actually happens. Purely additive — the eject itself, and the fact that a timeout grants no reward, are unchanged. See PF-20 for a separate, pre-existing portal-re-entry bug this session’s live TV validation surfaced. Harness: done — __ps33dDebug gained danceFeedbackText/ danceTimeoutHold; smoke_test_season2.py’s test_dance_session_timeout_ejects_to_previous_room now also asserts the “TIME’S UP!” hold fires (while still state:'dancing') before the eject to 'playing', with a real ~0.8s gap between the two.

PF-20 — Tanzstunde/Clouds portals were permanently one-shot per playthrough

Found: 2026-08-08, live TV playtest while validating PF-19 (“Portal can not be reentered”). What: PortalDef.used (level.ts) is a single shared one-shot gate, set true the instant a portal is first entered and never reset except by resetAllPortals() on a full “quit to title” (PF-17). That lock is load-bearing for Soccer/Lidl Lunch’s Underworld chest portals (a real one-off item, farmable without it), but Tanzstunde’s and Clouds’ dance portals got the same lock even though PF-18/PF-19’s design intent was always a repeatable portal — their reward is already capped a different way (danceSequenceStep’s per-step cost, recallSucceeded’s own idempotency guard, DANCE_SESSION_TIMEOUT_S). exitUnderworld() never touched .used on the way out, so once ejected (via the exit ladder or PF-19’s timeout), the physical portal was dead for the rest of the playthrough - confirmed pre-existing (unrelated to PF-19’s changes; exitUnderworld()‘s body is untouched by that fix) but surfaced only now. Fixed 2026-08-08: PortalDef gained oneShot?: boolean (default true, preserving Soccer/Lidl Lunch’s lock); Tanzstunde’s and Clouds’ portal defs set oneShot: false; exitUnderworld() resets used = false on exit whenever the portal it’s restoring isn’t one-shot. Harness: done — smoke_test_season2.py gained test_tanzstunde_portal_reenterable_after_eject, which walks in, times out via ?danceTimeoutS=, walks back into the same trigger, and asserts it re-enters underworld_tanzstunde a second time in the same playthrough.

PF-21 — DANCE_SESSION_TIMEOUT_S (60s) was too tight to actually learn the combo

Found: 2026-08-08, live TV playtest immediately after PF-19/PF-20 (“I managed to learn the sequence in 10 seconds. Had to start immediately after the intro. So 10 seconds is [too] tough.” — the 60s real default left too little margin for a cold-start attempt with no prior familiarity, especially once the demo phase’s own runtime eats into the same budget). Fixed 2026-08-08: raised 60 → 120, then — once PF-20’s portal re-entry fix prompted the realization that this timer’s original anti-farm job may not even be needed anymore if re-entry alone isn’t the exploit — set to a deliberately generous 5-minute stopgap (DANCE_SESSION_TIMEOUT_S = 300) so it’s effectively out of the way while the actual crediting design gets rethought for Season 3 (see MECHANICS-SEASON3-PLAN.md). Not a final value. Harness: n/a (a tuning constant, not new behavior) - the existing ?danceTimeoutS= override keeps the automated timeout test fast regardless of the real default’s value.

PF-18 — Tanzstunde teach + Upperworld recall were both farmable for free

Found: 2026-08-08, live TV playtest (“Both Tanzstunde training and recall can be used to farm credits 100 on each move”). Fixed and merged 2026-08-08 (commit 5b48320): danceSequenceStep() (player.ts) used to ADD energy and count toward itemsCollected (computeScore’s flat +100/item weight) - since teach loops on any miss until a clean pass (PF-3) and recall’s portal is re-enterable, both were strictly profitable to repeat with zero downside. Now costs 5 energy and grants a flat 10 danceCredits (tracked separately from itemsCollected), and a new DANCE_SESSION_TIMEOUT_S (60s real, overridable via ?danceTimeoutS= for tests) caps how long any one visit can run - on timeout, abandonDanceSequence() ejects the player back to the room before the Underworld/Upperworld via exitUnderworld() (same as a normal portal exit, not a respawn inside the same sub-level), doubling as an escape hatch for a stuck/AFK player rather than a pure penalty. Harness: done — 3 vitest cases (player.test.ts) pin the energy-cost/ credit math; smoke_test_season2.py extends the existing correct-inputs dance test to assert the exact economy (100 energy → 80, 0 → 40 credits after a clean 4-step pass) and adds a dedicated timeout/escape-hatch test.

PF-17 — Portals never reopened for a second playthrough on the same page load

Found: 2026-08-08, live TV playtest (“Portals should reset after each game. Now no portals open for the second game regardless of chosen player”). Fixed and merged 2026-08-08 (commit 5b48320): PortalDef.used lives on the LEVELS/LEVELS2 module-level LevelData singletons - loadLevelData() never clones level itself, only items - so marking a portal used (the existing “can’t be farmed” gate within one playthrough) silently persisted for the rest of the page session, including into a second playthrough via “End Current Game” + a new player choice. This is a different bug from the Season 1/Season 2 PortalDef aliasing fix (see “Season 1 vs. Season 2” in MECHANICS.md) - that one stops the two seasons from sharing one object; this one is about the same season’s own object never resetting across a new game. Fixed: quitToTitle() now calls a new resetAllPortals(), clearing used on every portal across both LEVELS and LEVELS2. Harness: done — smoke_test_season2.py’s anyPortalUsed debug flag verifies a portal marked used gets cleared by the real OSD “End Current Game” button, without needing to script a full physical replay back to it.

PF-16 — kn00t ambushes were one-shot forever instead of a recurring threat

Found: 2026-08-08, live TV playtest request (“should be recurring… after distance/time… only from season 2 onwards”). Fixed and merged 2026-08-08 (task #16, commit ffbfcff): distance+time re-arm (AMBUSH_REARM_DISTANCE_PX=200, AMBUSH_REARM_TIME_S=5, both evaluated live, latching only once both clear simultaneously) replacing the permanent collected=true. Real geometry backing the thresholds: Lidl Lunch’s ambush is 449px from spawn (fastest sprint back ~3.2s, comfortably under the time gate), Soccer’s is 1524px. Verified empirically: near-spawn respawn still doesn’t chain-death (the original 2026-08-07 softlock stays fixed), and a real hit-respawn-wait-return cycle does re-trigger. Harness: todo — same shape as PF-4/PF-7’s reachability checks.

PF-1/PF-2/PF-8 — New portals were unmarked and crammed right before their boss

Found: 2026-08-08, live TV playtest, Tanzstunde S2 + Clouds S2. Fixed and merged 2026-08-08 (tasks #17/#18, commits d6ea0b5/d17be01):

  • Tanzstunde teach portal: new backstage_door.png sprite; repositioned x=1795→645, next to the second dancePartner waltz; made a genuine optional detour (requires a deliberate jump onto an elevated platform, matching Soccer’s own portal precedent) rather than an unmarked mandatory walk-through.
  • Clouds Upperworld portal: new drawSkyPortal() visual (pulsing glow + orbiting sparkles, no existing sprite fit a mid-air portal); repositioned to the world ceiling near spawn (x=30,y=0), reachable only via sustained flight, fully clear of the boss and all plane patrols. Deliberately kept separate from the wind zone (PF-6) rather than integrating them. Harness: — (visual/discoverability, not a checkable invariant) for the markers; the reachability-shape question (PF-8) folds into the same harness item as PF-4/PF-7.

PF-6 — Clouds’ wind-gust gauntlet was imperceptible, mechanically and visually

Found: 2026-08-08, live TV playtest, Clouds S2. Fixed and merged 2026-08-08 (task #18, commit d17be01): push -35→-80, pinned exactly to RUN_BASE=80 (walking fully overpowered, a bare run exactly cancels, only real speed-tier build-up buys forward progress). Zone widened/moved (x=1550,w=250, was x=1700,w=120), leaving a real buffer before the boss. Telegraph now renders 140px before entry (was zero warning) and switched from white (measured to blend into Clouds’ actual sky background almost exactly) to amber for real hue contrast. Harness: done — the wind-magnitude/telegraph assertions in smoke_test_season2.py now check the real numbers, not just “ran without error.”

PF-9 — Minigolf pit-4/crawlspace geometry overlap

Found: 2026-08-08, mechanics audit (MECHANICS-AUDIT-2026-08.md). Fixed and merged 2026-08-08 (task #20, commit b1c0f70): TUNNEL_FLOOR_W 140→122, stopping the crawlspace floor exactly at the bridging crate’s edge instead of 18px into pit 4’s own heal fill-rect. Verified via a real A/B: reproduced the exact bug on the unfixed code first (a scripted idle through the heal window produced a genuine 21px y-snap), then confirmed it’s gone after the fix. Harness: —.

PF-5 — Only one kn00t cameo (Lidl Lunch freezer) actually landed as a joke

Found: 2026-08-08, live TV playtest feedback, confirmed via a full human playthrough (“the cameos hurt as expected”). Fixed and merged 2026-08-08 (task #19, commit 6108088), per CAMEO-COMEDY-2026-08.md’s root-cause finding (both weak cameos secretly wore a different level’s own future boss portrait): Minigolf swapped to the never-before-rendered kn00t_1 “camo” and hidden in a sand-trap pit (hiddenUntilHazard:5); Soccer swapped to kn00t_3 “pirate”, position unchanged. Added a dedicated ambush_reveal SFX (dissonant stab + descending cackle) layered on top of the existing hurt, not replacing it. Also folded in (same commit): chest resize 10×8→20×16, since it was previously the smallest item in the game despite being the rarest/highest-value pickup. Harness: — (comedy quality isn’t testable; the sprite/placement change itself is covered by existing ambush smoke-test assertions).

PF-3 — Dance lesson (teach mode) didn’t loop until the player succeeded

Found: 2026-08-08, live TV playtest, Tanzstunde S2. Fixed and merged 2026-08-08 (task #14, commit ce74f99): real demo phase (the dancePartner NPC performs DANCE_COMBO once, banner WATCH!), loop the interactive attempt until 1 clean pass, persistent per-move learned checkmarks, and a LEARNED!/RECALLED! fanfare reusing drawGoalFanfare()’s visual language. Re-confirmed post-merge via full regression: an all-miss run loops indefinitely (teachCompleted stays false across 2+ attempt cycles) instead of resolving after one pass; a correctly-timed run resolves after exactly 1 clean pass. Harness: — (UX guarantee, not a static invariant).

PF-10 — Dance overlay’s “->” separator overloaded with real left/right inputs

Found: 2026-08-08, live TV playtest, Clouds S2 recall screen — confirmed as the proximate cause of a real failed attempt (misread as “press right now” instead of “and then”). Fixed and merged 2026-08-08 (folded into task #14, commit ce74f99): removed the arrow separator entirely — teach mode shows [X]/[ ] checkmarks per move, recall’s hidden-label fallback uses a numbered list. Harness: —.

PF-4 — Dress-code guard’s only bypass was accidental and load-bearing

Found: 2026-08-08, live TV playtest + investigation, Tanzstunde S2 — a 4px-margin accident was the only thing standing between the guard and a genuine softlock (later softened by the mechanics audit finding two independent chances at Tailored Clothes, not one — see the commit for full context). Fixed and merged 2026-08-08 (task #13, commit 738cfd9): guard height 54→90 (top y=138→102); from flat ground a standing jump is now 40px short (still uncheesable); from the Dance Shoes platform, WALK_SPEED fails (18px short) but RUN_BASE clears with 15-26px of real margin — verified via actual scripted jumps from three takeoff points, not just arithmetic. New takeoff_marker.png telegraph (reusing the existing windsock.png pole+flag convention). Sprite redesigned from ladder-looking to a stern bouncer on the original stanchion base (two alternates generated, not wired in). Re-confirmed post-merge: the running jump clears (reached x=874.2), the same jump walking fails safely (grounded at x=808.0, retriable, not a softlock). Harness: todo — “is every hard gate’s required item reachable given one-way progression” is exactly the check the mechanics-audit harness should encode generally, this instance just got a manual fix first.

PF-12 — Only one poisonEgg existed, so the Knight capability barely registered

Found: 2026-08-08, live TV playtest request: “add more poison eggs so the uervel knight gets more obvious.” Exactly one poisonEgg existed in the whole game (Season 1 Lidl Lunch only, x=1520) — the hasKnight-defeats-poisonEgg capability only ever got one chance to land as a felt before/after moment, and never in Season 2 at all despite the Knight persisting across seasons. Fixed and merged 2026-08-08 (task #15, commit c66db76): 3 new poisonEgg patrols added to lidl_lunch_s2.ts — one before KNIGHT_X=482 (the real-danger beat), two after (the “I can walk through this now” payoff, repeated). Net effect in one S2 playthrough: 1 pre-Knight egg, 3 post-Knight eggs (confirmed the original Season 1 egg already carries into S2 automatically, no season-filter removes it). Harness: —.

Wontfix

(none yet)

Note on the final combined-merge verification (2026-08-08)

After merging all six fixes above into main in sequence, one run of the full smoke_test_season2.py suite (15 checks) showed a single transient failure — test_underworld_tanzstunde_correct_inputs_no_errors reported state as 'title' instead of 'playing'. Investigated before trusting it: re-ran the same test in isolation (passed cleanly) and re-ran the full suite again end-to-end (passed cleanly, 15/15). Confirmed as timing contention from many sequential Playwright pages sharing one browser instance under load, not a real regression from the merge — this test’s page.wait_for_timeout() calls have tight-ish margins (documented in its own docstring as “approximate timing… to avoid being flaky on precise ms timing,” which held for the single-test case but not always under full-suite load). Low-priority follow-up if it recurs: loosen this specific test’s timing margins slightly.