Skip to content

Commit 3426fbd

Browse files
chore: clean up lib/analysis
1 parent fb6cd50 commit 3426fbd

9 files changed

Lines changed: 239 additions & 746 deletions

File tree

src/hooks/useAnalysisController/useAnalysisController.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import { useMoveRecommendations } from './useMoveRecommendations'
2020
import { MaiaEngineContext } from 'src/contexts/MaiaEngineContext'
2121
import { generateColorSanMapping, calculateBlunderMeter } from './utils'
2222
import { StockfishEngineContext } from 'src/contexts/StockfishEngineContext'
23+
import { storeGameAnalysisCache } from 'src/api/analysis'
2324
import {
25+
extractPlayerMistakes,
26+
isBestMove,
2427
collectEngineAnalysisData,
2528
generateAnalysisCacheKey,
26-
} from 'src/lib/analysisStorage'
27-
import { storeGameAnalysisCache } from 'src/api/analysis'
28-
import { extractPlayerMistakes, isBestMove } from 'src/lib/analysis'
29+
} from 'src/lib/analysis'
2930
import { LearnFromMistakesState, MistakePosition } from 'src/types/analysis'
3031
import { LEARN_FROM_MISTAKES_DEPTH } from 'src/constants/analysis'
3132

src/lib/analysis.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { Chess } from 'chess.ts'
12
import { cpToWinrate } from './stockfish'
23
import {
34
GameTree,
45
GameNode,
56
RawMove,
7+
MistakePosition,
68
MoveValueMapping,
79
StockfishEvaluation,
10+
CachedEngineAnalysisEntry,
811
} from 'src/types'
912

1013
export function convertBackendEvalToStockfishEval(
@@ -108,3 +111,233 @@ export function insertBackendStockfishEvalToGameTree(
108111
currentNode = currentNode?.mainChild
109112
}
110113
}
114+
115+
export const collectEngineAnalysisData = (
116+
gameTree: GameTree,
117+
): CachedEngineAnalysisEntry[] => {
118+
const positions: CachedEngineAnalysisEntry[] = []
119+
const mainLine = gameTree.getMainLine()
120+
121+
mainLine.forEach((node, index) => {
122+
if (!node.analysis.maia && !node.analysis.stockfish) {
123+
return
124+
}
125+
126+
const position: CachedEngineAnalysisEntry = {
127+
ply: index,
128+
fen: node.fen,
129+
}
130+
131+
if (node.analysis.maia) {
132+
position.maia = node.analysis.maia
133+
}
134+
135+
if (node.analysis.stockfish) {
136+
position.stockfish = {
137+
depth: node.analysis.stockfish.depth,
138+
cp_vec: node.analysis.stockfish.cp_vec,
139+
}
140+
}
141+
142+
positions.push(position)
143+
})
144+
145+
return positions
146+
}
147+
148+
const reconstructCachedStockfishAnalysis = (
149+
cpVec: { [move: string]: number },
150+
depth: number,
151+
fen: string,
152+
) => {
153+
const board = new Chess(fen)
154+
const isBlackTurn = board.turn() === 'b'
155+
156+
let bestCp = isBlackTurn ? Infinity : -Infinity
157+
let bestMove = ''
158+
159+
for (const move in cpVec) {
160+
const cp = cpVec[move]
161+
if (isBlackTurn) {
162+
if (cp < bestCp) {
163+
bestCp = cp
164+
bestMove = move
165+
}
166+
} else {
167+
if (cp > bestCp) {
168+
bestCp = cp
169+
bestMove = move
170+
}
171+
}
172+
}
173+
174+
const cp_relative_vec: { [move: string]: number } = {}
175+
for (const move in cpVec) {
176+
const cp = cpVec[move]
177+
cp_relative_vec[move] = isBlackTurn ? bestCp - cp : cp - bestCp
178+
}
179+
180+
const winrate_vec: { [move: string]: number } = {}
181+
for (const move in cpVec) {
182+
const cp = cpVec[move]
183+
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
184+
winrate_vec[move] = winrate
185+
}
186+
187+
let bestWinrate = -Infinity
188+
for (const move in winrate_vec) {
189+
const wr = winrate_vec[move]
190+
if (wr > bestWinrate) {
191+
bestWinrate = wr
192+
}
193+
}
194+
195+
const winrate_loss_vec: { [move: string]: number } = {}
196+
for (const move in winrate_vec) {
197+
winrate_loss_vec[move] = winrate_vec[move] - bestWinrate
198+
}
199+
200+
const sortedEntries = Object.entries(winrate_vec).sort(
201+
([, a], [, b]) => b - a,
202+
)
203+
204+
const sortedWinrateVec = Object.fromEntries(sortedEntries)
205+
const sortedWinrateLossVec = Object.fromEntries(
206+
sortedEntries.map(([move]) => [move, winrate_loss_vec[move]]),
207+
)
208+
209+
return {
210+
sent: true,
211+
depth,
212+
model_move: bestMove,
213+
model_optimal_cp: bestCp,
214+
cp_vec: cpVec,
215+
cp_relative_vec,
216+
winrate_vec: sortedWinrateVec,
217+
winrate_loss_vec: sortedWinrateLossVec,
218+
}
219+
}
220+
221+
export const applyEngineAnalysisData = (
222+
gameTree: GameTree,
223+
analysisData: CachedEngineAnalysisEntry[],
224+
): void => {
225+
const mainLine = gameTree.getMainLine()
226+
227+
analysisData.forEach((positionData) => {
228+
const { ply, maia, stockfish } = positionData
229+
230+
if (ply >= 0 && ply < mainLine.length) {
231+
const node = mainLine[ply]
232+
233+
if (node.fen === positionData.fen) {
234+
if (maia) {
235+
node.addMaiaAnalysis(maia)
236+
}
237+
238+
if (stockfish) {
239+
const stockfishEval = reconstructCachedStockfishAnalysis(
240+
stockfish.cp_vec,
241+
stockfish.depth,
242+
node.fen,
243+
)
244+
245+
if (
246+
!node.analysis.stockfish ||
247+
node.analysis.stockfish.depth < stockfish.depth
248+
) {
249+
node.addStockfishAnalysis(stockfishEval)
250+
}
251+
}
252+
}
253+
}
254+
})
255+
}
256+
257+
export const generateAnalysisCacheKey = (
258+
analysisData: CachedEngineAnalysisEntry[],
259+
): string => {
260+
const keyData = analysisData.map((pos) => ({
261+
ply: pos.ply,
262+
fen: pos.fen,
263+
hasStockfish: !!pos.stockfish,
264+
stockfishDepth: pos.stockfish?.depth || 0,
265+
hasMaia: !!pos.maia,
266+
maiaModels: pos.maia ? Object.keys(pos.maia).sort() : [],
267+
}))
268+
269+
return JSON.stringify(keyData)
270+
}
271+
272+
export function extractPlayerMistakes(
273+
gameTree: GameTree,
274+
playerColor: 'white' | 'black',
275+
): MistakePosition[] {
276+
const mainLine = gameTree.getMainLine()
277+
const mistakes: MistakePosition[] = []
278+
279+
for (let i = 1; i < mainLine.length; i++) {
280+
const node = mainLine[i]
281+
const isPlayerMove = node.turn === (playerColor === 'white' ? 'b' : 'w')
282+
283+
if (
284+
isPlayerMove &&
285+
(node.blunder || node.inaccuracy) &&
286+
node.move &&
287+
node.san
288+
) {
289+
const parentNode = node.parent
290+
if (!parentNode) continue
291+
292+
const stockfishEval = parentNode.analysis.stockfish
293+
if (!stockfishEval || !stockfishEval.model_move) continue
294+
295+
const chess = new Chess(parentNode.fen)
296+
const bestMoveResult = chess.move(stockfishEval.model_move, {
297+
sloppy: true,
298+
})
299+
if (!bestMoveResult) continue
300+
301+
mistakes.push({
302+
nodeId: `move-${i}`, // Simple ID based on position in main line
303+
moveIndex: i, // Index of the mistake node in the main line
304+
fen: parentNode.fen, // Position before the mistake
305+
playedMove: node.move,
306+
san: node.san,
307+
type: node.blunder ? 'blunder' : 'inaccuracy',
308+
bestMove: stockfishEval.model_move,
309+
bestMoveSan: bestMoveResult.san,
310+
playerColor,
311+
})
312+
}
313+
}
314+
315+
return mistakes
316+
}
317+
318+
export function getBestMoveForPosition(node: GameNode): {
319+
move: string
320+
san: string
321+
} | null {
322+
const stockfishEval = node.analysis.stockfish
323+
if (!stockfishEval || !stockfishEval.model_move) {
324+
return null
325+
}
326+
327+
const chess = new Chess(node.fen)
328+
const moveResult = chess.move(stockfishEval.model_move, { sloppy: true })
329+
330+
if (!moveResult) {
331+
return null
332+
}
333+
334+
return {
335+
move: stockfishEval.model_move,
336+
san: moveResult.san,
337+
}
338+
}
339+
340+
export function isBestMove(node: GameNode, moveUci: string): boolean {
341+
const bestMove = getBestMoveForPosition(node)
342+
return bestMove ? bestMove.move === moveUci : false
343+
}

src/lib/analysis/index.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/lib/analysis/mistakeDetection.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)