Skip to content

Commit c6396d7

Browse files
feat: load engine analysis from backend
1 parent 2476678 commit c6396d7

File tree

3 files changed

+140
-27
lines changed

3 files changed

+140
-27
lines changed

src/api/analysis/analysis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ export const storeEngineAnalysis = async (
700700
// Retrieve stored engine analysis from backend
701701
export const getEngineAnalysis = async (
702702
gameId: string,
703-
): Promise<EngineAnalysisPosition[] | null> => {
703+
): Promise<{ positions: EngineAnalysisPosition[] } | null> => {
704704
const res = await fetch(buildUrl(`analysis/get_engine_analysis/${gameId}`))
705705

706706
if (res.status === 401) {

src/hooks/useAnalysisController/useAnalysisController.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import { StockfishEngineContext } from 'src/contexts/StockfishEngineContext'
2222
import {
2323
collectEngineAnalysisData,
2424
generateAnalysisCacheKey,
25+
applyEngineAnalysisData,
2526
} from 'src/lib/analysisStorage'
26-
import { storeEngineAnalysis } from 'src/api/analysis/analysis'
27+
import {
28+
storeEngineAnalysis,
29+
getEngineAnalysis,
30+
} from 'src/api/analysis/analysis'
2731

2832
export interface GameAnalysisProgress {
2933
currentMoveIndex: number
@@ -128,6 +132,51 @@ export const useAnalysisController = (
128132
const saveAnalysisToBackendRef = useRef(saveAnalysisToBackend)
129133
saveAnalysisToBackendRef.current = saveAnalysisToBackend
130134

135+
const loadStoredAnalysis = useCallback(async () => {
136+
console.log(
137+
'loadStoredAnalysis called for game:',
138+
game.id,
139+
'type:',
140+
game.type,
141+
)
142+
143+
if (
144+
!game.id ||
145+
game.type === 'custom-pgn' ||
146+
game.type === 'custom-fen' ||
147+
game.type === 'tournament'
148+
) {
149+
console.log('Skipping analysis load - game not eligible')
150+
return
151+
}
152+
153+
try {
154+
console.log('Fetching stored analysis for game:', game.id)
155+
const storedAnalysis = await getEngineAnalysis(game.id)
156+
console.log('Received stored analysis:', storedAnalysis)
157+
158+
if (storedAnalysis && storedAnalysis.positions.length > 0) {
159+
applyEngineAnalysisData(game.tree, storedAnalysis.positions)
160+
setAnalysisState((prev) => prev + 1) // Trigger UI updates
161+
console.log(
162+
'Loaded stored analysis:',
163+
storedAnalysis.positions.length,
164+
'positions',
165+
)
166+
} else {
167+
console.log('No stored analysis found for game:', game.id)
168+
}
169+
} catch (error) {
170+
console.warn('Failed to load stored analysis:', error)
171+
// Don't show error to user as this is background functionality
172+
}
173+
}, [game.id, game.type])
174+
175+
// Load stored analysis when game changes
176+
useEffect(() => {
177+
loadStoredAnalysis()
178+
}, [loadStoredAnalysis])
179+
131180
// Setup auto-save timer
132181
useEffect(() => {
133182
// Clear existing timer
@@ -431,6 +480,7 @@ export const useAnalysisController = (
431480
resetProgress: resetGameAnalysisProgress,
432481
isEnginesReady: stockfish.isReady() && maia.status === 'ready',
433482
saveAnalysis: saveAnalysisToBackend,
483+
loadStoredAnalysis,
434484
},
435485
}
436486
}

src/lib/analysisStorage.ts

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { GameTree, GameNode } from 'src/types'
1+
import { Chess } from 'chess.ts'
2+
import { GameTree } from 'src/types'
23
import { EngineAnalysisPosition } from 'src/api/analysis/analysis'
4+
import { cpToWinrate } from 'src/lib/stockfish'
35

46
/**
57
* Collects analysis data from a game tree to send to the backend
@@ -63,18 +65,11 @@ export const applyEngineAnalysisData = (
6365

6466
// Apply Stockfish analysis
6567
if (stockfish) {
66-
// Create a StockfishEvaluation object with minimal required fields
67-
const stockfishEval = {
68-
sent: true,
69-
depth: stockfish.depth,
70-
model_move: Object.keys(stockfish.cp_vec)[0] || '',
71-
model_optimal_cp: Math.max(
72-
...Object.values(stockfish.cp_vec).map(Number),
73-
0,
74-
),
75-
cp_vec: stockfish.cp_vec,
76-
cp_relative_vec: calculateRelativeCp(stockfish.cp_vec),
77-
}
68+
const stockfishEval = reconstructStockfishEvaluation(
69+
stockfish.cp_vec,
70+
stockfish.depth,
71+
node.fen,
72+
)
7873

7974
// Only apply if we don't have deeper analysis already
8075
if (
@@ -90,19 +85,87 @@ export const applyEngineAnalysisData = (
9085
}
9186

9287
/**
93-
* Helper function to calculate relative centipawn values
88+
* Reconstruct a complete StockfishEvaluation from stored cp_vec using the same logic as the engine
9489
*/
95-
const calculateRelativeCp = (cpVec: {
96-
[move: string]: number
97-
}): { [move: string]: number } => {
98-
const maxCp = Math.max(...Object.values(cpVec))
99-
const relativeCp: { [move: string]: number } = {}
100-
101-
Object.entries(cpVec).forEach(([move, cp]) => {
102-
relativeCp[move] = cp - maxCp
103-
})
104-
105-
return relativeCp
90+
const reconstructStockfishEvaluation = (
91+
cpVec: { [move: string]: number },
92+
depth: number,
93+
fen: string,
94+
) => {
95+
const board = new Chess(fen)
96+
const isBlackTurn = board.turn() === 'b'
97+
98+
// Find the best move and cp (model_move and model_optimal_cp)
99+
let bestCp = isBlackTurn ? Infinity : -Infinity
100+
let bestMove = ''
101+
102+
for (const move in cpVec) {
103+
const cp = cpVec[move]
104+
if (isBlackTurn) {
105+
if (cp < bestCp) {
106+
bestCp = cp
107+
bestMove = move
108+
}
109+
} else {
110+
if (cp > bestCp) {
111+
bestCp = cp
112+
bestMove = move
113+
}
114+
}
115+
}
116+
117+
// Calculate cp_relative_vec using exact same logic as engine.ts:215-217
118+
const cp_relative_vec: { [move: string]: number } = {}
119+
for (const move in cpVec) {
120+
const cp = cpVec[move]
121+
cp_relative_vec[move] = isBlackTurn
122+
? bestCp - cp // Black turn: model_optimal_cp - cp
123+
: cp - bestCp // White turn: cp - model_optimal_cp
124+
}
125+
126+
// Calculate winrate_vec using exact same logic as engine.ts:219 and 233
127+
const winrate_vec: { [move: string]: number } = {}
128+
for (const move in cpVec) {
129+
const cp = cpVec[move]
130+
// Use exact same logic as engine: cp * (isBlackTurn ? -1 : 1)
131+
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
132+
winrate_vec[move] = winrate
133+
}
134+
135+
// Calculate winrate_loss_vec using the same logic as the engine (lines 248-264)
136+
let bestWinrate = -Infinity
137+
for (const move in winrate_vec) {
138+
const wr = winrate_vec[move]
139+
if (wr > bestWinrate) {
140+
bestWinrate = wr
141+
}
142+
}
143+
144+
const winrate_loss_vec: { [move: string]: number } = {}
145+
for (const move in winrate_vec) {
146+
winrate_loss_vec[move] = winrate_vec[move] - bestWinrate
147+
}
148+
149+
// Sort all vectors by winrate (descending) as done in engine.ts:267-281
150+
const sortedEntries = Object.entries(winrate_vec).sort(
151+
([, a], [, b]) => b - a,
152+
)
153+
154+
const sortedWinrateVec = Object.fromEntries(sortedEntries)
155+
const sortedWinrateLossVec = Object.fromEntries(
156+
sortedEntries.map(([move]) => [move, winrate_loss_vec[move]]),
157+
)
158+
159+
return {
160+
sent: true,
161+
depth,
162+
model_move: bestMove,
163+
model_optimal_cp: bestCp,
164+
cp_vec: cpVec,
165+
cp_relative_vec,
166+
winrate_vec: sortedWinrateVec,
167+
winrate_loss_vec: sortedWinrateLossVec,
168+
}
106169
}
107170

108171
/**

0 commit comments

Comments
 (0)