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 />
|
||||
<AnimatePresence mode="wait">
|
||||
<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="/rules" element={<PageTransition><RulesView 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]
|
||||
: 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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<RulesDrawer />
|
||||
<RulesDrawer onRulesChanged={refresh} />
|
||||
<div className="flex flex-col md:flex-row w-full items-start justify-between gap-4 px-4">
|
||||
|
||||
{/* Header/Info section */}
|
||||
|
|
|
|||
|
|
@ -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<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 [roomCode, setRoomCode] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<main
|
||||
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>
|
||||
<p className="text-neutral-500 font-medium mt-2">Realtime multiplayer with custom rules</p>
|
||||
<button
|
||||
data-action="start-new-game"
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -187,7 +214,13 @@ export function Lobby() {
|
|||
</span>
|
||||
</div>
|
||||
<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]"
|
||||
>
|
||||
Enter Room
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue