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.
This commit is contained in:
Stefan
2026-01-04 11:28:23 +01:00
parent 039637b2da
commit 33489d7029
3 changed files with 61 additions and 5 deletions
@@ -52,6 +52,7 @@ export default function OpeningTrainer({
currentFeedback, currentFeedback,
initializeSession, initializeSession,
makeMove, makeMove,
undoToMove,
navigateToMove, navigateToMove,
} = useOpeningTraining(); } = useOpeningTraining();
@@ -225,12 +226,12 @@ export default function OpeningTrainer({
const handleUndoDeviation = () => { const handleUndoDeviation = () => {
if (!session || session.deviationMoveIndex === null) return; if (!session || session.deviationMoveIndex === null) return;
// Navigate back to the move before deviation // Undo to the move before deviation (this truncates move history)
navigateToMove(session.deviationMoveIndex - 1); const targetIndex = session.deviationMoveIndex - 1;
setShowDeviationDialog(false); undoToMove(targetIndex);
// After a brief delay, make another legal move to continue in theory // Close the dialog
// This allows the player to try again setShowDeviationDialog(false);
}; };
const handleStartGameFromPosition = () => { const handleStartGameFromPosition = () => {
+13
View File
@@ -38,6 +38,7 @@ interface OpeningTrainingContextType {
// Actions // Actions
initializeSession: (opening: OpeningMetadata, forceNew?: boolean) => Promise<void>; initializeSession: (opening: OpeningMetadata, forceNew?: boolean) => Promise<void>;
makeMove: (san: string) => Promise<void>; makeMove: (san: string) => Promise<void>;
undoToMove: (index: number) => void;
navigateToMove: (index: number) => void; navigateToMove: (index: number) => void;
resetSession: () => 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) => { const navigateToMove = (index: number) => {
if (!session) return; if (!session) return;
@@ -355,6 +367,7 @@ export function OpeningTrainingProvider({
currentFeedback, currentFeedback,
initializeSession, initializeSession,
makeMove, makeMove,
undoToMove,
navigateToMove, navigateToMove,
resetSession, resetSession,
}; };
+42
View File
@@ -93,6 +93,7 @@ export type SessionAction =
| { type: 'NAVIGATE_PREVIOUS' } | { type: 'NAVIGATE_PREVIOUS' }
| { type: 'NAVIGATE_NEXT' } | { type: 'NAVIGATE_NEXT' }
| { type: 'NAVIGATE_TO_CURRENT' } // Jump back to end of history | { type: 'NAVIGATE_TO_CURRENT' } // Jump back to end of history
| { type: 'UNDO_TO_MOVE'; index: number } // Navigate and truncate history
// Theory tracking // Theory tracking
| { type: 'DEVIATION_DETECTED'; moveIndex: number } | { 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': { case 'NAVIGATE_TO_CURRENT': {
// Jump to the end of history (current position) // Jump to the end of history (current position)
const atEnd = state.currentMoveIndex === state.moveHistory.length; const atEnd = state.currentMoveIndex === state.moveHistory.length;