Skip to content

Commit 4d5b7f2

Browse files
Merge pull request #124 from CSSLab/dev
Add ability to analyze entire game + fix position evaluation graph
2 parents 108c665 + a1a17dc commit 4d5b7f2

File tree

19 files changed

+813
-27
lines changed

19 files changed

+813
-27
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react'
2+
import { render, screen } from '@testing-library/react'
3+
import { AnalysisConfigModal } from 'src/components/Analysis/AnalysisConfigModal'
4+
import { AnalysisProgressOverlay } from 'src/components/Analysis/AnalysisProgressOverlay'
5+
import '@testing-library/jest-dom'
6+
7+
// Mock framer-motion to avoid animation issues in tests
8+
jest.mock('framer-motion', () => ({
9+
motion: {
10+
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
11+
},
12+
AnimatePresence: ({ children }: any) => <>{children}</>,
13+
}))
14+
15+
describe('Analyze Entire Game Components', () => {
16+
describe('AnalysisConfigModal', () => {
17+
const defaultProps = {
18+
isOpen: true,
19+
onClose: jest.fn(),
20+
onConfirm: jest.fn(),
21+
initialDepth: 15,
22+
}
23+
24+
it('renders the modal when open', () => {
25+
render(<AnalysisConfigModal {...defaultProps} />)
26+
27+
expect(screen.getByText('Analyze Entire Game')).toBeInTheDocument()
28+
expect(
29+
screen.getByText(
30+
'Choose the Stockfish analysis depth for all positions in the game:',
31+
),
32+
).toBeInTheDocument()
33+
})
34+
35+
it('renders depth options', () => {
36+
render(<AnalysisConfigModal {...defaultProps} />)
37+
38+
expect(screen.getByText('Fast (d12)')).toBeInTheDocument()
39+
expect(screen.getByText('Balanced (d15)')).toBeInTheDocument()
40+
expect(screen.getByText('Deep (d18)')).toBeInTheDocument()
41+
})
42+
43+
it('renders start analysis button', () => {
44+
render(<AnalysisConfigModal {...defaultProps} />)
45+
46+
expect(screen.getByText('Start Analysis')).toBeInTheDocument()
47+
expect(screen.getByText('Cancel')).toBeInTheDocument()
48+
})
49+
50+
it('does not render when closed', () => {
51+
render(<AnalysisConfigModal {...defaultProps} isOpen={false} />)
52+
53+
expect(screen.queryByText('Analyze Entire Game')).not.toBeInTheDocument()
54+
})
55+
})
56+
57+
describe('AnalysisProgressOverlay', () => {
58+
const mockProgress = {
59+
currentMoveIndex: 5,
60+
totalMoves: 20,
61+
currentMove: 'e4',
62+
isAnalyzing: true,
63+
isComplete: false,
64+
isCancelled: false,
65+
}
66+
67+
const defaultProps = {
68+
progress: mockProgress,
69+
onCancel: jest.fn(),
70+
}
71+
72+
it('renders progress overlay when analyzing', () => {
73+
render(<AnalysisProgressOverlay {...defaultProps} />)
74+
75+
expect(screen.getByText('Analyzing Game')).toBeInTheDocument()
76+
expect(
77+
screen.getByText('Deep analysis in progress...'),
78+
).toBeInTheDocument()
79+
expect(screen.getByText('Position 5 of 20')).toBeInTheDocument()
80+
expect(screen.getByText('25%')).toBeInTheDocument()
81+
})
82+
83+
it('renders current move being analyzed', () => {
84+
render(<AnalysisProgressOverlay {...defaultProps} />)
85+
86+
expect(screen.getByText('Currently analyzing:')).toBeInTheDocument()
87+
expect(screen.getByText('e4')).toBeInTheDocument()
88+
})
89+
90+
it('renders cancel button', () => {
91+
render(<AnalysisProgressOverlay {...defaultProps} />)
92+
93+
expect(screen.getByText('Cancel Analysis')).toBeInTheDocument()
94+
})
95+
96+
it('does not render when not analyzing', () => {
97+
const notAnalyzingProgress = {
98+
...mockProgress,
99+
isAnalyzing: false,
100+
}
101+
102+
render(
103+
<AnalysisProgressOverlay
104+
{...defaultProps}
105+
progress={notAnalyzingProgress}
106+
/>,
107+
)
108+
109+
expect(screen.queryByText('Analyzing Game')).not.toBeInTheDocument()
110+
})
111+
})
112+
})
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { GameTree, GameNode } from 'src/types'
2+
import { Chess } from 'chess.ts'
3+
4+
/**
5+
* Test to verify that evaluation chart generation starts from the correct position
6+
* This test validates the fix for issue #118 where the position evaluation graph
7+
* was showing pre-opening moves that the player didn't actually play.
8+
*/
9+
describe('useOpeningDrillController evaluation chart generation', () => {
10+
// Helper function to simulate the extractNodeAnalysis logic
11+
const extractNodeAnalysisFromPosition = (
12+
startingNode: GameNode,
13+
playerColor: 'white' | 'black',
14+
) => {
15+
const moveAnalyses: Array<{
16+
move: string
17+
san: string
18+
fen: string
19+
isPlayerMove: boolean
20+
evaluation: number
21+
moveNumber: number
22+
}> = []
23+
const evaluationChart: Array<{
24+
moveNumber: number
25+
evaluation: number
26+
isPlayerMove: boolean
27+
}> = []
28+
29+
const extractNodeAnalysis = (
30+
node: GameNode,
31+
path: GameNode[] = [],
32+
): void => {
33+
const currentPath = [...path, node]
34+
35+
if (node.move && node.san) {
36+
const moveIndex = currentPath.length - 2
37+
const isPlayerMove =
38+
playerColor === 'white' ? moveIndex % 2 === 0 : moveIndex % 2 === 1
39+
40+
// Mock evaluation data
41+
const evaluation = Math.random() * 200 - 100 // Random evaluation between -100 and 100
42+
43+
const moveAnalysis = {
44+
move: node.move,
45+
san: node.san,
46+
fen: node.fen,
47+
isPlayerMove,
48+
evaluation,
49+
moveNumber: Math.ceil((moveIndex + 1) / 2),
50+
}
51+
52+
moveAnalyses.push(moveAnalysis)
53+
54+
const evaluationPoint = {
55+
moveNumber: moveAnalysis.moveNumber,
56+
evaluation,
57+
isPlayerMove,
58+
}
59+
60+
evaluationChart.push(evaluationPoint)
61+
}
62+
63+
if (node.children.length > 0) {
64+
extractNodeAnalysis(node.children[0], currentPath)
65+
}
66+
}
67+
68+
extractNodeAnalysis(startingNode)
69+
return { moveAnalyses, evaluationChart }
70+
}
71+
72+
it('should start analysis from opening end node rather than game root', () => {
73+
// Create a game tree representing: 1. e4 e5 2. Nf3 Nc6 (opening) 3. Bb5 a6 (drill moves)
74+
const chess = new Chess()
75+
const gameTree = new GameTree(chess.fen())
76+
77+
// Add opening moves (these should NOT be included in evaluation chart)
78+
chess.move('e4')
79+
const e4Node = gameTree.addMainMove(
80+
gameTree.getRoot(),
81+
chess.fen(),
82+
'e2e4',
83+
'e4',
84+
)!
85+
86+
chess.move('e5')
87+
const e5Node = gameTree.addMainMove(e4Node, chess.fen(), 'e7e5', 'e5')!
88+
89+
chess.move('Nf3')
90+
const nf3Node = gameTree.addMainMove(e5Node, chess.fen(), 'g1f3', 'Nf3')!
91+
92+
chess.move('Nc6')
93+
const nc6Node = gameTree.addMainMove(nf3Node, chess.fen(), 'b8c6', 'Nc6')! // This is the opening end
94+
95+
// Add drill moves (these SHOULD be included in evaluation chart)
96+
chess.move('Bb5')
97+
const bb5Node = gameTree.addMainMove(nc6Node, chess.fen(), 'f1b5', 'Bb5')!
98+
99+
chess.move('a6')
100+
const a6Node = gameTree.addMainMove(bb5Node, chess.fen(), 'a7a6', 'a6')!
101+
102+
// Test starting from game root (old behavior - should include all moves)
103+
const { moveAnalyses: rootAnalyses, evaluationChart: rootChart } =
104+
extractNodeAnalysisFromPosition(gameTree.getRoot(), 'white')
105+
106+
// Test starting from opening end (new behavior - should only include drill moves)
107+
const {
108+
moveAnalyses: openingEndAnalyses,
109+
evaluationChart: openingEndChart,
110+
} = extractNodeAnalysisFromPosition(nc6Node, 'white')
111+
112+
// Verify that starting from root includes all moves (including opening)
113+
expect(rootAnalyses).toHaveLength(6) // e4, e5, Nf3, Nc6, Bb5, a6
114+
expect(rootChart).toHaveLength(6)
115+
116+
// Verify that starting from opening end only includes post-opening moves
117+
// Note: This includes the last opening move (Nc6) which provides context for the evaluation chart
118+
expect(openingEndAnalyses).toHaveLength(3) // Nc6 (last opening move), Bb5, a6
119+
expect(openingEndChart).toHaveLength(3)
120+
121+
// Verify the moves are correct - the first should be the last opening move, then drill moves
122+
expect(openingEndAnalyses[0].san).toBe('Nc6') // Last opening move
123+
expect(openingEndAnalyses[1].san).toBe('Bb5') // First drill move
124+
expect(openingEndAnalyses[1].isPlayerMove).toBe(true) // White's move
125+
expect(openingEndAnalyses[2].san).toBe('a6') // Second drill move
126+
expect(openingEndAnalyses[2].isPlayerMove).toBe(false) // Black's move
127+
128+
// Verify evaluation chart matches move analyses
129+
expect(openingEndChart[0].moveNumber).toBe(openingEndAnalyses[0].moveNumber)
130+
expect(openingEndChart[1].moveNumber).toBe(openingEndAnalyses[1].moveNumber)
131+
})
132+
133+
it('should handle the case where opening end node is null', () => {
134+
const chess = new Chess()
135+
const gameTree = new GameTree(chess.fen())
136+
137+
// Add some moves
138+
chess.move('e4')
139+
const e4Node = gameTree.addMainMove(
140+
gameTree.getRoot(),
141+
chess.fen(),
142+
'e2e4',
143+
'e4',
144+
)!
145+
146+
// Test with null opening end node (should fallback to root)
147+
const startingNode = null || gameTree.getRoot() // Simulates the fallback logic
148+
const { moveAnalyses, evaluationChart } = extractNodeAnalysisFromPosition(
149+
startingNode,
150+
'white',
151+
)
152+
153+
expect(moveAnalyses).toHaveLength(1)
154+
expect(evaluationChart).toHaveLength(1)
155+
expect(moveAnalyses[0].san).toBe('e4')
156+
})
157+
})

0 commit comments

Comments
 (0)