diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ad6df07b..a5b4042b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -19,12 +19,15 @@ import { Icon } from "@iconify/react"; import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; import GraphTab from "@/sections/analysis/panelBody/graphTab"; import { PageTitle } from "@/components/pageTitle"; +import { useAnalysisSync } from "@/sections/analysis/hooks/useAnalysisSync"; export default function GameAnalysis() { const theme = useTheme(); const [tab, setTab] = useState(0); const isLgOrGreater = useMediaQuery(theme.breakpoints.up("lg")); + useAnalysisSync(); + const gameEval = useAtomValue(gameEvalAtom); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); diff --git a/src/sections/analysis/hooks/useAnalysisSync.ts b/src/sections/analysis/hooks/useAnalysisSync.ts new file mode 100644 index 00000000..62e52880 --- /dev/null +++ b/src/sections/analysis/hooks/useAnalysisSync.ts @@ -0,0 +1,46 @@ +import { useAtom, useAtomValue } from "jotai"; +import { boardAtom, gameAtom } from "@/sections/analysis/states"; +import { useEffect } from "react"; +import { Chess } from "chess.js"; + +export const useAnalysisSync = () => { + const [game, setGame] = useAtom(gameAtom); + const board = useAtomValue(boardAtom); + + useEffect(() => { + const boardHistory = board.history({ verbose: true }); + const gameHistory = game.history({ verbose: true }); + + if (boardHistory.length === gameHistory.length + 1) { + const isExtension = gameHistory.every( + (m, i) => m.after === boardHistory[i].after + ); + + if (isExtension) { + const newGame = new Chess(); + + try { + newGame.loadPgn(board.pgn()); + + const originalHeaders = game.getHeaders(); + if (originalHeaders["SetUp"] === "1") { + newGame.setHeader("SetUp", "1"); + newGame.setHeader("FEN", originalHeaders["FEN"]); + } + } catch { + try { + newGame.load(board.fen()); + newGame.setHeader("SetUp", "1"); + const startFen = game.getHeaders()["FEN"] || board.fen(); + newGame.setHeader("FEN", startFen); + } catch (err) { + console.error("Critical sync error", err); + return; + } + } + + setGame(newGame); + } + } + }, [board, game, setGame]); +}; diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index 257be3f8..2dfbcb20 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -41,21 +41,30 @@ export const useCurrentPosition = (engine: UciEngine | null) => { if (gameEval) { const evalIndex = boardHistory.length; + const cachedEval = gameEval.positions?.[evalIndex]; - position.eval = { - ...gameEval.positions[evalIndex], - lines: gameEval.positions[evalIndex].lines.slice(0, multiPv), - }; - position.lastEval = - evalIndex > 0 - ? { - ...gameEval.positions[evalIndex - 1], - lines: gameEval.positions[evalIndex - 1].lines.slice( - 0, - multiPv - ), - } - : undefined; + if (cachedEval) { + position.eval = { + ...gameEval.positions[evalIndex], + lines: gameEval.positions[evalIndex].lines.slice(0, multiPv), + }; + } + + const previousCachedEval = + evalIndex > 0 ? gameEval.positions?.[evalIndex - 1] : undefined; + + if (previousCachedEval) { + position.lastEval = + evalIndex > 0 + ? { + ...gameEval.positions[evalIndex - 1], + lines: gameEval.positions[evalIndex - 1].lines.slice( + 0, + multiPv + ), + } + : undefined; + } } } @@ -117,47 +126,56 @@ export const useCurrentPosition = (engine: UciEngine | null) => { }; 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" ? -2 : -3); - - 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, - }, - ], + try { + const setPartialEval = (positionEval: PositionEval) => { + setCurrentPosition({ ...position, eval: positionEval }); }; - }); - - const positionsWithMoveClassification = getMovesClassification( - rawPositions, - uciMoves, - fens - ); - - setCurrentPosition({ - ...position, - eval: positionsWithMoveClassification.slice(-1)[0], - lastEval: positionsWithMoveClassification.slice(-2)[0], - }); + 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" ? -2 : -3 + ); + + 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], + }); + } catch (error) { + if (error instanceof Error && error.message === "Engine not ready") { + return; + } + console.error("Error calculating evaluation:", error); + } }; getPositionEval();