feat(chess): add localStorage auto-save and restore (P3.14)

This commit is contained in:
Joey Yakimowich-Payne 2026-04-16 16:24:49 -06:00
commit a75b6f041b
No known key found for this signature in database
4 changed files with 43 additions and 2 deletions

View file

@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { Routes, Route, useNavigate } from 'react-router-dom' import { Routes, Route, useNavigate } from 'react-router-dom'
import { GameView } from '../ui/GameView' import { GameView } from '../ui/GameView'
import { RulesView } from '../ui/RulesView' import { RulesView } from '../ui/RulesView'
@ -5,11 +6,23 @@ import { SavePanel } from '../ui/SavePanel'
import { ImportExport } from '../ui/ImportExport' import { ImportExport } from '../ui/ImportExport'
import { useChessEngine } from '../hooks/useChessEngine' import { useChessEngine } from '../hooks/useChessEngine'
import { ChessEngine } from '../engine' import { ChessEngine } from '../engine'
import { loadAutoSave } from '../persist/autosave.js'
import type { AttrKey, FactValue, EntityId } from '@paratype/rete' import type { AttrKey, FactValue, EntityId } from '@paratype/rete'
export function App() { export function App() {
const chessState = useChessEngine() const chessState = useChessEngine()
// Restore autosave on mount — [] deps is intentional (run once, avoid infinite loop)
useEffect(() => {
const saved = loadAutoSave();
if (!saved) return;
const newEngine = new ChessEngine();
const oldFacts = newEngine.session.allFacts();
for (const f of oldFacts) newEngine.session.retract(f.id, f.attr as AttrKey);
for (const f of saved) newEngine.session.insert(f.id as EntityId, f.attr as AttrKey, f.value as FactValue);
chessState.loadEngine(newEngine);
}, []); // intentional: run once on mount, chessState ref is stable
return ( return (
<div data-testid="app-root"> <div data-testid="app-root">
<Routes> <Routes>

View file

@ -1,6 +1,7 @@
import { useState, useRef, useCallback } from 'react'; import { useState, useRef, useCallback } from 'react';
import { ChessEngine, type GameResult } from '../engine'; import { ChessEngine, type GameResult } from '../engine';
import type { PieceType } from '../schema'; import type { PieceType } from '../schema';
import { saveAutoSave } from '../persist/autosave.js';
export function useChessEngine() { export function useChessEngine() {
const engineRef = useRef<ChessEngine | null>(null); const engineRef = useRef<ChessEngine | null>(null);
@ -19,6 +20,7 @@ export function useChessEngine() {
const loadEngine = useCallback((newEngine: ChessEngine) => { const loadEngine = useCallback((newEngine: ChessEngine) => {
engineRef.current = newEngine; engineRef.current = newEngine;
moveHistoryRef.current = []; moveHistoryRef.current = [];
saveAutoSave(newEngine.session.allFacts());
setTick(t => t + 1); setTick(t => t + 1);
}, []); }, []);
@ -43,6 +45,7 @@ export function useChessEngine() {
if (move) { if (move) {
moveHistoryRef.current.push({ from, to, promoteTo }); moveHistoryRef.current.push({ from, to, promoteTo });
const result = engine.applyMove(move, promoteTo); const result = engine.applyMove(move, promoteTo);
saveAutoSave(engine.session.allFacts());
setTick(t => t + 1); // trigger re-render setTick(t => t + 1); // trigger re-render
return result; return result;
} }
@ -60,6 +63,7 @@ export function useChessEngine() {
} }
} }
engineRef.current = newEngine; engineRef.current = newEngine;
saveAutoSave(newEngine.session.allFacts());
setTick(t => t + 1); setTick(t => t + 1);
} }
}, []); }, []);

View file

@ -0,0 +1,23 @@
import { exportGame, importGame } from './io.js';
const AUTOSAVE_KEY = 'paratype-chess:v1:autosave';
export function saveAutoSave(facts: Array<{ id: number; attr: string; value: unknown }>): void {
try {
localStorage.setItem(AUTOSAVE_KEY, exportGame(facts));
} catch { /* ignore quota errors */ }
}
export function loadAutoSave(): Array<{ id: number; attr: string; value: unknown }> | null {
try {
const raw = localStorage.getItem(AUTOSAVE_KEY);
if (!raw) return null;
return importGame(raw);
} catch {
return null; // silently discard invalid/stale/version-mismatched data
}
}
export function clearAutoSave(): void {
localStorage.removeItem(AUTOSAVE_KEY);
}

View file

@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"rootDir": "src",
"composite": true "composite": true
}, },
"include": ["src/**/*", "tests/**/*"] "include": ["src/**/*"]
} }