From 33489d7029cbe01e770d30d0b4e2cf26b08dbed0 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 4 Jan 2026 11:28:23 +0100 Subject: [PATCH] fix: Undo button now properly truncates move history - Added UNDO_TO_MOVE action that both navigates AND truncates history - Added undoToMove() function to OpeningTrainingContext - Updated handleUndoDeviation to use undoToMove instead of navigate Previously, the Undo button only navigated back but didn't remove the deviation moves, so the session remained in off-book state. --- .../OpeningTrainer/OpeningTrainer.tsx | 11 ++--- src/contexts/OpeningTrainingContext.tsx | 13 ++++++ src/lib/openingTrainer/sessionReducer.ts | 42 +++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/components/OpeningTrainer/OpeningTrainer.tsx b/src/components/OpeningTrainer/OpeningTrainer.tsx index 61efdea..59db972 100644 --- a/src/components/OpeningTrainer/OpeningTrainer.tsx +++ b/src/components/OpeningTrainer/OpeningTrainer.tsx @@ -52,6 +52,7 @@ export default function OpeningTrainer({ currentFeedback, initializeSession, makeMove, + undoToMove, navigateToMove, } = useOpeningTraining(); @@ -225,12 +226,12 @@ export default function OpeningTrainer({ const handleUndoDeviation = () => { if (!session || session.deviationMoveIndex === null) return; - // Navigate back to the move before deviation - navigateToMove(session.deviationMoveIndex - 1); - setShowDeviationDialog(false); + // Undo to the move before deviation (this truncates move history) + const targetIndex = session.deviationMoveIndex - 1; + undoToMove(targetIndex); - // After a brief delay, make another legal move to continue in theory - // This allows the player to try again + // Close the dialog + setShowDeviationDialog(false); }; const handleStartGameFromPosition = () => { diff --git a/src/contexts/OpeningTrainingContext.tsx b/src/contexts/OpeningTrainingContext.tsx index fe71a4a..07981f7 100644 --- a/src/contexts/OpeningTrainingContext.tsx +++ b/src/contexts/OpeningTrainingContext.tsx @@ -38,6 +38,7 @@ interface OpeningTrainingContextType { // Actions initializeSession: (opening: OpeningMetadata, forceNew?: boolean) => Promise; makeMove: (san: string) => Promise; + undoToMove: (index: number) => void; navigateToMove: (index: number) => void; resetSession: () => void; } @@ -294,6 +295,17 @@ export function OpeningTrainingProvider({ } }; + const undoToMove = (index: number) => { + if (!session) return; + + console.log('[OpeningTraining] Undoing to move index:', index); + + dispatch({ type: 'UNDO_TO_MOVE', index }); + + // Clear feedback when undoing + setCurrentFeedback(null); + }; + const navigateToMove = (index: number) => { if (!session) return; @@ -355,6 +367,7 @@ export function OpeningTrainingProvider({ currentFeedback, initializeSession, makeMove, + undoToMove, navigateToMove, resetSession, }; diff --git a/src/lib/openingTrainer/sessionReducer.ts b/src/lib/openingTrainer/sessionReducer.ts index 5d5e2b4..2884748 100644 --- a/src/lib/openingTrainer/sessionReducer.ts +++ b/src/lib/openingTrainer/sessionReducer.ts @@ -93,6 +93,7 @@ export type SessionAction = | { type: 'NAVIGATE_PREVIOUS' } | { type: 'NAVIGATE_NEXT' } | { type: 'NAVIGATE_TO_CURRENT' } // Jump back to end of history + | { type: 'UNDO_TO_MOVE'; index: number } // Navigate and truncate history // Theory tracking | { type: 'DEVIATION_DETECTED'; moveIndex: number } @@ -348,6 +349,47 @@ export function sessionReducer( }); } + case 'UNDO_TO_MOVE': { + const { index } = action; + + // Clamp index to valid range (-1 means start position, truncate all moves) + const clampedIndex = Math.max(-1, Math.min(index, state.moveHistory.length - 1)); + + // Truncate move history to this point + const newHistory = clampedIndex < 0 ? [] : state.moveHistory.slice(0, clampedIndex + 1); + + // Build FEN at target index + const newFEN = buildFenAtIndex(newHistory, newHistory.length); + + // Reset deviation if we're now back in theory + const isNowInTheory = isInTheory(state.opening, newHistory); + const newDeviationIndex = isNowInTheory ? null : state.deviationMoveIndex; + + // Determine phase + let phase: SessionPhase; + if (newDeviationIndex !== null) { + phase = 'off_book'; + } else if (isEndOfRepertoire(state.opening, newHistory.length)) { + phase = 'end_of_repertoire'; + } else if (isOpponentTurn(state.opening, newHistory)) { + phase = 'opponent_turn'; + } else { + phase = 'user_turn'; + } + + return { + ...state, + moveHistory: newHistory, + currentMoveIndex: newHistory.length, + currentFEN: newFEN, + deviationMoveIndex: newDeviationIndex, + isInTheory: isNowInTheory, + phase, + pendingOpponentMove: null, + lastUpdatedAt: Date.now(), + }; + } + case 'NAVIGATE_TO_CURRENT': { // Jump to the end of history (current position) const atEnd = state.currentMoveIndex === state.moveHistory.length;