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.pngsprite; repositionedx=1795→645, next to the seconddancePartnerwaltz; 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.