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>
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>
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>
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>
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>
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>
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>
12-task TDD plan in two increments:
- Increment 1 (Tasks 1-5): announce-all-to-both + capture tally.
- Increment 2 (Tasks 6-11): client-local phantom opponent-piece layer.
Each task has exact files, complete code, and verification commands.
Server/shared tasks are TDD'd with vitest; client tasks use svelte-check
plus manual verification (no client test harness, by design).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three features requested by Andrew Freiberg (physical-game player) and
refined by Seth, bringing digital blind chess closer to the physical
table:
1. Moderator announces every move and attempted move to both players
(widen announcement audience to 'both'; suppress bot retry churn).
2. Running capture tally (server-derived per-viewer protocol field).
3. Phantom opponent pieces — a client-local, drag-and-drop opponent-model
overlay, blind mode only, never sent to the server.
Spec only; no implementation. Phased: F1+F2 then F3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A second, LAN-only deploy alongside the CT 690 / chess.sethpc.xyz
instance. Runs on VDJ-RIG as a persistent systemd daemon, served on
port 80 and reachable at http://chess.local via an mDNS alias.
- blind-chess-local.service: server unit; binds port 80 as the
non-root blindchess user via CAP_NET_BIND_SERVICE.
- chess-mdns-alias{,.service}: publishes the chess.local mDNS name
with avahi-publish -a -R (-R skips the reverse PTR, which would
otherwise collide with the host's own <hostname>.local record).
- install-local.sh: idempotent root-side installer (Node 22 via
NodeSource, avahi-utils, blindchess user, /opt/blind-chess, units).
- CLAUDE.md: documents the local instance under Operations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase line gains the 2026-04-29 fix; test count 75→78; observability gap
narrowed to note `[bot resign]` is now grep-able while metrics/tracing
remain deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-play transcripts produced by `pnpm selfplay --transcripts` land in
`tmp/selfplay-runs/<timestamp>/` — operator scratch, not part of source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- CLAUDE.md: phase line moved to "Phase 1 deployed"; key files lists
the new bot module, game-end extraction, and selfplay harness.
- DECISIONS.md: new "Phase 1 implementation outcomes" subsection records
the CasualBrain-engine reversal, the FEN-vanilla-only invariant, why
blind keeps heuristic, and the bot-slot token randomization. The
earlier "Stockfish deferred" entry is partially superseded.
- .claude/handoffs/: handoff document for the next session.
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.
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>
- 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
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>
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>
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>
- DECISIONS.md: in-game chat (player↔player and human↔Gemma) deferred
indefinitely. Blind-mode chat is a side channel that defeats the
moderator-vocabulary security boundary; chat with Gemma leaks belief
state mid-game. Resolvable but expensive — revisit only on demand.
- Spec: same deferral noted in "Out of scope".
- New plan: docs/superpowers/plans/2026-04-28-ai-player-phase-1-casual.md
— 13 tasks, 80 sub-steps. Phase 1 only (Casual bot end-to-end). Phase 2
(Recon) gets its own plan once Phase 1 outcomes inform Recon's target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates CLAUDE.md "Current State" + "Key files" to point at the new spec.
Adds DECISIONS.md "AI / computer player" section (11 settled decisions).
Strikes through the prior "Client-side AI / hint generation — out of scope"
row with a "partially superseded" note: the reversal applies only to the
human-vs-AI path. Adds 7 new Deferred/Rejected rows for AI-feature scope.
Handoff at .claude/handoffs/2026-04-28-170713-ai-player-spec.md captures
session state for the next pickup (writing-plans → Phase 1 implementation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-phase plan: Casual bot first (algorithmic, ~200 LoC), then Recon bot
(gemma4:26b chat agent) with persistent private chat history per game.
Bots play through the same view filter and FSM as humans — no oracle access.
Endpoint priority: steel141 RTX 3090 Ti primary, pve197 V100 fallback. Mid-game
GPU failover allowed (one-way). Reasoning hidden from user during play, revealed
in collapsible post-game panel.
Reverses the 2026-04-28 DECISIONS.md row "Client-side AI / hint generation —
explicitly out of scope." Reversal is partial: human-vs-human hint generation
remains rejected; this spec only adds AI in the human-vs-AI path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>