carlok — zsh — 88×30

cat _posts/2026-11-03-red-ink-sprint-26-a-second-pass-through-the-six-bouts-wasd-by-physical-key-and-a-save-that-admits-when-it-failed.md

red-ink sprint 26: a second pass through the six bouts, WASD by physical key, and a save that admits when it failed

Once all six bouts are won, red-ink’s title card offers “Second pass: bout N”: the same six fighters at the player’s own difficulty, each with one change — Reyes iron jaw at 1.4× health, Dag quick hands with reaction 0.8× and cooldown 0.85×, Pike and Voss heavier hands at 1.1× and 1.25× damage, Chen relentless at 0.8× cooldown, Halvorsen iron jaw at 1.06× health — with the HUD reading “BOUT n / 6 · II”, wins counting only in progress.pass2, and the save moving to version 2 with a migrate step that adds the field (e2a32bf). Measured with the new balance-run --pass2, 200 bot meets on Normal and seed 1: the first pass is identical at 90.5 / 67.5 / 44.0 / 36.0 / 30.0 / 22.0 while the second reads 71.5 / 52.0 / 16.5 / 16.0 / 12.0 / 11.5, and the plan’s Hard multipliers had to be dropped — Hard alone takes the bot to 0 against Pike and Halvorsen — as did block and dodge changes, which did not move the bot at all.

Movement now follows the physical key: event.code KeyW/KeyA/KeyS/KeyD moves the fighter whatever the caps are labelled, so AZERTY and Dvorak get WASD under the fingers, a key typed W from the Z position or a comma on Dvorak no longer moves, and the hints say ZQSD where Chromium’s getLayoutMap reports other caps — jab, heavy, block and dodge stay by typed letter, and none of it was tried on a real AZERTY board (15399bf). Four flow fixes came with it: a Continue action on the title card for a player with one to five wins (0fa1e82), Enter standing aside when focus is on any other button, link or field (38e2472), a win card that says the win will be lost when storage throws instead of dropping it in silence (8da0c81), and a pause-menu restart that starts at full health rather than healing to the damaged value plus 35 (a831652).

The save carries a version and a migrateProgress step now, so a sprint 25 save loads intact and is stamped on the next write, broken JSON gives the defaults, and a save from a newer build is kept exactly as it is (d18fa99); a pasted link gets a real preview, with a description, theme colour, SVG favicon and a 1200×630 assets/social.jpg, and the twitter:card tag was dropped the next day because the target is Instagram and it reads Open Graph (c01fbba, 5f37b30). The repo also got an AGENTS.md of the rules a background agent must not break, and dependency care: grouped weekly Dependabot for npm and Actions with Babylon and esbuild deliberately ignored, a non-blocking npm audit workflow, and every action pinned to the commit SHA it resolves to (cef5d59, a618e39). One CI run failed on a test that read fighter state two frames after a key press, which is longer than the whole heavy windup on a slow runner, so it polls up to 40 frames instead; the sprint closes at 53 of 53 with the bout table and step probe unchanged (fee32d1, 5d73833, #20).