From 0583984723422d2b3dbef515b7cff1c3825831c1 Mon Sep 17 00:00:00 2001 From: "claude (blind_chess)" Date: Mon, 18 May 2026 20:24:13 -0400 Subject: [PATCH] feat(client): local-only phantom-layer store Co-Authored-By: Claude Opus 4.7 (1M context) --- .../client/src/lib/stores/phantoms.svelte.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/client/src/lib/stores/phantoms.svelte.ts diff --git a/packages/client/src/lib/stores/phantoms.svelte.ts b/packages/client/src/lib/stores/phantoms.svelte.ts new file mode 100644 index 0000000..a2e6ecf --- /dev/null +++ b/packages/client/src/lib/stores/phantoms.svelte.ts @@ -0,0 +1,71 @@ +import { + opponentStartPosition, + deserializePhantoms, + type Color, + type Piece, + type PieceType, + type Square, +} from '@blind-chess/shared'; + +/** + * Client-LOCAL store for the player's phantom opponent-model layer. + * This data NEVER reaches the server — it is the player's private guess. + * Do not read this store in any `send`/`commit` path. + */ +function makeStore() { + const state = $state<{ phantoms: Partial> }>({ phantoms: {} }); + let gameId: string | null = null; + let oppColor: Color = 'b'; + + function key(id: string) { return `bc:phantoms:${id}`; } + + function persist() { + if (gameId) localStorage.setItem(key(gameId), JSON.stringify(state.phantoms)); + } + + /** Load (or first-time seed) the phantom layer for a blind game. */ + function loadForGame(id: string, you: Color) { + gameId = id; + oppColor = you === 'w' ? 'b' : 'w'; + const raw = localStorage.getItem(key(id)); + if (raw === null) { + // First load — seed with the opponent's starting army. + state.phantoms = opponentStartPosition(oppColor); + persist(); + } else { + state.phantoms = deserializePhantoms(raw); + } + } + + function place(sq: Square, type: PieceType) { + state.phantoms = { ...state.phantoms, [sq]: { color: oppColor, type } }; + persist(); + } + + function move(from: Square, to: Square) { + const p = state.phantoms[from]; + if (!p) return; + const next = { ...state.phantoms }; + delete next[from]; + next[to] = p; + state.phantoms = next; + persist(); + } + + function remove(sq: Square) { + const next = { ...state.phantoms }; + delete next[sq]; + state.phantoms = next; + persist(); + } + + /** Drop the layer when a game ends — avoids unbounded localStorage growth. */ + function clearForGame(id: string) { + localStorage.removeItem(key(id)); + if (gameId === id) { state.phantoms = {}; gameId = null; } + } + + return { state, loadForGame, place, move, remove, clearForGame }; +} + +export const phantoms = makeStore();