Add comprehensive integration tests for family training mode
Created 21 test cases covering: - Variation tree building - Position navigation - Move validation across variations - Branch point detection - End of line detection All tests pass (21/21).
This commit is contained in:
@@ -0,0 +1,380 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for Family Training Mode
|
||||||
|
* Tests the variation tree and multi-variation support
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildVariationTree,
|
||||||
|
getTreeNodeAtPosition,
|
||||||
|
getMatchingVariations,
|
||||||
|
getAllPossibleNextMoves,
|
||||||
|
isMoveInVariationTree,
|
||||||
|
identifyCurrentVariation,
|
||||||
|
describeCurrentPosition,
|
||||||
|
} from '../gameLogic';
|
||||||
|
import { OpeningMetadata } from '@/lib/openings';
|
||||||
|
import { MoveHistoryEntry } from '@/types/openingTraining';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Data - Italian Game variations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const italianGameVariations: OpeningMetadata[] = [
|
||||||
|
{
|
||||||
|
eco: 'C50',
|
||||||
|
name: 'Italian Game',
|
||||||
|
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4',
|
||||||
|
src: 'test',
|
||||||
|
isEcoRoot: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eco: 'C53',
|
||||||
|
name: 'Italian Game: Classical Variation',
|
||||||
|
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3',
|
||||||
|
src: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eco: 'C54',
|
||||||
|
name: 'Italian Game: Giuoco Piano',
|
||||||
|
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4',
|
||||||
|
src: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eco: 'C55',
|
||||||
|
name: 'Italian Game: Two Knights Defense',
|
||||||
|
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6',
|
||||||
|
src: 'test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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',
|
||||||
|
evaluation: { score: 0, mate: null, depth: 15, bestMove: 'e4', ponder: null },
|
||||||
|
classification: {
|
||||||
|
category: 'in-theory',
|
||||||
|
inRepertoire: true,
|
||||||
|
evaluationChange: 0,
|
||||||
|
isSignificantSwing: false,
|
||||||
|
theoreticalAlternatives: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe('Family Training Mode - Variation Tree', () => {
|
||||||
|
describe('buildVariationTree', () => {
|
||||||
|
it('should build a tree from multiple variations', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
expect(tree.familyName).toBe('Italian Game');
|
||||||
|
expect(tree.allVariations).toHaveLength(4);
|
||||||
|
expect(tree.children.size).toBe(1); // Only 1. e4 at root
|
||||||
|
expect(tree.children.has('e4')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create correct branching structure', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4, there should be 2 branches
|
||||||
|
const e4Node = tree.children.get('e4')!;
|
||||||
|
const e5Node = e4Node.children.get('e5')!;
|
||||||
|
const nf3Node = e5Node.children.get('Nf3')!;
|
||||||
|
const nc6Node = nf3Node.children.get('Nc6')!;
|
||||||
|
const bc4Node = nc6Node.children.get('Bc4')!;
|
||||||
|
|
||||||
|
// After Bc4, should have Bc5 and Nf6 as options
|
||||||
|
expect(bc4Node.children.size).toBe(2);
|
||||||
|
expect(bc4Node.children.has('Bc5')).toBe(true);
|
||||||
|
expect(bc4Node.children.has('Nf6')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track variations at each node', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
const e4Node = tree.children.get('e4')!;
|
||||||
|
// All variations start with e4
|
||||||
|
expect(e4Node.variations).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark end of line correctly', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// Navigate to end of "Italian Game" (shortest variation)
|
||||||
|
const e4Node = tree.children.get('e4')!;
|
||||||
|
const e5Node = e4Node.children.get('e5')!;
|
||||||
|
const nf3Node = e5Node.children.get('Nf3')!;
|
||||||
|
const nc6Node = nf3Node.children.get('Nc6')!;
|
||||||
|
const bc4Node = nc6Node.children.get('Bc4')!;
|
||||||
|
|
||||||
|
expect(bc4Node.isEndOfLine).toBe(true); // End of C50 variation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTreeNodeAtPosition', () => {
|
||||||
|
it('should return root node for empty move history', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const node = getTreeNodeAtPosition(tree, []);
|
||||||
|
|
||||||
|
expect(node).not.toBeNull();
|
||||||
|
expect(node!.variations).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to correct position', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
const node = getTreeNodeAtPosition(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(node).not.toBeNull();
|
||||||
|
expect(node!.move).toBe('Nf3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for moves not in any variation', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('d4', 'white', 1), // Not in Italian Game!
|
||||||
|
];
|
||||||
|
|
||||||
|
const node = getTreeNodeAtPosition(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(node).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMatchingVariations', () => {
|
||||||
|
it('should return all variations at start', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const variations = getMatchingVariations(tree, []);
|
||||||
|
|
||||||
|
expect(variations).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should narrow down variations as moves are played', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
createMoveEntry('Nf6', 'black', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
const variations = getMatchingVariations(tree, moveHistory);
|
||||||
|
|
||||||
|
// Only Two Knights Defense
|
||||||
|
expect(variations).toHaveLength(1);
|
||||||
|
expect(variations[0].name).toBe('Italian Game: Two Knights Defense');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for off-book moves', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('c5', 'black', 1), // Sicilian, not Italian!
|
||||||
|
];
|
||||||
|
|
||||||
|
const variations = getMatchingVariations(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(variations).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllPossibleNextMoves', () => {
|
||||||
|
it('should return all possible first moves', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const moves = getAllPossibleNextMoves(tree, []);
|
||||||
|
|
||||||
|
expect(moves).toHaveLength(1);
|
||||||
|
expect(moves[0].move).toBe('e4');
|
||||||
|
expect(moves[0].variations).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return multiple options at branch points', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
const moves = getAllPossibleNextMoves(tree, moveHistory);
|
||||||
|
|
||||||
|
// Should have Bc5 (Classical/Giuoco Piano) and Nf6 (Two Knights)
|
||||||
|
expect(moves).toHaveLength(2);
|
||||||
|
const moveNames = moves.map(m => m.move).sort();
|
||||||
|
expect(moveNames).toEqual(['Bc5', 'Nf6']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array at end of all variations', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// Go to end of longest variation (Giuoco Piano)
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
createMoveEntry('Bc5', 'black', 3),
|
||||||
|
createMoveEntry('c3', 'white', 4),
|
||||||
|
createMoveEntry('Nf6', 'black', 4),
|
||||||
|
createMoveEntry('d4', 'white', 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
const moves = getAllPossibleNextMoves(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(moves).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isMoveInVariationTree', () => {
|
||||||
|
it('should return true for valid moves', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
const isValid = isMoveInVariationTree(tree, [], 'e4');
|
||||||
|
expect(isValid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid moves', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
const isValid = isMoveInVariationTree(tree, [], 'd4');
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate moves at branch points', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(isMoveInVariationTree(tree, moveHistory, 'Bc5')).toBe(true);
|
||||||
|
expect(isMoveInVariationTree(tree, moveHistory, 'Nf6')).toBe(true);
|
||||||
|
expect(isMoveInVariationTree(tree, moveHistory, 'd6')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('describeCurrentPosition', () => {
|
||||||
|
it('should describe starting position', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
const desc = describeCurrentPosition(tree, []);
|
||||||
|
|
||||||
|
expect(desc.matchingCount).toBe(4);
|
||||||
|
expect(desc.isEndOfLine).toBe(false);
|
||||||
|
expect(desc.nextMoves).toContain('e4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify branch points', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
const desc = describeCurrentPosition(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(desc.matchingCount).toBe(4); // All 4 variations pass through this position
|
||||||
|
expect(desc.nextMoves).toHaveLength(2);
|
||||||
|
expect(desc.nextMoves).toContain('Bc5');
|
||||||
|
expect(desc.nextMoves).toContain('Nf6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect end of line', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// Go to actual end of a line (Giuoco Piano)
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
createMoveEntry('Bc5', 'black', 3),
|
||||||
|
createMoveEntry('c3', 'white', 4),
|
||||||
|
createMoveEntry('Nf6', 'black', 4),
|
||||||
|
createMoveEntry('d4', 'white', 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
const desc = describeCurrentPosition(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(desc.isEndOfLine).toBe(true); // End of Giuoco Piano
|
||||||
|
expect(desc.matchingCount).toBe(1); // Only Giuoco Piano reaches here
|
||||||
|
expect(desc.nextMoves).toHaveLength(0); // No more moves in repertoire
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('identifyCurrentVariation', () => {
|
||||||
|
it('should identify specific variation', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// Two Knights Defense
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
createMoveEntry('Nf6', 'black', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = identifyCurrentVariation(tree, moveHistory);
|
||||||
|
|
||||||
|
expect(result.exact).toHaveLength(1);
|
||||||
|
expect(result.exact[0].name).toBe('Italian Game: Two Knights Defense');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return multiple variations when moves overlap', () => {
|
||||||
|
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
|
||||||
|
|
||||||
|
// After Bc5, both Classical and Giuoco Piano are possible
|
||||||
|
const moveHistory = [
|
||||||
|
createMoveEntry('e4', 'white', 1),
|
||||||
|
createMoveEntry('e5', 'black', 1),
|
||||||
|
createMoveEntry('Nf3', 'white', 2),
|
||||||
|
createMoveEntry('Nc6', 'black', 2),
|
||||||
|
createMoveEntry('Bc4', 'white', 3),
|
||||||
|
createMoveEntry('Bc5', 'black', 3),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = identifyCurrentVariation(tree, moveHistory);
|
||||||
|
|
||||||
|
// No exact matches at this position, but multiple possible continuations
|
||||||
|
expect(result.possible.length).toBeGreaterThanOrEqual(2);
|
||||||
|
const names = result.possible.map(v => v.name);
|
||||||
|
expect(names).toContain('Italian Game: Classical Variation');
|
||||||
|
expect(names).toContain('Italian Game: Giuoco Piano');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user