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>
Deployed 2026-05-18 to CT 690 (chess.sethpc.xyz) and chess.local
(VDJ-RIG); both verified serving the new client build. CLAUDE.md and
the handoff updated to deployed state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DECISIONS.md: new "Table-fidelity features" section + deferred items
(smart-tracker rejected, highlight/phantom coupling deferred,
abandoned-game localStorage cleanup deferred).
- CLAUDE.md: current state, test count 78->87, key files, known gaps.
- spec: record that the driver unit test covers the bot-suppression
path in place of the considered-and-dropped ai-game-casual integration
test (resolves a spec/implementation drift the final review flagged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tasks 8/10/11 received review fixes during execution; the plan's code
blocks are updated to match what shipped:
- Task 8: drag controller handles pointercancel + idempotent start.
- Task 10: palette pieces are plain spans + svelte-ignore (no
focusable-but-not-operable role/tabindex).
- Task 11: phantom-load effect keyed on gameId; drag ghost gated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>