Skip to content

fix(m3): planet-1's summoned platform arms on arrival, not on drop - #40

Merged
ksdisch merged 3 commits into
mainfrom
fix/m3-platform-arm-on-arrival
Aug 13, 2026
Merged

fix(m3): planet-1's summoned platform arms on arrival, not on drop#40
ksdisch merged 3 commits into
mainfrom
fix/m3-platform-arm-on-arrival

Conversation

@ksdisch

@ksdisch ksdisch commented Aug 13, 2026

Copy link
Copy Markdown
Owner

The finding

Three AI-pilot co-op playtests (harness in the pi fork at .pi/playtest/, zero-diff on this repo) never once crossed planet-1's pit at x=660–880. The corrected death table (pilot 3, 2026-08-12) pinned the cause precisely:

  • The sentry band (x≈80–560) is solved — pre-committed "armed" moves cross it in ~2.26s including driver reaction against a 3.0s freeze, 0.74s of slack, confirmed empirically twice.
  • The blocker was the platform's earn-vs-life ratio: ~4.9s to earn (4835–4869ms, n=5), 5.0s of life counted from the drop, at a fixed x=770 — and the pit sits immediately after the band. So the pair had to hold a 3.0s freeze and a 5.0s platform open simultaneously, from two puzzles with a ~7× solve-time spread (freeze 672–676ms, n=5).
  • No pair ever managed the overlap. Every death clustered at the pit lip (x≈659, 665); one armed move landed 0.2s after the platform expired.

The change

One lever, exactly the one the report recommended — the platform's life now starts on arrival, not on drop:

  • summonPlatform() no longer schedules the fade at drop time. It stamps lifetimeMs on the sprite and the platform holds indefinitely.
  • The existing astronaut↔platforms collider doubles as the "reached it" signal, calling a new armPlatform() that starts the same 5000ms (8000ms boosted) fade/destroy on first contact and no-ops thereafter via an armed sprite-data flag.
  • Lifetime rides on the sprite rather than a scene field, so a scene restart can't strand a countdown.

Deliberately untouched: the freeze timer, the patrol band, the drop x, the platform's earn cost, the PLATFORM HOLDS! re-cast branch, and the pit geometry. No protocol, relay, dependency, or puzzle-difficulty changes.

Both powers stay mandatory — the pit is still uncrossable without a platform, the band still unrunnable without a freeze. The pit becomes a coordination problem instead of a stopwatch problem: the phone banks the platform first (~4.9s), then earns the freeze (~0.7s), instead of racing them.

Verification

Full gate green: typecheck · typecheck:tests · 204 Vitest · build · smoke:relay.

Live headless runs at ?solo=1&test=1 via the pinned Playwright MCP:

Assertion Result
Untouched platform holds past the old 5s life platformCount === 1 at t = 0.3s, 3s, 6s, 9s, 12s (old code: destroyed at 5s)
Untouched platform doesn't arm on a near miss Astronaut fell past it into the pit → respawnCount 1, platformCount still 1
Clear with the freeze earned ~12s after the platform won: true, 0 respawns, completed['planet-1'], planet-2 unlocked
Platform still expires ~5s after contact (life re-anchored, not removed) Destroyed 5000ms after a contact falling inside the observed 2962–3684ms on-platform window (5431ms measured from centre-enters-span)

Also in this PR

Docs that stated drop-time expiry are corrected (docs/AUTONOMY.md sharp-edges + playbook step 3, /verify-planet's re-cast note), and the phone's cast copy follows the behaviour ("bridge waits, then holds 5s once he steps on").

The two secondary playtest findings are recorded, not fixed — both are real level/UX work beyond this lever, so they land as new ## Open backlog items:

  • The level punishes standing still — deaths land in the gaps between commands while the pair coordinates by chat. The pit half is fixed here; the sentry-band half remains.
  • The support seat is blind to world state — three pilots independently reported it; it caused death misattribution (a death at x≈377 drew "cast Illuminate?", 570px away from the dark zone at x=920).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TerCorG6J23DL2YCZ1nUqh

ksdisch and others added 3 commits August 13, 2026 11:02
Three AI-pilot co-op playtests never once crossed planet-1's pit at
x=660-880. The corrected death table pinned the cause: the platform cost
~4.9s to earn (4835-4869ms, n=5) and lived 5.0s from the moment it
dropped, and the pit sits immediately after the sentry band -- so the
pair had to hold a 3.0s freeze and a 5.0s platform open SIMULTANEOUSLY
from two puzzles with a ~7x solve-time spread (freeze 672-676ms, n=5).
No pair managed the overlap; every death clustered at the pit lip
(x~659, 665), one landing 0.2s after the platform expired.

The lever the report identified is the platform's earn-vs-life ratio, so
that is the only thing this touches. summonPlatform() no longer schedules
the fade at drop time -- it stamps lifetimeMs on the sprite and the
platform holds indefinitely. The astronaut/platforms collider doubles as
the "reached it" signal, calling a new armPlatform() that starts the same
5000ms (8000ms boosted) fade/destroy on FIRST CONTACT and no-ops
thereafter via an `armed` sprite-data flag. Lifetime rides on the sprite
rather than a scene field so a scene restart cannot strand a countdown.

The freeze timer, patrol band, drop x, earn cost, PLATFORM HOLDS! re-cast
branch and pit geometry are untouched, and both powers stay mandatory:
the pit is still uncrossable without a platform, the band still
unrunnable without a freeze. The pit becomes a coordination problem
instead of a stopwatch problem -- the phone banks the platform first,
then earns the freeze.

Phone copy follows the behaviour; AUTONOMY.md and /verify-planet
re-document the sharp edge as arm-on-arrival. BACKLOG records this plus
the two secondary playtest findings (the level punishes standing still;
the support seat is blind to world state) as new Open items.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TerCorG6J23DL2YCZ1nUqh
Adversarial review F1. The previous commit armed the countdown from the
astronaut/platforms collider callback, which Phaser fires on any resolved
contact on any face -- not on "the astronaut reached it". Two consequences,
both re-introducing the stopwatch failure the change exists to remove:

- Planet-1's jump is a 78px gap (ground right edge 640 -> platform spans
  722-818) onto a 14px band. An undershoot clips the platform's LEFT face
  on the way into the pit, burning the whole bridge on a platform nobody
  stood on. The astronaut respawns at spawn.x needing a fresh freeze to
  re-cross the sentry band and cannot return in time.
- Planet-3 makes it structural, not an edge case: the pit is degenerate
  (continuous ground) and platformDrop (730,470) puts a knee-high ledge
  spanning y 463-477 in the path of an astronaut whose body spans y
  452-500. A horizontal bump at ground level is its ONLY possible first
  contact, so the flourish armed itself the moment you walked into it.

armPlatform() now gates on a landing: arcade separation parks a landed
astronaut's feet on the platform's top face, so body.bottom <= top + 2px
identifies a landing and rejects both a side clip and an underside bonk,
independent of sprite dimensions.

Second prong: while a platform was armed, the PLATFORM HOLDS! branch
swallowed the phone's re-cast behind a banner claiming the bridge was
fine -- so a burnt platform cost the pair a whole ~4.9s TapSequence solve
for nothing. The banner now fires only for an UNARMED platform (which
genuinely cannot be improved on); a re-cast onto an armed one calls the
new refreshPlatform(), cancelling the pending expiry and any fade in
flight and returning it to the waiting state. An astronaut still standing
on it re-arms next frame with a full new lifetime.

Verified headlessly at ?solo=1&test=1: planet-3 sustained side-bump held
the platform 8s+ (pre-fix it died at 5s); a planet-1 undershoot into the
pit respawned with platformCount still 1; a re-cast 4s into the countdown
extended life to 5043ms past the re-cast instead of dying at 5s; and
planet-1 cleared 3/3 with 0 respawns and the platform banked 12s early.
Gate: typecheck x2, 204 Vitest, build, smoke:relay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TerCorG6J23DL2YCZ1nUqh
Adversarial review F5, a consequence of the landing gate in d4f5a3c. Once
a never-mounted platform persists, planet-3's placement becomes a
permanent lethal pin.

Planet-3's ground is continuous, so its summoned platform is a knee-high
ledge ON the walking path rather than a bridge over a pit: walking right
into it BLOCKS the astronaut instead of landing it. Committed geometry at
the old drop x=730 -- curtain x 576..664 (planet3.ts:45-48), platform
span 682..778 (96px texture), astronaut body 32x48 -- left an 18px gap
for a 32px body. A blocked astronaut sat at left=650, i.e. 14px inside
the curtain, and died the instant the 2500ms Phase Dash window closed.
Before the landing gate the side bump armed the platform and it faded
after 5s, so the trap cleared itself; after it, nothing retires an
un-mounted platform (the phone's re-cast hits the !armed PLATFORM HOLDS!
branch and resetAstronaut never touches the group), so it repeated on
every attempt for the rest of the scene.

Moving the drop 730 -> 750 is the root fix -- the constraint window is
narrow but real:

  blocked astronaut left = (x - 48) - 32  >= curtain right 664  -> x >= 744
  platform right         =  x + 48        <= dark-zone left 805  -> x <= 757

750 sits mid-window: blocked left 670 (6px clear of the curtain), right
edge 798 (7px clear of the dark zone). The residual "un-mounted ledge
persists" behaviour is now benign -- stepping on it arms and retires it,
and it is 37px tall on continuous ground, so it is an obstacle, never a
trap. Three colocated planet3 tests pin the clearance so it cannot
regress silently (207 tests, was 204); planet-1 and planet-2 drop into
open pits, where none of this applies.

BACKLOG's Done note is corrected where this branch falsified it: the
trigger is a landing (not first contact), the PLATFORM HOLDS! branch was
reworked rather than untouched, and the drop x did move on planet-3.

Verified headlessly at ?solo=1&test=1: pinned against the ledge at x=686
with phaseActive false for 9s -> 0 deaths (pre-fix: guaranteed loop).
Gate: typecheck x2, 207 Vitest, build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TerCorG6J23DL2YCZ1nUqh
@ksdisch

ksdisch commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review — CLEAR TO MERGE (rounds: 3)

# Severity (final) Finding Disposition
F1 should-fix Armed on any contact, not on arrival — a side clip into the pit burned the bridge, and PLATFORM HOLDS! then swallowed the phone's ~4.9s re-cast Fixed in d4f5a3c, verified
F5 should-fix planet-3: a never-mounted ledge became permanent and pinned the astronaut 14px inside the plasma curtain — a repeating death loop Fixed in a888417, verified
F6 nice-to-have Docs describing the old trigger BACKLOG.md half fixed in a888417 (verified); AUTONOMY/verify-planet half → follow-up
F2 nice-to-have /verify-planet §3 still says "cast at the pit" while its own rules section says cast early Follow-up
F3 nice-to-have HANDOFF.md / Decisions.md not updated for this branch Follow-up
F4 nice-to-have Phone copy promises "5s"; lasting-platform makes it 8s. Also "he" → "they" Follow-up
F7 nice-to-have At x=750 the ledge reaches planet-3's always-solid hidden-platform collider, mildly eroding Illuminate's perceptual gate Follow-up (accept-and-document — the clean clearance rule is unsatisfiable inside the window F5 forces)
F8 nice-to-have The "blocked, not landed" test omits PLATFORM_LANDING_EPSILON and has no upper bound Follow-up

Waived by Kyle: none — no waiver was requested or granted; both blocking findings were fixed and re-verified.

Follow-ups (nice-to-have): F6 docs/AUTONOMY.md:138,154 + .claude/commands/verify-planet.md:29-32 still say "first contact" (highest value — it misleads a headless driver on planet-3, where a side bump no longer arms the platform); F2 verify-planet §3 wording; F3 HANDOFF.md + a Decisions.md row for the lifetime-semantics reversal; F4 phone copy 5s/8s + pronoun; F7 document planet-3's third placement constraint; F8 tighten the blocked-not-landed assertion.

Coverage: complete — all three rounds reported complete coverage; no bound left unclosed.

Two rounds of fixes came out of this: the original commit armed the countdown from the collider callback, which Phaser fires on any face, and the fix for that then made a never-mounted platform permanent — which planet-3's geometry turned lethal. Both were caught here, not in play.

Zero-context reviewer + neutral judge on disputes (no disputes were raised); anchored at a888417.

@ksdisch
ksdisch merged commit 8318f82 into main Aug 13, 2026
1 check passed
@ksdisch
ksdisch deleted the fix/m3-platform-arm-on-arrival branch August 13, 2026 16:44
ksdisch added a commit that referenced this pull request Aug 13, 2026
Nice-to-have findings from the adversarial review of PR #40, deliberately
not fixed in-loop. Review record:
~/.claude/reviews/constellation/2026-08-13-fix-m3-platform-arm-on-arrival.md

- F6: docs/AUTONOMY.md (playbook step 3 + the sharp edge) and
  /verify-planet's operating rules said the platform arms on "first
  contact". It arms on the first LANDING — a side clip does not arm it,
  which matters most on planet-3, where the ledge sits on continuous
  ground and walking into it never starts the countdown. As written it
  told a headless driver to wait for platformCount to reach 0 forever.
- F2: /verify-planet step 3 still said to cast summon-platform "at the
  pit", contradicting its own rules 20 lines up. Mirrors AUTONOMY step 3.
- F3: HANDOFF.md was dated 2026-07-26 and claimed two remaining Open
  BACKLOG items (now four); refreshed the date, standing, next move and
  recently-touched files. Decisions.md gains D9 for the drop-time →
  landing-time lifetime reversal.
- F4: the phone's platform copy promised "holds 5s" while the
  lasting-platform talent makes it 8000ms. Dropped the number rather than
  threading the boosted flag through FEEDBACK; "he" → "they".
- F7: documented the fourth placement constraint on planet-3's
  platformDrop — the hidden platform's collider is solid always, so the
  ledge can bump it — and recorded that clean clearance is unsatisfiable
  inside the 744..757 window F5 forces. Accepted, not solved.
- F8: the "blocked, not landed" test asserted GROUND_SURFACE_Y >
  platformTop, looser than the runtime gate; it now asserts against
  PLATFORM_LANDING_EPSILON and gains an upper bound so a ledge raised
  above head height fails too.

Gates: typecheck · typecheck:tests · vitest 207/207 · build.


Claude-Session: https://claude.ai/code/session_01YLr4UWKBbzmSaMxi1vbkdR

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ksdisch

ksdisch commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Correction (edited). The first version of this comment claimed the run
confirmed "the pit is uncrossable without a platform" and referred to "nineteen
crossings". Our own adversarial review caught both: every trial ran with a
bridge standing, so the no-platform counterfactual was never tested, and there
were zero crossings in the whole run. The corrected, narrower version follows.


A behavioural cross-check on this PR's pit gate, from the pi fork's playtest
harness: 68 scripted trials driving the real game at 604a422 through a
headless two-client harness, sweeping the jump point across the usable take-off
band.

planet1.test.ts already asserts the gate analytically — the surviving ground
tiles are 256px edge-to-edge against a ~245px running jump, so the platform is
required. This does not re-prove that, and it does not test the counterfactual:
a bridge was summoned before every trial (platformCount=1, 68 of 68), so no
no-platform run exists in the data. The freeze half of "both powers stay
mandatory" is likewise untested — every trial also cast Freeze Stars.

What the run does show, on trajectories where the platform is not a confound:

  • Nothing reached the far ledge, in any trial. Zero of 68.
  • 19 trials took a ground-launched jump that never contacted the bridge at
    all
    (take-offs x=608–644, no bridge contact recorded) and none of them
    spanned 640→896. On those 19 the platform is irrelevant to the outcome, so
    they are direct behavioural evidence for the same property the unit test
    derives geometrically.
  • 19 further trials did land on the bridge (touchdown x=784–820) and then ran
    off its far edge — a harness artifact, not a game one: our /move keeps
    holding the direction key after touchdown where a player would release it.

Two incidental observations, both offered as notes rather than defects:

  • The corridor ceiling (400x20 at (420,360), spanning x 220..620) is a second,
    undocumented bound on the jump. A take-off left of roughly x=564..580 clips
    it: apex y comes back 394 instead of 362, and the astronaut drops in well
    short of the bridge. (The range, not a point: the harness reads take-off as
    the last sampled position still at ground height, which under-reports by up to
    16px.) Combined with the fall edge at x=656, that leaves a strip of roughly
    75-90px from which a crossing can be started at all. Generous for a sighted
    player — it just is not written down anywhere.
  • armPlatform's landing gate behaves as described here, and the 2px epsilon is
    doing real work: a jump that flies over the bridge grazes its top face and
    arms it. Correct by the rule as written (the feet did touch the top), but it
    means a near-miss overhead can start the countdown on a bridge nobody crossed.

No action requested. Full method, per-trial data and caveats:
.pi/playtest/AIM-SWEEP-2026-08-13.md in ksdisch/pi.

This comment is AI-generated from a scripted playtest run in the pi fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant