Added #/pinball-editor — a temporary drag-to-edit layout board (points,
bumpers/posts, flipper pivot+tip, mirror lock, add/delete nodes,
rubber-band multi-select, live JSON export/autosave). Used it to
redesign the table by hand instead of guessing coordinates, then
rebuilt TABLE in pinball.js from the exported layout:
- Real outlane tubes on both sides (inboard of the outer wall) instead
of a sealed funnel — a ball can miss the flipper and drain, same as
a real table, verified to have no wedge points.
- Flippers same length, mirrored pivots and rest angle; slingshots and
outlane tubes are exact mirrors of each other.
- Everything (outlanes, slings, bumpers, posts, flippers) recentred on
the whole table width — including the shooter lane — rather than
just the playfield-minus-lane, so it isn't visually lopsided.
- Bumper/post sizes back to a consistent set after some experimentation
drifted them apart (15/16/15/12/12 and 7/7/8).
- Physics: heavier gravity (was floaty), stronger slingshot kick (the
rubber wasn't doing anything), eased back down slightly after the
first pass felt too fast, flipper length is now per-flipper.
- The "gravity only goes down" trap-safety test (added last session)
now runs against this hand-edited geometry too and passes clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
A small canvas pinball engine (src/lib/pinball.js): fixed-timestep physics
with 5 substeps, circle-vs-segment collision, two rotating flippers whose
angular velocity drives the ball, 3 pop bumpers, 2 slingshots, a charged
plunger, 3 balls. Lighting all 3 bumpers "catches" a random Pokémon for a
bonus. Ball-search kicks a wedged ball toward centre; three strikes and it
drains. High score in pdx.pinball.
Hidden like "Who's that Pokémon?" — no nav entry; the two games
cross-link from each other's back-nav, or reach it at #/pinball.
Controls: ← / → (also A/L, Z) flippers, hold Space / the Launch button for
the plunger; on touch, tap the table's left or right half.
Rendering: the canvas is sized from its container with a DPR-capped
backing store (crisp on any display). `_frame()` is exposed so the physics
can be driven headlessly. 3 unit tests for the geometry helpers.
The physics are stable and the game loop is complete; feel (flipper
strength, ball speed, table balance) still wants hands-on tuning.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Fuzzy: src/lib/fuzzy.js — editDistance + closeEnough (length-scaled typo
tolerance, 0 for names ≤4 chars). submitGuess() accepts a near-miss, so
"charizrd" counts. Plus inline ghost-text completion: once the typed
prefix matches exactly one Pokémon, the rest of the name shows greyed
after the caret; → (at end), Tab or End accepts it.
Generation filter: a "Pull from" select (All / Gen 1–9, built from the
snapshot's species.generation). Choice persists in pdx.whosThat.gen;
changing it reshuffles the pool (and the completion list) and resets the
streak.
7 new unit tests for fuzzy.js (48 total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
.nav__brand is display:none below 1024px, so "tap the wordmark" was
desktop-only. Add a shared onTapCode() helper (src/lib/tap-code.js) and
put the 7-tap shortcut on the dex feed's game-title (.feed-head__id),
which is visible on every screen size. Keeps the wordmark tap for desktop.
3 tests for onTapCode (count, window reset, unbind).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Guess the Pokémon from its silhouette (official artwork, filter:brightness(0)
until revealed). Streak + best + win% persisted in pdx.whosThat. Three
misses auto-reveal; hints (letter count, then first letter) after wrong
guesses; guess matching is punctuation/space/case-insensitive.
Not in the nav or any link. Reached by the Konami code (↑↑↓↓←→←→BA,
lib/konami.js wired in main.js), seven quick taps on the nav wordmark, or
the bare #/whos-that hash once you know it.
9 new unit tests for the konami matcher (sequence, case-insensitivity,
rewind, partial).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
Gen 2 (GSC) is the one mainline dex that records which Unown letters
you've caught — a 26-byte ordered list right after the Seen flag array
(seen + 0x20). Parse it, map letters to the app's Form Dex slugs
('unown' for A, 'unown-b'…'unown-z'), and tick them via a new
markFormsCaught() on the formTracking store. A garbage/out-of-range list
is ignored rather than guessed at.
The import dialog and result line now surface "· N Unown letters".
Verified: 6 new unit tests (synthetic Gen 2 SRAM → slugs, empty list,
garbage rejection; markFormsCaught behaviour) + a real end-to-end import
of a synthetic Crystal save writing {unown, unown-c, unown-o, unown-z}
to pdx.formTracking.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
31 tests in test/, run in a plain Node env with a tiny localStorage shim
(no jsdom — its Node-22.4 transitive-dep breakage isn't worth working
around for logic tests):
- type-chart: modern effectiveness, dual-type stacking, and the era rules
(no Fairy pre-6, Steel resists Ghost/Dark pre-6, the Gen 1 Ghost→Psychic
and Bug↔Poison quirks).
- damage-calc: nature multipliers, the Gen 3+ stat formula (Garchomp
reference values), STAB/effectiveness/immunity, pre/post-Gen-6 crit.
- savedex: a synthetic Gen 1 SRAM (bitfields + checksum) round-trips;
checksum-mismatch and size guards.
- selection: normalizeSelection v2→v3 migration, per-game isolation,
global favourite, stats vs statsNational.
- pokedex-resolver: prettify, dex resolution against a mini snapshot.
`npm test` wired into CI after format:check.
Also: docker-compose host port 8080 → 4753 (README updated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu