diff --git a/src/components/OpeningTrainer/OpeningTrainer.tsx b/src/components/OpeningTrainer/OpeningTrainer.tsx index 59db972..6ce9c3d 100644 --- a/src/components/OpeningTrainer/OpeningTrainer.tsx +++ b/src/components/OpeningTrainer/OpeningTrainer.tsx @@ -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, diff --git a/src/lib/openingTrainer/__tests__/familyTraining.test.ts b/src/lib/openingTrainer/__tests__/familyTraining.test.ts index 29524fa..db8b5db 100644 --- a/src/lib/openingTrainer/__tests__/familyTraining.test.ts +++ b/src/lib/openingTrainer/__tests__/familyTraining.test.ts @@ -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', diff --git a/src/lib/openingTrainer/__tests__/frenchDefenseFamily.test.ts b/src/lib/openingTrainer/__tests__/frenchDefenseFamily.test.ts new file mode 100644 index 0000000..034db95 --- /dev/null +++ b/src/lib/openingTrainer/__tests__/frenchDefenseFamily.test.ts @@ -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']); + }); +}); diff --git a/src/lib/openingTrainer/__tests__/openingLoader.test.ts b/src/lib/openingTrainer/__tests__/openingLoader.test.ts new file mode 100644 index 0000000..53ef467 --- /dev/null +++ b/src/lib/openingTrainer/__tests__/openingLoader.test.ts @@ -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(); + 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); + }); +});