feat(client): local-only phantom-layer store
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Record<Square, Piece>> }>({ 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();
|
||||||
Reference in New Issue
Block a user