fix(chess): preset toggles refresh engine state immediately; new-game flows reset the board
This commit is contained in:
parent
50fba0bbd7
commit
858d326895
6 changed files with 70 additions and 12 deletions
|
|
@ -32,7 +32,7 @@ export function App() {
|
||||||
<Toaster position="top-center" richColors closeButton />
|
<Toaster position="top-center" richColors closeButton />
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<Routes location={location} key={location.pathname}>
|
<Routes location={location} key={location.pathname}>
|
||||||
<Route path="/" element={<PageTransition><Lobby /></PageTransition>} />
|
<Route path="/" element={<PageTransition><Lobby chessState={chessState} /></PageTransition>} />
|
||||||
<Route path="/game" element={<PageTransition><GameView engineState={chessState} /></PageTransition>} />
|
<Route path="/game" element={<PageTransition><GameView engineState={chessState} /></PageTransition>} />
|
||||||
<Route path="/rules" element={<PageTransition><RulesView chessState={chessState} /></PageTransition>} />
|
<Route path="/rules" element={<PageTransition><RulesView chessState={chessState} /></PageTransition>} />
|
||||||
<Route path="/save" element={<PageTransition><SaveWrapper chessState={chessState} /></PageTransition>} />
|
<Route path="/save" element={<PageTransition><SaveWrapper chessState={chessState} /></PageTransition>} />
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,16 @@ export function useChessEngine() {
|
||||||
? moveHistoryRef.current[moveHistoryRef.current.length - 1]
|
? moveHistoryRef.current[moveHistoryRef.current.length - 1]
|
||||||
: null;
|
: 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 {
|
return {
|
||||||
engine,
|
engine,
|
||||||
turn: getTurn(),
|
turn: getTurn(),
|
||||||
|
|
@ -97,6 +107,7 @@ export function useChessEngine() {
|
||||||
undo,
|
undo,
|
||||||
canUndo: moveHistoryRef.current.length > 0,
|
canUndo: moveHistoryRef.current.length > 0,
|
||||||
loadEngine,
|
loadEngine,
|
||||||
|
refresh,
|
||||||
lastMove,
|
lastMove,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function GameView({ engineState }: GameViewProps) {
|
||||||
const localChessState = useChessEngine();
|
const localChessState = useChessEngine();
|
||||||
const state = engineState || localChessState;
|
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) => {
|
const handleMove = (from: number, to: number, promoteTo?: PieceType) => {
|
||||||
applyMove(from, to, promoteTo || 'queen');
|
applyMove(from, to, promoteTo || 'queen');
|
||||||
|
|
@ -82,7 +82,7 @@ export function GameView({ engineState }: GameViewProps) {
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="flex flex-col items-center gap-8 py-8 w-full max-w-4xl mx-auto"
|
className="flex flex-col items-center gap-8 py-8 w-full max-w-4xl mx-auto"
|
||||||
>
|
>
|
||||||
<RulesDrawer />
|
<RulesDrawer onRulesChanged={refresh} />
|
||||||
<div className="flex flex-col md:flex-row w-full items-start justify-between gap-4 px-4">
|
<div className="flex flex-col md:flex-row w-full items-start justify-between gap-4 px-4">
|
||||||
|
|
||||||
{/* Header/Info section */}
|
{/* Header/Info section */}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import { pieceAssets } from '../assets/pieces';
|
import { pieceAssets } from '../assets/pieces';
|
||||||
|
import { ChessEngine } from '../engine';
|
||||||
|
import { clearAutoSave } from '../persist/autosave';
|
||||||
|
|
||||||
const WS_URL =
|
const WS_URL =
|
||||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_WS_URL'] ??
|
(import.meta as { env?: Record<string, string> }).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 [codeInput, setCodeInput] = useState('');
|
||||||
const [roomCode, setRoomCode] = useState<string | null>(null);
|
const [roomCode, setRoomCode] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
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 () => {
|
const handleCreate = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -101,6 +121,7 @@ export function Lobby() {
|
||||||
sessionStorage.setItem('room-code', code);
|
sessionStorage.setItem('room-code', code);
|
||||||
sessionStorage.setItem('room-token', token);
|
sessionStorage.setItem('room-token', token);
|
||||||
sessionStorage.setItem('player-color', color);
|
sessionStorage.setItem('player-color', color);
|
||||||
|
resetToFreshGame();
|
||||||
setRoomCode(code);
|
setRoomCode(code);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Could not connect to server');
|
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-code', result.code);
|
||||||
sessionStorage.setItem('room-token', result.token);
|
sessionStorage.setItem('room-token', result.token);
|
||||||
sessionStorage.setItem('player-color', result.color);
|
sessionStorage.setItem('player-color', result.color);
|
||||||
|
resetToFreshGame();
|
||||||
navigate('/game');
|
navigate('/game');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Invalid room code');
|
setError(err instanceof Error ? err.message : 'Invalid room code');
|
||||||
|
|
@ -130,6 +152,11 @@ export function Lobby() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePlaySolo = () => {
|
||||||
|
resetToFreshGame();
|
||||||
|
navigate('/game');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
data-testid="page-home"
|
data-testid="page-home"
|
||||||
|
|
@ -151,11 +178,11 @@ export function Lobby() {
|
||||||
<h1 className="text-3xl font-extrabold text-neutral-900 tracking-tight">Paratype Chess</h1>
|
<h1 className="text-3xl font-extrabold text-neutral-900 tracking-tight">Paratype Chess</h1>
|
||||||
<p className="text-neutral-500 font-medium mt-2">Realtime multiplayer with custom rules</p>
|
<p className="text-neutral-500 font-medium mt-2">Realtime multiplayer with custom rules</p>
|
||||||
<button
|
<button
|
||||||
data-action="start-new-game"
|
data-action="play-solo"
|
||||||
onClick={() => navigate('/game')}
|
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"
|
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
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -187,7 +214,13 @@ export function Lobby() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => 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]"
|
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
|
Enter Room
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,13 @@ import { toast } from 'sonner';
|
||||||
* changes externally (e.g. via the /rules page), so this drawer stays in
|
* changes externally (e.g. via the /rules page), so this drawer stays in
|
||||||
* sync if both UIs are open.
|
* 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);
|
const [open, setOpen] = useState(false);
|
||||||
// Force-refresh trigger. The registry itself has no event emitter, so we
|
// 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
|
// 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);
|
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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState, useMemo, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { PRESET_REGISTRY } from '../presets/index.js';
|
import { PRESET_REGISTRY } from '../presets/index.js';
|
||||||
import { ChessEngine } from '../engine.js';
|
import { ChessEngine } from '../engine.js';
|
||||||
|
import { clearAutoSave } from '../persist/autosave.js';
|
||||||
import type { useChessEngine } from '../hooks/useChessEngine.js';
|
import type { useChessEngine } from '../hooks/useChessEngine.js';
|
||||||
|
|
||||||
interface RulesViewProps {
|
interface RulesViewProps {
|
||||||
|
|
@ -88,9 +89,12 @@ export function RulesView({ chessState, isGameActive }: RulesViewProps) {
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
// Starting a new game with the selected ruleset: reset the engine so
|
// Starting a new game with the selected ruleset: reset the engine so
|
||||||
// opening moves are generated under the active presets. We keep the
|
// opening moves are generated under the active presets. Clear the
|
||||||
// user's selection intact in local state (the registry is the source
|
// autosave too — otherwise a full-page reload would re-hydrate the
|
||||||
// of truth; the useEffect above has already synced it).
|
// 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) {
|
if (chessState) {
|
||||||
chessState.loadEngine(new ChessEngine());
|
chessState.loadEngine(new ChessEngine());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue