Commit Graph

40 Commits

Author SHA1 Message Date
claude (blind_chess) c01244c850 fix: promotion dialog only fires for genuine pawn promotions
The "Promote pawn" dialog popped for any pawn "moved" toward the last
rank: the commit paths checked piece type + destination rank but never
the pawn's SOURCE rank. With the phantom layer now filling ranks 7-8
with tappable phantom pieces, tapping one (which falls through to the
real-move handler) while a real pawn was armed triggered the dialog for
a move no pawn could make — and for any phantom type, not just pawns.

Root cause: incomplete promotion detection, duplicated in Game.svelte
`onCommit` and the server's `isPromotionRequired`. Replaced with one
shared `isPromotionMove(piece, from, to)` — pawn, from the rank adjacent
to promotion, to the promotion rank, at most one file over — used by
both. 7 unit tests in packages/shared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:45:42 -04:00
claude (blind_chess) d10e581243 fix(client): light outline on dark phantom glyphs for panel contrast
Opponent (black) phantom pieces rendered near-invisible on the dark
--panel background in the palette and Captures panel, and the black
drag-ghost was low-contrast over dark areas. Black piece glyphs in
those three spots now get a light text-shadow outline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:22:50 -04:00
claude (blind_chess) 82a69d8812 fix(client): key phantom-load effect on gameId, gate the drag ghost
Re-key the phantom-load effect on `loadedFor` (tracks gameId) so it
reloads if the same <Game> instance is reused for a different game
without a remount. Also gate the drag-ghost block behind
`phantomLayerEnabled` for consistency with all other phantom UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:50:13 -04:00
claude (blind_chess) 313837eb21 feat(client): wire the phantom opponent-model layer into the game view
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:45:40 -04:00
claude (blind_chess) 816f89be36 feat(client): phantom-piece palette component
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:41:43 -04:00
claude (blind_chess) c65db03cfa chore(client): suppress phantom-span a11y warning with documented svelte-ignore
Phantom pieces are a pointer-only overlay; adding role/tabindex would create
a focusable element with no keyboard equivalent (worse a11y, not better).
The real game remains fully keyboard-operable via the square <button>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:39:37 -04:00
claude (blind_chess) 599dc17f44 feat(client): render and drag phantom pieces on the board
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:35:58 -04:00
claude (blind_chess) 4b3e587f6c fix(client): handle pointercancel and make drag-start idempotent
Add onCancel handler for browser/OS gesture takeover (scroll, palm
rejection) that previously leaked window listeners and left drag stuck.
Extract detach() helper called by onUp, onCancel, and start() for
idempotency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:32:20 -04:00
claude (blind_chess) f52f7dbb8f feat(client): pointer-event drag controller for the phantom layer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:28:37 -04:00
claude (blind_chess) bd98315fe3 fix(client): guard phantom-store mutations against unset game and no-op move
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:27:25 -04:00
claude (blind_chess) 0583984723 feat(client): local-only phantom-layer store
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:24:13 -04:00
claude (blind_chess) 2ae2c8013c test(shared): cover null-valued entry in deserializePhantoms
Adds a null-valued entry under a valid square key (d3) to the
'keeps valid entries and drops invalid ones' fixture, proving the
typeof/null guard branch in deserializePhantoms is exercised.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:23:17 -04:00
claude (blind_chess) a574100e25 feat(shared): pure phantom-model helpers (seed positions, deserialize)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:20:46 -04:00
claude (blind_chess) 783d85a40c feat(client): capture-tally panel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:16:18 -04:00
claude (blind_chess) 3169995d7f refactor(server): type captureTally accumulators as PieceTally
Tighten the local byYou/byOpponent accumulators from the wider
Record<string, number> to PieceTally, matching the return type's fields
and preventing silent invalid-key writes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:14:26 -04:00
claude (blind_chess) ce36755a89 feat(server): per-viewer capture tally on joined and update messages
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:09:26 -04:00
claude (blind_chess) 0498f1de43 feat(client): label attempted-move announcements by player
Attempted-move lines (no_such_piece, no_legal_moves, wont_help, illegal_move)
now show "White — " or "Black — " prefix derived from ply parity. Removed
alarm-red err styling; replaced with neutral dim+italic via .entry.attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:04:49 -04:00
claude (blind_chess) 5282237027 refactor(bot): hoist the rejection announcement to a single local
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:03:37 -04:00
claude (blind_chess) 558891ed37 feat(bot): suppress bot retry-search churn from the moderator log
Pop intermediate wont_help/illegal_move/no_such_piece/no_legal_moves
announcements produced during the bot's decision cycle before any
broadcast reaches the human opponent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:00:25 -04:00
claude (blind_chess) 76717cf52e docs(server): correct translateMove audience docs after the 'both' change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:58:48 -04:00
claude (blind_chess) 41b3ab93bb feat(server): moderator announces every move and attempt to both players
All move-event announcements in translator.ts and all attempted-move
announcements in commit.ts now use audience 'both' so the moderator
panel is a complete shared transcript for both players.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:54:34 -04:00
claude (blind_chess) dc7f8adcdf fix(bot): blind Casual no longer resigns prematurely under check
The blind-mode CasualBrain heuristic ignored the moderator's
'<own>_in_check' announcement and scored moves on capture/advance/development
signals uncorrelated with check resolution. chess.js rejected every
non-resolving attempt, BotDriver's RETRY_CAP=5 fired, and the bot resigned.
100-game blind self-play: 100% resignations at avg ply 26.

Fix:
- CasualBrain.detectOwnCheck() scans newAnnouncements for the own-color
  in_check tag; when set, heuristicPick() applies a +5000 boost to king
  moves so they're tried first. Information stays within the public
  moderator vocabulary — no oracle access, view-filter invariant intact.
- RETRY_CAP raised 5 -> 25. Vanilla never hits the cap (chess.js verbose
  moves are guaranteed legal); blind needs more budget to find a legal
  move through pseudo-legal candidates.
- BotDriver.botResign() now logs '[bot resign]' with gameId/color/mode/ply/
  reason/detail. Previously silent — operator had no signal in journald.

Verification (100-game blind Casual-vs-Casual self-play):
- avgPly: 26 -> 90 (3.5x)
- Resignations: 100% -> 17%
- Checkmates: 0% -> 42%
- Threefold draws: 0% -> 41%

Vanilla regression check (80 games combined): 0 resigns either way,
strength unchanged (Casual still wins 98% vs random).

78 tests pass (was 75; +2 new check-resolution tests, +1 retry-cap test
updated to match new cap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 05:56:02 -04:00
claude (blind_chess) 7c18725586 feat(bot): vanilla CasualBrain delegates to js-chess-engine
The hand-rolled scoring heuristic lost to a random-move baseline 7-7 in
self-play — far below the spec's >=80% acceptance bar. Swap in a real
chess engine (js-chess-engine, MIT, ~400KB, no native deps) for vanilla
mode at level 2 with randomness=30 to break threefold cycles.

- BrainInput.fen added; driver populates it ONLY in vanilla mode.
  Blind mode omits the FEN so the engine path can't smuggle opponent
  positions past the view filter.
- CasualBrain in vanilla: convert FEN -> EngineGame -> ai({level: 2});
  validate the engine's move is in legalCandidates; fall back to
  heuristic on miss.
- Blind mode unchanged (engine isn't useful when only own pieces are
  visible — that's Phase 2 Recon's territory).

Self-play vs RandomBrain (100 games each direction, vanilla):
  - Casual(W) vs Random(B): W=97%
  - Random(W) vs Casual(B): B=96%
Casual-vs-Casual vanilla balanced, ~5-30ms/move. All 54 tests still pass.

Refresh .secrets.baseline (stale) to allow new pnpm-lock.yaml hashes.
2026-04-28 15:14:12 -04:00
claude (blind_chess) 06bd144f7c feat(client): AI badge and bot-moving turn indicator
Track aiOpponent in game store; show a pill badge in the topbar for AI
games, update turn label to "<Brain> is moving…" on the bot's turn,
and suppress the disconnected-opponent banner when the opponent is a bot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:26:25 -04:00
claude (blind_chess) 31f68db654 feat(client): two-section landing — friend vs Casual bot 2026-04-28 14:24:06 -04:00
claude (blind_chess) cb8e017792 fix(bot): wire aiOpponent into joined and update server messages 2026-04-28 14:22:41 -04:00
claude (blind_chess) 73d5d0cb93 test(bot): integration tests for Casual vs human 2026-04-28 14:21:27 -04:00
claude (blind_chess) 88bc23b0d0 fix(bot): harden ws.ts integration seam
- maybeAbandon Promise no longer floats from setTimeout
- broadcastSinceLast loses dead extra parameter
- bot-slot token is randomized so a third party can't hijack the
  bot's color by guessing a fixed placeholder
2026-04-28 14:17:46 -04:00
claude (blind_chess) a9660c0694 feat(bot): pokeBot + broadcastSinceLast hooks into ws.ts handlers
Replace broadcastNewAnnouncements/broadcastUpdate with watermark-based
broadcastSinceLast; add pokeBot helper; make all state-mutating handlers
async; hook pokeBot after every mutation so the CasualBrain fires on
each turn without oracle access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:13:24 -04:00
claude (blind_chess) 58e1fc5bd8 feat(bot): POST /api/games instantiates CasualBrain + BotDriver 2026-04-28 14:10:19 -04:00
claude (blind_chess) 9a837ec319 feat(bot): vsAi/aiOpponent protocol fields and bot-driver registry 2026-04-28 14:07:01 -04:00
claude (blind_chess) 4407110147 fix(bot): finalize game on bot checkmate; harden driver dispatch
Extract endGame/finalizeIfEnded to game-end.ts so driver.ts can call
finalizeIfEnded after an applied move (fix: bot checkmate was not
setting game.status='finished'). Wrap entire dispatch() call in
try/catch for exception safety. Move lastSeenAnnouncementCount advance
to after successful dispatch so retry attempts see FSM rejection
announcements. Add checkmate-finalize test; lock retry-cap at 5 calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:04:22 -04:00
claude (blind_chess) 3798b9c00d feat(bot): BotDriver with mutex, retry cap, and dispatch
Wires Brain to Game: init/onStateChange/dispose lifecycle, in-flight
mutex, 5-attempt retry loop with attemptHistory, resign-on-cap. Also
adds Game.aiOpponent? field to state.ts for Task 5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:56:28 -04:00
claude (blind_chess) ebd1463b0a docs(bot): clarify when scoreMove early-return fires 2026-04-28 13:52:22 -04:00
claude (blind_chess) aa7bc30ee1 feat(bot): CasualBrain with capture/development/center heuristics 2026-04-28 13:48:34 -04:00
claude (blind_chess) f48e0a9cdf feat(bot): legalCandidates for vanilla and blind modes 2026-04-28 13:42:37 -04:00
claude (blind_chess) bc954f4748 feat(bot): scaffold Brain interface and types 2026-04-28 13:38:16 -04:00
claude (blind_chess) a878dee0d9 fix(client): wrap connect/disconnect in untrack() to break effect loop
Svelte 5 $effect tracks every $state read inside its body. The
lifecycle effect that calls game.connect(gameId) implicitly read
state.ws (inside connect()) and then wrote to it, producing an
effect_update_depth_exceeded loop. Symptom in production: the
browser opened ~12 WS connections/sec, none completed the upgrade
handshake, and the lobby flow appeared stuck on 'waiting for
opponent' (the opponent's WS never stabilized long enough for the
server to send 'joined'). untrack() opts the call out of dep tracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:32:29 -04:00
claude (blind_chess) 80c4b8fc50 fix(client): rename stores/game.ts → game.svelte.ts so Svelte 5 runes compile
Svelte 5 runes ($state, $derived, $effect) only run through the
compiler in .svelte and .svelte.ts/js files. A plain .ts file leaves
$state(...) as a literal call at runtime, causing
"ReferenceError: $state is not defined" and a blank page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:22:28 -04:00
claude (blind_chess) a6de43edc1 feat: implement and deploy blind_chess MVP
- pnpm workspace: shared/server/client packages
- Server: Fastify+ws, chess.js, FSM (touch-move + hierarchy),
  per-player view filter, zod validation, rate limiting, grace-window
  disconnect handling
- Client: Svelte 5 + Vite, click-to-move board, moderator panel,
  promotion/draw dialogs
- Shared: protocol types, ModeratorText enum, geometricMoves helper
  (provably zero opponent-info leak)
- 43 tests pass (21 shared, 22 server incl. 4 real-WS integration)
- Deploy: CT 690 on node-241 (192.168.0.245), systemd-managed,
  Caddy block for chess.sethpc.xyz
- Live at https://chess.sethpc.xyz

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:20:18 -04:00