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