Commit Graph

31 Commits

Author SHA1 Message Date
claude (blind_chess) f8faa11b6d docs: design spec for table-fidelity feature batch
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>
2026-05-18 18:52:00 -04:00
claude (blind_chess) b01f324c3b feat(deploy): local chess.local instance for VDJ-RIG
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>
2026-05-18 18:40:02 -04:00
claude (blind_chess) e75f5fff7b docs: CLAUDE.md current state reflects blind-Casual check fix
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>
2026-04-29 15:41:42 -04:00
claude (blind_chess) 04494fcdee docs: handoff for blind Casual check-resolution fix
Captures session state: root cause, fix, verification numbers (blind 100%
-> 17% resignation, avg ply 26 -> 90), preserved view-filter invariant,
deferred Phase 2 work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 06:05:21 -04:00
claude (blind_chess) f00164ebbb chore: gitignore tmp/ for self-play transcripts
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>
2026-04-29 05:56:14 -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) 1213ec8fb1 docs: handoff reflects final merged state 2026-04-28 15:25:03 -04:00
claude (blind_chess) 1674695eef docs: AI Phase 1 shipped — context, decisions, handoff
- 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.
2026-04-28 15:20:24 -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) dc5e6678b9 feat(bot): self-play harness with Casual and random baselines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 14:52:10 -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) 6d457a2321 docs(plan): defer in-game chat, add Phase 1 (Casual) implementation plan
- 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>
2026-04-28 13:31:12 -04:00
claude (blind_chess) 729199097e docs: log AI-player spec approval, update context, add handoff
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>
2026-04-28 13:12:04 -04:00
claude (blind_chess) 288693fcd6 docs(spec): add AI/computer player design spec
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>
2026-04-28 12:37:48 -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
claude (blind_chess) 9a5ad55f30 chore: initial scaffold — spec, decisions, gitignore 2026-04-28 10:53:26 -04:00