From 5379893288d7e1707830efb3ba0cf334913a5136 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sun, 14 Apr 2024 19:12:59 +0200 Subject: [PATCH] feat : add live move classification --- src/lib/engine/uciEngine.ts | 11 ++- .../analysis/hooks/useCurrentPosition.ts | 97 ++++++++++++++++--- .../reviewPanelHeader/analyzeButton.tsx | 14 ++- src/sections/analysis/states.ts | 4 +- src/types/eval.ts | 10 +- 5 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index 273ffbc..8a35f13 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -213,7 +213,7 @@ export abstract class UciEngine { depth = 16, multiPv = this.multiPv, setPartialEval, - }: EvaluatePositionWithUpdateParams): Promise { + }: EvaluatePositionWithUpdateParams): Promise { this.throwErrorIfNotReady(); const lichessEvalPromise = getLichessEval(fen, multiPv); @@ -224,6 +224,7 @@ export abstract class UciEngine { const whiteToPlay = fen.split(" ")[1] === "w"; const onNewMessage = (messages: string[]) => { + if (!setPartialEval) return; const parsedResults = parseEvaluationResults(messages, whiteToPlay); setPartialEval(parsedResults); }; @@ -235,15 +236,17 @@ export abstract class UciEngine { lichessEval.lines.length >= multiPv && lichessEval.lines[0].depth >= depth ) { - setPartialEval(lichessEval); - return; + setPartialEval?.(lichessEval); + return lichessEval; } - await this.sendCommands( + const results = await this.sendCommands( [`position fen ${fen}`, `go depth ${depth}`], "bestmove", onNewMessage ); + + return parseEvaluationResults(results, whiteToPlay); } public async getEngineNextMove( diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index f24d2a4..03a8781 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -5,12 +5,15 @@ import { engineMultiPvAtom, gameAtom, gameEvalAtom, + savedEvalsAtom, } from "@/sections/analysis/states"; import { CurrentPosition, PositionEval } from "@/types/eval"; import { useAtom, useAtomValue } from "jotai"; import { useEffect } from "react"; import { useEngine } from "../../../hooks/useEngine"; import { EngineName } from "@/types/enums"; +import { getEvaluateGameParams } from "@/lib/chess"; +import { getMovesClassification } from "@/lib/engine/helpers/moveClassification"; export const useCurrentPosition = (engineName?: EngineName) => { const [currentPosition, setCurrentPosition] = useAtom(currentPositionAtom); @@ -20,6 +23,7 @@ export const useCurrentPosition = (engineName?: EngineName) => { const board = useAtomValue(boardAtom); const depth = useAtomValue(engineDepthAtom); const multiPv = useAtomValue(engineMultiPvAtom); + const [savedEvals, setSavedEvals] = useAtom(savedEvalsAtom); useEffect(() => { const position: CurrentPosition = { @@ -44,21 +48,92 @@ export const useCurrentPosition = (engineName?: EngineName) => { } } - if (!position.eval && engine?.isReady()) { - const setPartialEval = (positionEval: PositionEval) => { - setCurrentPosition({ ...position, eval: positionEval }); + setCurrentPosition(position); + + if (!position.eval && engine?.isReady() && engineName) { + const getFenEngineEval = async ( + fen: string, + setPartialEval?: (positionEval: PositionEval) => void + ) => { + if (!engine?.isReady() || !engineName) + throw new Error("Engine not ready"); + const savedEval = savedEvals[fen]; + if ( + savedEval && + savedEval.engine === engineName && + savedEval.lines[0].depth >= depth + ) { + setPartialEval?.(savedEval); + return savedEval; + } + + const rawPositionEval = await engine.evaluatePositionWithUpdate({ + fen, + depth, + multiPv, + setPartialEval, + }); + + setSavedEvals((prev) => ({ + ...prev, + [fen]: { ...rawPositionEval, engine: engineName }, + })); + + return rawPositionEval; }; - engine.evaluatePositionWithUpdate({ - fen: board.fen(), - depth, - multiPv, - setPartialEval, - }); + const getPositionEval = async () => { + const setPartialEval = (positionEval: PositionEval) => { + setCurrentPosition({ ...position, eval: positionEval }); + }; + const rawPositionEval = await getFenEngineEval( + board.fen(), + setPartialEval + ); + + if (boardHistory.length === 0) return; + + const params = getEvaluateGameParams(board); + const fens = params.fens.slice(board.turn() === "w" ? -3 : -4); + const uciMoves = params.uciMoves.slice(board.turn() === "w" ? -3 : -4); + + const lastRawEval = await getFenEngineEval(fens.slice(-2)[0]); + const rawPositions: PositionEval[] = fens.map((_, idx) => { + if (idx === fens.length - 2) return lastRawEval; + if (idx === fens.length - 1) return rawPositionEval; + return { + lines: [ + { + pv: [], + depth: 0, + multiPv: 1, + cp: 1, + }, + ], + }; + }); + + const positionsWithMoveClassification = getMovesClassification( + rawPositions, + uciMoves, + fens + ); + + setCurrentPosition({ + ...position, + eval: positionsWithMoveClassification.slice(-1)[0], + lastEval: positionsWithMoveClassification.slice(-2)[0], + }); + }; + + getPositionEval(); } - setCurrentPosition(position); - }, [gameEval, board, game, engine, depth, multiPv, setCurrentPosition]); + return () => { + engine?.stopSearch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [gameEval, board, game, engine, depth, multiPv]); return currentPosition; }; diff --git a/src/sections/analysis/reviewPanelHeader/analyzeButton.tsx b/src/sections/analysis/reviewPanelHeader/analyzeButton.tsx index 200c1b1..8cf56ee 100644 --- a/src/sections/analysis/reviewPanelHeader/analyzeButton.tsx +++ b/src/sections/analysis/reviewPanelHeader/analyzeButton.tsx @@ -6,13 +6,15 @@ import { evaluationProgressAtom, gameAtom, gameEvalAtom, + savedEvalsAtom, } from "../states"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { getEvaluateGameParams } from "@/lib/chess"; import { useGameDatabase } from "@/hooks/useGameDatabase"; import { LoadingButton } from "@mui/lab"; import { useEngine } from "@/hooks/useEngine"; import { logAnalyticsEvent } from "@/lib/firebase"; +import { SavedEvals } from "@/types/eval"; export default function AnalyzeButton() { const engineName = useAtomValue(engineNameAtom); @@ -25,6 +27,7 @@ export default function AnalyzeButton() { const { setGameEval, gameFromUrl } = useGameDatabase(); const [gameEval, setEval] = useAtom(gameEvalAtom); const game = useAtomValue(gameAtom); + const setSavedEvals = useSetAtom(savedEvalsAtom); const readyToAnalyse = engine?.isReady() && game.history().length > 0 && !evaluationProgress; @@ -49,6 +52,15 @@ export default function AnalyzeButton() { setGameEval(gameFromUrl.id, newGameEval); } + const gameSavedEvals: SavedEvals = params.fens.reduce((acc, fen, idx) => { + acc[fen] = { ...newGameEval.positions[idx], engine: engineName }; + return acc; + }, {} as SavedEvals); + setSavedEvals((prev) => ({ + ...prev, + ...gameSavedEvals, + })); + logAnalyticsEvent("analyze_game", { engine: engineName, depth: engineDepth, diff --git a/src/sections/analysis/states.ts b/src/sections/analysis/states.ts index e8b89a0..cb56306 100644 --- a/src/sections/analysis/states.ts +++ b/src/sections/analysis/states.ts @@ -1,5 +1,5 @@ import { EngineName } from "@/types/enums"; -import { CurrentPosition, GameEval } from "@/types/eval"; +import { CurrentPosition, GameEval, SavedEvals } from "@/types/eval"; import { Chess } from "chess.js"; import { atom } from "jotai"; @@ -16,3 +16,5 @@ export const engineNameAtom = atom(EngineName.Stockfish16); export const engineDepthAtom = atom(16); export const engineMultiPvAtom = atom(3); export const evaluationProgressAtom = atom(0); + +export const savedEvalsAtom = atom({}); diff --git a/src/types/eval.ts b/src/types/eval.ts index 54f66c4..7c51fab 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -38,7 +38,7 @@ export interface EvaluatePositionWithUpdateParams { fen: string; depth?: number; multiPv?: number; - setPartialEval: (positionEval: PositionEval) => void; + setPartialEval?: (positionEval: PositionEval) => void; } export interface CurrentPosition { @@ -55,3 +55,11 @@ export interface EvaluateGameParams { multiPv?: number; setEvaluationProgress?: (value: number) => void; } + +export interface SavedEval { + bestMove?: string; + lines: LineEval[]; + engine: EngineName; +} + +export type SavedEvals = Record;