diff --git a/packages/chess/src/app/App.tsx b/packages/chess/src/app/App.tsx index d91e462..7b926b3 100644 --- a/packages/chess/src/app/App.tsx +++ b/packages/chess/src/app/App.tsx @@ -32,7 +32,7 @@ export function App() { - } /> + } /> } /> } /> } /> diff --git a/packages/chess/src/hooks/useChessEngine.ts b/packages/chess/src/hooks/useChessEngine.ts index 0a9084c..20e6796 100644 --- a/packages/chess/src/hooks/useChessEngine.ts +++ b/packages/chess/src/hooks/useChessEngine.ts @@ -87,6 +87,16 @@ export function useChessEngine() { ? moveHistoryRef.current[moveHistoryRef.current.length - 1] : null; + /** + * Force the hook's derived state (facts, legalMoves, turn, result) to + * recompute. Call this after mutating external state that the engine + * reads but React can't see — for example, toggling PRESET_REGISTRY + * entries that change the set of legal moves without triggering a move. + */ + const refresh = useCallback(() => { + setTick(t => t + 1); + }, []); + return { engine, turn: getTurn(), @@ -97,6 +107,7 @@ export function useChessEngine() { undo, canUndo: moveHistoryRef.current.length > 0, loadEngine, + refresh, lastMove, }; } diff --git a/packages/chess/src/ui/GameView.tsx b/packages/chess/src/ui/GameView.tsx index c24e9c0..5641461 100644 --- a/packages/chess/src/ui/GameView.tsx +++ b/packages/chess/src/ui/GameView.tsx @@ -17,7 +17,7 @@ export function GameView({ engineState }: GameViewProps) { const localChessState = useChessEngine(); const state = engineState || localChessState; - const { facts, legalMoves, turn, result, applyMove, undo, canUndo, lastMove } = state; + const { facts, legalMoves, turn, result, applyMove, undo, canUndo, lastMove, refresh } = state; const handleMove = (from: number, to: number, promoteTo?: PieceType) => { applyMove(from, to, promoteTo || 'queen'); @@ -82,7 +82,7 @@ export function GameView({ engineState }: GameViewProps) { transition={{ duration: 0.3 }} className="flex flex-col items-center gap-8 py-8 w-full max-w-4xl mx-auto" > - + {/* Header/Info section */} diff --git a/packages/chess/src/ui/Lobby.tsx b/packages/chess/src/ui/Lobby.tsx index 6164b53..a627db9 100644 --- a/packages/chess/src/ui/Lobby.tsx +++ b/packages/chess/src/ui/Lobby.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'motion/react'; import { pieceAssets } from '../assets/pieces'; +import { ChessEngine } from '../engine'; +import { clearAutoSave } from '../persist/autosave'; const WS_URL = (import.meta as { env?: Record }).env?.['VITE_WS_URL'] ?? @@ -86,13 +88,31 @@ function oneShotRoomRequest( }); } -export function Lobby() { +interface LobbyProps { + /** Optional — when provided, create/join/solo flows reset the local + * ChessEngine so the user starts from the standard opening rather + * than whatever was restored from autosave. */ + chessState?: { + loadEngine: (engine: ChessEngine) => void; + }; +} + +export function Lobby({ chessState }: LobbyProps = {}) { const [codeInput, setCodeInput] = useState(''); const [roomCode, setRoomCode] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const resetToFreshGame = () => { + // Wipe the autosave so a later full-page reload doesn't re-hydrate + // the previous (finished) game, and replace the in-memory engine + // with a brand-new one now so the board shows the starting position + // the instant the user lands on /game. + clearAutoSave(); + chessState?.loadEngine(new ChessEngine()); + }; + const handleCreate = async () => { setLoading(true); setError(null); @@ -101,6 +121,7 @@ export function Lobby() { sessionStorage.setItem('room-code', code); sessionStorage.setItem('room-token', token); sessionStorage.setItem('player-color', color); + resetToFreshGame(); setRoomCode(code); } catch (err) { setError(err instanceof Error ? err.message : 'Could not connect to server'); @@ -122,6 +143,7 @@ export function Lobby() { sessionStorage.setItem('room-code', result.code); sessionStorage.setItem('room-token', result.token); sessionStorage.setItem('player-color', result.color); + resetToFreshGame(); navigate('/game'); } catch (err) { setError(err instanceof Error ? err.message : 'Invalid room code'); @@ -130,6 +152,11 @@ export function Lobby() { } }; + const handlePlaySolo = () => { + resetToFreshGame(); + navigate('/game'); + }; + return ( Paratype Chess Realtime multiplayer with custom rules navigate('/game')} + data-action="play-solo" + onClick={handlePlaySolo} className="mt-4 text-sm font-semibold text-neutral-500 hover:text-neutral-900 transition-colors underline underline-offset-4 decoration-neutral-300 hover:decoration-neutral-900" > - Play Solo (Local) + Play Solo (Local) — fresh board @@ -187,7 +214,13 @@ export function Lobby() { navigate('/game')} + onClick={() => { + // Re-reset just before navigation in case the user + // spent a long time on this page and autosave was + // somehow re-written in the meantime. + resetToFreshGame(); + navigate('/game'); + }} className="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition-all shadow-md active:scale-[0.98]" > Enter Room diff --git a/packages/chess/src/ui/RulesDrawer.tsx b/packages/chess/src/ui/RulesDrawer.tsx index e07a72e..9ba41ab 100644 --- a/packages/chess/src/ui/RulesDrawer.tsx +++ b/packages/chess/src/ui/RulesDrawer.tsx @@ -16,7 +16,13 @@ import { toast } from 'sonner'; * changes externally (e.g. via the /rules page), so this drawer stays in * sync if both UIs are open. */ -export function RulesDrawer() { +interface RulesDrawerProps { + /** Called after any toggle so the parent can recompute derived UI state + * (legal-move highlights, etc.) that depends on the active preset set. */ + onRulesChanged?: () => void; +} + +export function RulesDrawer({ onRulesChanged }: RulesDrawerProps = {}) { const [open, setOpen] = useState(false); // Force-refresh trigger. The registry itself has no event emitter, so we // bump this counter on every open and every toggle click to re-read the @@ -49,6 +55,10 @@ export function RulesDrawer() { } } setTick((t) => t + 1); + // Ask the parent engine state to recompute legalMoves — otherwise the + // Board's highlighted drop targets won't reflect the new rule set + // until the next move bumps the hook's internal tick. + onRulesChanged?.(); }; return ( diff --git a/packages/chess/src/ui/RulesView.tsx b/packages/chess/src/ui/RulesView.tsx index 2a52f0c..e87991d 100644 --- a/packages/chess/src/ui/RulesView.tsx +++ b/packages/chess/src/ui/RulesView.tsx @@ -2,6 +2,7 @@ import { useState, useMemo, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { PRESET_REGISTRY } from '../presets/index.js'; import { ChessEngine } from '../engine.js'; +import { clearAutoSave } from '../persist/autosave.js'; import type { useChessEngine } from '../hooks/useChessEngine.js'; interface RulesViewProps { @@ -88,9 +89,12 @@ export function RulesView({ chessState, isGameActive }: RulesViewProps) { const handleApply = () => { // Starting a new game with the selected ruleset: reset the engine so - // opening moves are generated under the active presets. We keep the - // user's selection intact in local state (the registry is the source - // of truth; the useEffect above has already synced it). + // opening moves are generated under the active presets. Clear the + // autosave too — otherwise a full-page reload would re-hydrate the + // previous in-progress game. The registry is already synced via the + // useEffect above, so the fresh engine will see the active presets + // on its first getAllLegalMoves() call. + clearAutoSave(); if (chessState) { chessState.loadEngine(new ChessEngine()); }
Realtime multiplayer with custom rules