Files
blind_chess/DECISIONS.md
T
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

15 KiB
Raw Blame History

DECISIONS.md — blind_chess Decision Log

Project-specific decisions. For global/cross-cutting decisions, see ~/bin/DECISIONS.md.

Format: YYYY-MM-DD: <decision> — <why>

Architecture

  • 2026-04-28: Node 22 + TypeScript stack — single-language top-to-bottom; chess.js is the de facto rules engine and lives natively here.
  • 2026-04-28: pnpm workspace with three packages — packages/server (Fastify + ws), packages/client (Svelte + Vite), packages/shared (TS types). Shared types are the load-bearing decision: the WS protocol drift surface is high-risk and shared types catch it at compile time.
  • 2026-04-28: Fastify > Express — better TypeScript ergonomics, faster, cleaner plugin model for ws integration.
  • 2026-04-28: Svelte > React — smaller bundle, reactive stores fit the constantly-changing board state model. React is overkill for a 2-route app.
  • 2026-04-28: chess.js for rules + custom geometricMoves helper — chess.js doesn't expose pseudo-legal moves; ~80 LoC pure function covers all six piece types. Lives in packages/shared so server and client use the same code.
  • 2026-04-28: In-memory only; Map<gameId, Game> is the entire database — simplest possible. SQLite later if crash recovery becomes painful. Rejected: SQLite for MVP (premature given hobby-project scope).
  • 2026-04-28: Single-port Node service — Fastify serves both static client and /ws upgrade on port 3000. No reverse proxy logic in our service; Caddy CT 600 handles TLS and routing.
  • 2026-04-28: Deploy target: new LXC on node-241 — clean isolation, matches existing patterns. Behind Caddy CT 600 at chess.sethpc.xyz.
  • 2026-04-28: No auth beyond the hashed game link — friction-minimal; appropriate for casual play. No Authentik gate. Rejected: gating with Authentik (overkill).
  • 2026-04-28: 8-character gameId (32 bits, ^[a-z0-9]{8}$), 24-character PlayerToken (144 bits) — game IDs short enough for hand-shareable links, tokens long enough to prevent guessing.
  • 2026-04-28: WebSocket transport for in-game; REST POST /api/games for creation — keeps create flow simple (refresh-friendly, cacheable), keeps in-game traffic on a single channel.

Implementation

  • 2026-04-28: Both modes (vanilla + blind) shipped day one — single engine, mode = per-player view filter. Vanilla mode is "blind mode with full reveal."
  • 2026-04-28: Moderator hierarchy refined to four tiers: (1) no_such_piece, (2) no_legal_moves = pseudo-legal ∅, (3) wont_help = pseudo-legal ≠∅ but legal ∅ (pin OR unresolved check), (4) silent = legal moves exist. Each tier is information-strictly-monotonic (more info revealed at later tiers).
  • 2026-04-28: Touch-move FSM — tap arms (reversible, client-side only), drag-start or destination-click commits ("touches"). Server tracks armed: { color, from }. no_legal_moves and wont_help checks fire only on first commit with a piece; once committed, all subsequent failed attempts are illegal_move with the touch staying.
  • 2026-04-28: Highlighting (blind+ON) is purely geometric — function of (piece type, position, own-piece set), no opponent input. Rays extend through unseen opponent pieces. Stop at own pieces. Off-board excluded. Zero opponent info leak. (Vanilla+ON shows engine-truth: legal-empty as green dot, legal-capture as red ring.)
  • 2026-04-28: Game creation: creator picks side at create time (default random); single-use link (first joiner takes the open slot, then locked); no spectators in MVP; link dies with the game.
  • 2026-04-28: Reconnect via opaque PlayerToken in browser localStorage, 5-minute grace window — generous for phone hiccups, short enough that abandoned games end. Grace expiry → endReason: 'abandoned', opponent wins. Both-sides simultaneous expiry → game ends with winner: undefined.
  • 2026-04-28: Pawn promotion via modal (Q/R/B/N), client must include promotion field in the move; moderator announces the promotion (it's tactically significant — public info).
  • 2026-04-28: All draws auto-detected (stalemate, insufficient material, threefold, 50-move) — casual-play friendly; no "claim" UI.
  • 2026-04-28: Announcement is an enum (ModeratorText), not a free-form string. Display strings live client-side. Tests assert against enum values.
  • 2026-04-28: update is the single, idempotent server-to-client message that includes a filtered view and any new Announcement entries. Replaying the latest update produces correct render.
  • 2026-04-28: Moderator-vocabulary "errors" (no_such_piece, no_legal_moves, wont_help, illegal_move) come through as Announcement entries on update, NOT as error messages. Errors reserved for protocol failures.
  • 2026-04-28: Janitor prunes finished games after 30 min idle; active games never expire (until restart).
  • 2026-04-28: Rate limiting via per-token bucket on commit: 10/s, burst 20 — well above human pace, well below abuse.
  • 2026-04-28: Mobile-first responsive design — IDEA.md's share-a-link flow strongly implies phone use.
  • 2026-04-28: Logging via Pino (Fastify default) → journald. /api/health for Uptime Kuma probe. No Prometheus/OpenTelemetry in MVP.
  • 2026-04-28: Resign + draw-offer/accept-decline flow — standard chess UX. Resignation ends without grace; disconnect applies grace.
  • 2026-04-28: Game-over screen reveals full board for both sides — post-game review is part of the experience.

Implementation outcomes (2026-04-28 build session)

  • 2026-04-28: Repo: git.sethpc.xyz/Seth/blind_chess. Created via gitea create blind_chess. Default branch main.
  • 2026-04-28: CT: 690 on node-241, hostname blind-chess, IP 192.168.0.245, Debian 12, Node 22.22.2. 2 cores / 512 MB RAM / 8 GB rootfs. Resting memory ~29 MB, plenty of headroom.
  • 2026-04-28: Chosen chess.js v1.4.0 — uses Move.isEnPassant() / isKingsideCastle() / isQueensideCastle() instead of the deprecated flags string. The Move constructor's deprecated flags field is intentionally not relied upon.
  • 2026-04-28: Half-move clock for the 50-move rule is read from FEN field 4 (chess.js doesn't expose it directly). See translator.ts:halfMoveClock.
  • 2026-04-28: Shared package import resolutionpackages/shared/package.json main and exports point to ./dist/. Source .ts is dev-only. Always run pnpm --filter @blind-chess/shared build before pnpm --filter @blind-chess/server build (the workspace project refs handle this when running pnpm -r build).
  • 2026-04-28: Client routing is hash-based with a pathname fallback in App.svelte so https://chess.sethpc.xyz/g/<id> (the share URL) and https://chess.sethpc.xyz/#/g/<id> (the post-create URL) both render the game. The Fastify SPA fallback serves index.html on any non-matching text/html request.
  • 2026-04-28: Click-to-move only — drag-and-drop deferred. Tap-arm + tap-destination is faithful to the touch-move FSM and works identically on phone and desktop.
  • 2026-04-28: WS path through Caddywss://chess.sethpc.xyz/ws?game=<id> works without explicit transport ws config. Caddy's reverse_proxy handles upgrade transparently.
  • 2026-04-28: Public DNS — relies on existing *.sethpc.xyz wildcard pointing at the WAN IP; no Pi-hole entry was needed. Caddy host-routes chess.sethpc.xyz to 192.168.0.245:3000.

AI / computer player (designed 2026-04-28, not yet implemented)

Spec: docs/superpowers/specs/2026-04-28-ai-player-design.md. All decisions below are settled at spec-approval time; revisit if implementation surfaces something the spec didn't anticipate.

  • 2026-04-28: Two AI bots, phased deliveryCasualBrain (Phase 1, algorithmic, in-process) ships first; ReconBrain (Phase 2, gemma4:26b chat agent) ships second. Phased to keep research uncertainty (Recon's actual playing strength) from blocking shipping anything. Rejected: combined launch, single difficulty-dial UX, throwaway Casual-as-stub.
  • 2026-04-28: Bots use the same view filter as humansBotDriver calls buildView(game, botColor); bot input is filtered BoardView + Announcement[]. No oracle access. Preserves the architectural invariant: the view filter is the only egress for board state, even for in-process bots. Rejected: "easy mode" oracle access for Casual to keep it simple.
  • 2026-04-28: In-process virtual players, not external WS clientsBotDriver lives in the existing Fastify server, dispatches actions through the same commit handler humans use. One process, no new deploy targets. Rejected: external bot processes (more operational surface, no real benefit), hybrid Casual-in-process / Recon-external (asymmetric for no gain).
  • 2026-04-28: Recon bot is a stateful chat agent, not stateless — per-game chat history persists across turns as the bot's private memory. Each turn appends user (new view + announcements + candidates) + assistant (reasoning + move). Reasoning is hidden from the human during play, revealed in collapsible post-game panel. Rejected: stateless one-shot move-picker (loses belief-tracking across turns), revealing reasoning during play (would leak strategic intent).
  • 2026-04-28: Endpoint priority: steel141 RTX 3090 Ti primary, pve197 V100 fallback — preflight on game creation; mid-game failover allowed once (one-way). Rationale: 3090 Ti benchmarks at ~134 tok/s on gemma4:26b; V100 estimated ~80 tok/s. Both have the model present. Rejected: no failover (worse UX), bidirectional flap (more complexity, no real benefit).
  • 2026-04-28: GPU shown to user — persistent badge under AI's slot reads "gemma4:26b · RTX 3090 Ti" (or V100 / failed-over variant). Game-start moderator-panel UI message explicitly names the model + host. Rationale: chess.sethpc.xyz is a personal homelab site; surfacing the hardware is brand-appropriate and gives honest feedback when fallback engages. Rejected: hiding the GPU (would be opaque on slow V100 fallback).
  • 2026-04-28: gemma4:26b model choice — sweet spot per gemma4-research: ~134 tok/s decode on 3090 Ti (4.7× faster than 31B), MoE 3.8B active, vision-capable (not used here). Rejected: 31B (5× slower, marginal strength gain not worth latency), e4b (too small for this task).
  • 2026-04-28: Per-move latency budget: 30s normal, 90s first-move — first-move headroom covers cold-start (steel141 keep_alive=30m policy, ~30-60s reload after idle). Beyond 90s, treat as endpoint failure → failover. Rejected: tighter cap (false-positives on cold start), looser cap (UX death).
  • 2026-04-28: Recon "done" bar: ≥60% wins over 50 Recon-vs-Casual self-play games — concrete, measurable acceptance bound. If Recon misses 60% but holds >40%, prompt-engineering rabbit hole; if <40%, design signal (try 31B or feed textual board representation). Self-play harness lives in scripts/selfplay.ts, not in CI. Rejected: subjective "feels okay" bar (would let weak Recon ship), bar against humans (untestable at scale).
  • 2026-04-28: Reasoning hidden during play, revealed post-game — Gemma's chat history is private during the game; on game end, the chat history is copied to Game.aiThoughtsLog and the post-game screen shows a collapsible "View gemma4's reasoning" section. Rejected: live streaming "thinking tokens" to user (leaks strategy), permanent hiding (loses showcase value of the project).
  • 2026-04-28: vsAi field added to CreateGameRequest; aiInfo field added to joined/update server messages; 'ai_unavailable' added to EndReason — minimal protocol surface for the feature. AI metadata is NOT in ModeratorText enum (kept clean). UI-system messages for game-start info and failover events are style-distinct from Announcement entries.

Deferred / Rejected

  • 2026-04-28: Tactical-advice interpretation of "won't help you" — rejected. The phrase is a check-resolution announcement, not engine evaluation. Subjective "this move is bad" is anti-fun and out of scope.
  • 2026-04-28: Spectator mode — deferred. Single-use links and no spectators in MVP. Revisit if there's demand.
  • 2026-04-28: Time controls (clocks) — deferred. Untimed correspondence-style for MVP. Optional 5+0 / 10+0 / 15+10 in a follow-up if Seth wants.
  • 2026-04-28: SQLite persistence — deferred. In-memory only for MVP. Add when crash recovery becomes painful (1-day implementation: serialize Map on ExecStop, deserialize on ExecStart).
  • 2026-04-28: End-to-end browser tests (Playwright) — out of scope for MVP. Protocol-level integration tests cover the same drift surface for ~10× less maintenance. Manual phone+desktop testing suffices.
  • 2026-04-28: Vanilla-only or blind-only MVP — rejected in favor of both-from-day-one. The shared engine + view-filter architecture means vanilla is essentially free.
  • 2026-04-28: Authentik gate on chess.sethpc.xyz — rejected. The hashed link IS the auth; an additional gate would be friction with no security benefit (link guessing is already infeasible).
  • 2026-04-28: CI/CD automation — deferred. Manual pnpm -r build + rsync + systemctl restart is fine for a hobby project. Add Gitea Actions later if deploy friction grows.
  • 2026-04-28: Move log / PGN export, post-game replay — deferred. Announcements are persisted in-game (so the moderator-panel scrollback works); export and replay are post-MVP.
  • 2026-04-28: Public lobby / matchmaking / ratings — out of scope. This is a private-link game, not a chess site.
  • 2026-04-28: Pre-deploy "server restarting" warning to active players — stretch goal, not MVP. Mitigation for now: deploy during low-usage windows.
  • 2026-04-28: Client-side AI / hint generation — explicitly out of scope. Human vs. human only. Partially superseded 2026-04-28 by AI-player spec. Reversal applies only to the human-vs-AI path; client-side AI / hint generation in human-vs-human games remains rejected.
  • 2026-04-28: Difficulty slider for AI — rejected. Two named buttons (Casual, Recon) only. No continuum; the two bots are architecturally different, not tuneable strengths of the same engine.
  • 2026-04-28: Stockfish for vanilla-mode AI strength — deferred. Vanilla is a side-effect, not a feature target. Revisit if users explicitly ask for strong vanilla AI.
  • 2026-04-28: Live token streaming during Gemma's thinking — rejected for MVP. Static "AI is thinking..." indicator only. Streaming would leak strategic intent and adds protocol complexity.
  • 2026-04-28: Mid-game GPU flap-back — rejected. Once failed over to V100, stays there for the rest of the game even if steel141 recovers. Simpler, more predictable, and chat-history is mid-flight.
  • 2026-04-28: AI vs AI public spectate-able games — rejected for MVP. Self-play harness is CLI-only (scripts/selfplay.ts).
  • 2026-04-28: Per-turn context compaction — deferred. Spec uses num_ctx: 32768 which covers ~128 turns; longer games would overflow but are rare in casual play. Add running-summary compaction if seen in practice.
  • 2026-04-28: Bot rating / Elo / personalities — out of scope. Two named buttons, no scoreboard.