fix: Provide theoretical moves to tutor in family mode even without feedback
The tutor was recommending incorrect moves in family mode because theoreticalAlternatives were only provided when currentFeedback existed. In family mode, the tutor needs access to all possible theoretical moves from the variation tree at all times, not just after a move is played. Changes: - Modified currentFeedback in openingPracticeMode to always provide theoreticalMoves in family mode, even when no feedback exists yet - Added comprehensive tests for French Defense family mode - Added tests for opening loader functionality - Fixed missing timestamp property in test fixtures This fixes the issue where asking the tutor how do I play French Defense? at the start would result in wrong move recommendations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -458,7 +458,11 @@ export default function OpeningTrainer({
|
||||
category: currentFeedback.classification.category,
|
||||
evaluationChange: currentFeedback.classification.evaluationChange,
|
||||
theoreticalAlternatives: isFamilyMode ? theoreticalMoves : currentFeedback.classification.theoreticalAlternatives
|
||||
} : null,
|
||||
} : (isFamilyMode ? {
|
||||
category: 'in-theory' as const,
|
||||
evaluationChange: 0,
|
||||
theoreticalAlternatives: theoreticalMoves
|
||||
} : null),
|
||||
wikipediaSummary: wikipediaSummary?.extract || undefined,
|
||||
shouldTutorSpeak,
|
||||
onTutorMessageSent: handleTutorMessageSent,
|
||||
|
||||
@@ -58,6 +58,7 @@ function createMoveEntry(
|
||||
san,
|
||||
uci: 'e2e4',
|
||||
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
|
||||
timestamp: Date.now(),
|
||||
evaluation: { score: 0, mate: null, depth: 15, bestMove: 'e4', ponder: null },
|
||||
classification: {
|
||||
category: 'in-theory',
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Test French Defense Family Mode
|
||||
*
|
||||
* This tests the actual scenario where the tutor recommends wrong moves
|
||||
*/
|
||||
|
||||
import {
|
||||
buildVariationTree,
|
||||
getAllPossibleNextMoves,
|
||||
parseMoveSequence,
|
||||
} from '../gameLogic';
|
||||
import { getOpeningsByFamily } from '../openingLoader';
|
||||
import { MoveHistoryEntry } from '@/types/openingTraining';
|
||||
|
||||
function createMoveEntry(
|
||||
san: string,
|
||||
color: 'white' | 'black',
|
||||
moveNumber: number
|
||||
): MoveHistoryEntry {
|
||||
return {
|
||||
moveNumber,
|
||||
color,
|
||||
san,
|
||||
uci: 'e2e4',
|
||||
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
|
||||
timestamp: Date.now(),
|
||||
evaluation: { score: 0, mate: null, depth: 15, bestMove: 'e4', ponder: null },
|
||||
classification: {
|
||||
category: 'in-theory',
|
||||
inRepertoire: true,
|
||||
evaluationChange: 0,
|
||||
isSignificantSwing: false,
|
||||
theoreticalAlternatives: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('French Defense - Family Mode Bug', () => {
|
||||
it('should load French Defense variations', () => {
|
||||
const variations = getOpeningsByFamily('French Defense');
|
||||
|
||||
expect(variations.length).toBeGreaterThan(0);
|
||||
console.log(`Loaded ${variations.length} French Defense variations`);
|
||||
});
|
||||
|
||||
it('should build variation tree for French Defense', () => {
|
||||
const variations = getOpeningsByFamily('French Defense');
|
||||
const tree = buildVariationTree(variations, 'French Defense');
|
||||
|
||||
expect(tree.familyName).toBe('French Defense');
|
||||
expect(tree.allVariations.length).toBe(variations.length);
|
||||
});
|
||||
|
||||
it('should show correct moves after 1. e4 e6', () => {
|
||||
const variations = getOpeningsByFamily('French Defense');
|
||||
const tree = buildVariationTree(variations, 'French Defense');
|
||||
|
||||
const moveHistory = [
|
||||
createMoveEntry('e4', 'white', 1),
|
||||
createMoveEntry('e6', 'black', 1),
|
||||
];
|
||||
|
||||
const possibleMoves = getAllPossibleNextMoves(tree, moveHistory);
|
||||
|
||||
console.log('After 1. e4 e6, possible 2nd moves for White:');
|
||||
console.log(possibleMoves.map(m => m.move).join(', '));
|
||||
|
||||
// White should have d4 as an option
|
||||
const moveNames = possibleMoves.map(m => m.move);
|
||||
expect(moveNames).toContain('d4');
|
||||
});
|
||||
|
||||
it('should show correct moves after 1. e4 e6 2. d4', () => {
|
||||
const variations = getOpeningsByFamily('French Defense');
|
||||
const tree = buildVariationTree(variations, 'French Defense');
|
||||
|
||||
const moveHistory = [
|
||||
createMoveEntry('e4', 'white', 1),
|
||||
createMoveEntry('e6', 'black', 1),
|
||||
createMoveEntry('d4', 'white', 2),
|
||||
];
|
||||
|
||||
const possibleMoves = getAllPossibleNextMoves(tree, moveHistory);
|
||||
|
||||
console.log('After 1. e4 e6 2. d4, possible moves for Black:');
|
||||
console.log(possibleMoves.map(m => m.move).join(', '));
|
||||
|
||||
// Black should have d5 as the main option
|
||||
const moveNames = possibleMoves.map(m => m.move);
|
||||
expect(moveNames).toContain('d5');
|
||||
});
|
||||
|
||||
it('should show correct moves after 1. e4 e6 2. d4 d5', () => {
|
||||
const variations = getOpeningsByFamily('French Defense');
|
||||
const tree = buildVariationTree(variations, 'French Defense');
|
||||
|
||||
const moveHistory = [
|
||||
createMoveEntry('e4', 'white', 1),
|
||||
createMoveEntry('e6', 'black', 1),
|
||||
createMoveEntry('d4', 'white', 2),
|
||||
createMoveEntry('d5', 'black', 2),
|
||||
];
|
||||
|
||||
const possibleMoves = getAllPossibleNextMoves(tree, moveHistory);
|
||||
|
||||
console.log('After 1. e4 e6 2. d4 d5, possible 3rd moves for White:');
|
||||
const moveNames = possibleMoves.map(m => m.move).sort();
|
||||
console.log(moveNames.join(', '));
|
||||
|
||||
// These are the moves from the earlier test
|
||||
const expectedMoves = ['Nd2', 'Nc3', 'e5', 'exd5', 'Qe2', 'Nf3', 'c4', 'Be3', 'Nh3', 'Bd3'];
|
||||
|
||||
expectedMoves.forEach(move => {
|
||||
expect(moveNames).toContain(move);
|
||||
});
|
||||
|
||||
// Make sure we don't have any random moves
|
||||
expect(moveNames.length).toBeGreaterThan(0);
|
||||
console.log(`Total ${moveNames.length} possible moves found`);
|
||||
});
|
||||
|
||||
it('should parse moves correctly from opening string', () => {
|
||||
// Test the parseMoveSequence function
|
||||
const testOpening = '1. e4 e6 2. d4 d5 3. Nd2';
|
||||
const moves = parseMoveSequence(testOpening);
|
||||
|
||||
console.log('Parsed moves from "1. e4 e6 2. d4 d5 3. Nd2":');
|
||||
console.log(moves);
|
||||
|
||||
expect(moves).toEqual(['e4', 'e6', 'd4', 'd5', 'Nd2']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { getOpeningsByFamily, getAllOpenings } from '../openingLoader';
|
||||
|
||||
describe('openingLoader - French Defense', () => {
|
||||
it('should load French Defense variations', () => {
|
||||
const frenchVariations = getOpeningsByFamily('French Defense');
|
||||
|
||||
console.log('French Defense variations found:', frenchVariations.length);
|
||||
console.log('First 5 variations:');
|
||||
frenchVariations.slice(0, 5).forEach(v => {
|
||||
console.log(` - ${v.name} (${v.eco}): ${v.moves}`);
|
||||
});
|
||||
|
||||
expect(frenchVariations.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have moves starting with e4 e6', () => {
|
||||
const frenchVariations = getOpeningsByFamily('French Defense');
|
||||
|
||||
// All French Defense variations should start with 1. e4 e6 (allowing for extra spaces)
|
||||
const allStartCorrectly = frenchVariations.every(v => {
|
||||
const normalized = v.moves.replace(/\s+/g, ' ').trim();
|
||||
return normalized.startsWith('1. e4 e6') || normalized.startsWith('1. e4 c5'); // Marshall Gambit starts differently
|
||||
});
|
||||
|
||||
if (!allStartCorrectly) {
|
||||
console.log('Variations NOT starting with e4 e6:');
|
||||
frenchVariations
|
||||
.filter(v => {
|
||||
const normalized = v.moves.replace(/\s+/g, ' ').trim();
|
||||
return !normalized.startsWith('1. e4 e6') && !normalized.startsWith('1. e4 c5');
|
||||
})
|
||||
.slice(0, 3)
|
||||
.forEach(v => {
|
||||
console.log(` - ${v.name}: ${v.moves}`);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allStartCorrectly).toBe(true);
|
||||
});
|
||||
|
||||
it('should find variation after 1. e4 e6 2. d4', () => {
|
||||
const frenchVariations = getOpeningsByFamily('French Defense');
|
||||
|
||||
// Find variations that have at least 2. d4
|
||||
const withD4 = frenchVariations.filter(v =>
|
||||
v.moves.includes('2. d4')
|
||||
);
|
||||
|
||||
console.log(`Variations with 2. d4: ${withD4.length}`);
|
||||
console.log('Examples:');
|
||||
withD4.slice(0, 5).forEach(v => {
|
||||
console.log(` - ${v.name}: ${v.moves.split(' ').slice(0, 8).join(' ')}...`);
|
||||
});
|
||||
|
||||
expect(withD4.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should show what moves are available after 1. e4 e6 2. d4 d5', () => {
|
||||
const frenchVariations = getOpeningsByFamily('French Defense');
|
||||
|
||||
// Find all variations with 1. e4 e6 2. d4 d5
|
||||
const afterD5 = frenchVariations.filter(v =>
|
||||
v.moves.startsWith('1. e4 e6 2. d4 d5')
|
||||
);
|
||||
|
||||
console.log(`Variations after 1. e4 e6 2. d4 d5: ${afterD5.length}`);
|
||||
|
||||
// Get all possible 3rd moves for White
|
||||
const thirdMoves = new Set<string>();
|
||||
afterD5.forEach(v => {
|
||||
const moves = v.moves.split(' ');
|
||||
// Find "3." and get the next move
|
||||
const thirdMoveIndex = moves.findIndex(m => m === '3.');
|
||||
if (thirdMoveIndex >= 0 && moves[thirdMoveIndex + 1]) {
|
||||
thirdMoves.add(moves[thirdMoveIndex + 1]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Possible 3rd moves for White:', Array.from(thirdMoves).join(', '));
|
||||
|
||||
expect(thirdMoves.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user