chore: initial scaffold — spec, decisions, gitignore

This commit is contained in:
claude (blind_chess)
2026-04-28 10:53:26 -04:00
commit 9a5ad55f30
5 changed files with 772 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# 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.
## Deferred / Rejected
<!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas -->
- 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.