From 98d99c0df06ef686461d841c88f9d3723e1d4a35 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sat, 17 Feb 2024 19:58:00 +0100 Subject: [PATCH] feat : show eval --- src/lib/chess.ts | 64 ++++++++++++++ src/lib/engine/stockfish.ts | 86 ++++++++++++++++--- src/sections/index/board.tsx | 9 +- src/sections/index/index.state.ts | 5 +- src/sections/index/reviewPanelBody.tsx | 25 +++--- src/sections/index/reviewPanelToolbar.tsx | 29 ++++++- src/sections/index/reviewResult.tsx | 27 +++++- .../index/selectGame/gameOrigin.state.ts | 3 - src/sections/index/selectGame/inputGame.tsx | 22 ++--- src/types/eval.ts | 4 +- 10 files changed, 224 insertions(+), 50 deletions(-) create mode 100644 src/lib/chess.ts delete mode 100644 src/sections/index/selectGame/gameOrigin.state.ts diff --git a/src/lib/chess.ts b/src/lib/chess.ts new file mode 100644 index 0000000..953f184 --- /dev/null +++ b/src/lib/chess.ts @@ -0,0 +1,64 @@ +import { Chess } from "chess.js"; + +export const initPgn = new Chess().pgn(); + +export const getGameFens = (pgn: string): string[] => { + const game = new Chess(); + game.loadPgn(pgn); + return game.history({ verbose: true }).map((move) => move.before); +}; + +export const getLastFen = (pgn: string): string => { + const game = new Chess(); + game.loadPgn(pgn); + return game.fen(); +}; + +export const undoLastMove = (pgn: string): string => { + if (pgn === initPgn) return pgn; + const game = new Chess(); + game.loadPgn(pgn); + game.undo(); + return game.pgn(); +}; + +export const addMove = ( + pgn: string, + move: { + from: string; + to: string; + promotion?: string; + } +): string => { + const game = new Chess(); + game.loadPgn(pgn); + game.move(move); + return game.pgn(); +}; + +export const addNextMove = (boardPgn: string, gamePgn: string): string => { + const board = new Chess(); + board.loadPgn(boardPgn); + + const game = new Chess(); + game.loadPgn(gamePgn); + + const nextMoveIndex = board.history().length; + const nextMove = game.history({ verbose: true })[nextMoveIndex]; + + if (nextMove) { + board.move({ + from: nextMove.from, + to: nextMove.to, + promotion: nextMove.promotion, + }); + } + + return board.pgn(); +}; + +export const getNextMoveIndex = (pgn: string): number => { + const game = new Chess(); + game.loadPgn(pgn); + return game.history().length; +}; diff --git a/src/lib/engine/stockfish.ts b/src/lib/engine/stockfish.ts index 5b2f3f4..07ec622 100644 --- a/src/lib/engine/stockfish.ts +++ b/src/lib/engine/stockfish.ts @@ -1,4 +1,4 @@ -import { GameEval, MoveEval } from "@/types/eval"; +import { GameEval, LineEval, MoveEval } from "@/types/eval"; export class Stockfish { private worker: Worker; @@ -26,16 +26,18 @@ export class Stockfish { public async init(): Promise { await this.sendCommands(["uci"], "uciok"); await this.sendCommands( - ["setoption name MultiPV value 2", "isready"], + ["setoption name MultiPV value 3", "isready"], "readyok" ); this.ready = true; + console.log("Stockfish initialized"); } public shutdown(): void { this.ready = false; this.worker.postMessage("quit"); this.worker.terminate(); + console.log("Stockfish shutdown"); } public isReady(): boolean { @@ -68,16 +70,59 @@ export class Stockfish { await this.sendCommands(["ucinewgame", "isready"], "readyok"); this.worker.postMessage("position startpos"); + let whiteCpVsBestMove = 0; + let blackCpVsBestMove = 0; const moves: MoveEval[] = []; for (const fen of fens) { console.log(`Evaluating position: ${fen}`); const result = await this.evaluatePosition(fen, depth); + + const bestLine = result.lines[0]; + const beforeMoveBestLine: LineEval = moves.at(-1)?.lines[0] ?? { + pv: [], + cp: 0, + }; + const wasWhiteMove = fen.split(" ")[1] === "b"; + const cpVsBestMove = this.calculateCpVsBestMove( + bestLine, + beforeMoveBestLine + ); + if (wasWhiteMove) { + whiteCpVsBestMove += cpVsBestMove; + } else { + blackCpVsBestMove += cpVsBestMove; + } + moves.push(result); } this.ready = true; console.log("Game evaluated"); - return { moves }; + console.log(moves); + const whiteAccuracy = this.calculateAccuracy( + whiteCpVsBestMove, + moves.length + ); + const blackAccuracy = this.calculateAccuracy( + blackCpVsBestMove, + moves.length + ); + return { moves, whiteAccuracy, blackAccuracy }; + } + + private calculateAccuracy(cpVsBestMove: number, movesNb: number): number { + return 100 - (cpVsBestMove / movesNb) * 100; + } + + private calculateCpVsBestMove( + bestLine: LineEval, + beforeMoveBestLine: LineEval + ): number { + if (bestLine.cp === undefined || beforeMoveBestLine.cp === undefined) { + return 0; + } + + return bestLine.cp - beforeMoveBestLine.cp; } public async evaluatePosition(fen: string, depth = 16): Promise { @@ -93,6 +138,7 @@ export class Stockfish { bestMove: "", lines: [], }; + const tempResults: Record = {}; for (const result of results) { if (result.startsWith("bestmove")) { @@ -104,22 +150,40 @@ export class Stockfish { if (result.startsWith("info")) { const pv = this.getResultPv(result); - const score = this.getResultProperty(result, "cp"); + const multiPv = this.getResultProperty(result, "multipv"); + if (!pv || !multiPv) continue; + const cp = this.getResultProperty(result, "cp"); const mate = this.getResultProperty(result, "mate"); - if (pv) { - parsedResults.lines.push({ - pv, - score: score ? parseInt(score) : undefined, - mate: mate ? parseInt(mate) : undefined, - }); - } + tempResults[multiPv] = { + pv, + cp: cp ? parseInt(cp) : undefined, + mate: mate ? parseInt(mate) : undefined, + }; } } + parsedResults.lines = Object.values(tempResults).sort(this.sortLines); + return parsedResults; } + private sortLines(a: LineEval, b: LineEval): number { + if (a.mate !== undefined && b.mate !== undefined) { + return a.mate - b.mate; + } + + if (a.mate !== undefined) { + return -a.mate; + } + + if (b.mate !== undefined) { + return b.mate; + } + + return (b.cp ?? 0) - (a.cp ?? 0); + } + private getResultProperty( result: string, property: string diff --git a/src/sections/index/board.tsx b/src/sections/index/board.tsx index ad6011e..c06f014 100644 --- a/src/sections/index/board.tsx +++ b/src/sections/index/board.tsx @@ -1,21 +1,26 @@ import { drawBoard } from "@/lib/board"; import { drawEvaluationBar } from "@/lib/evalBar"; +import { useAtomValue } from "jotai"; import { useEffect, useRef } from "react"; +import { boardPgnAtom } from "./index.state"; +import { getLastFen } from "@/lib/chess"; export default function Board() { const boardRef = useRef(null); const evalBarRef = useRef(null); + const boardPgn = useAtomValue(boardPgnAtom); + const boardFen = getLastFen(boardPgn); useEffect(() => { const ctx = boardRef.current?.getContext("2d"); if (!ctx) return; - drawBoard(ctx); + drawBoard(ctx, boardFen); const evalCtx = evalBarRef.current?.getContext("2d"); if (!evalCtx) return; drawEvaluationBar(evalCtx); - }, []); + }, [boardFen]); return (
diff --git a/src/sections/index/index.state.ts b/src/sections/index/index.state.ts index 010a40f..3c3311f 100644 --- a/src/sections/index/index.state.ts +++ b/src/sections/index/index.state.ts @@ -1,4 +1,7 @@ +import { initPgn } from "@/lib/chess"; import { GameEval } from "@/types/eval"; import { atom } from "jotai"; -export const gameReviewAtom = atom(undefined); +export const gameEvalAtom = atom(undefined); +export const gamePgnAtom = atom(initPgn); +export const boardPgnAtom = atom(initPgn); diff --git a/src/sections/index/reviewPanelBody.tsx b/src/sections/index/reviewPanelBody.tsx index a674028..fe34a49 100644 --- a/src/sections/index/reviewPanelBody.tsx +++ b/src/sections/index/reviewPanelBody.tsx @@ -3,29 +3,32 @@ import ReviewResult from "./reviewResult"; import SelectDepth from "./selectDepth"; import SelectGameOrigin from "./selectGame/selectGameOrigin"; import { Stockfish } from "@/lib/engine/stockfish"; -import { useAtomValue } from "jotai"; -import { gameFensAtom } from "./selectGame/gameOrigin.state"; +import { useAtomValue, useSetAtom } from "jotai"; +import { boardPgnAtom, gameEvalAtom, gamePgnAtom } from "./index.state"; +import { getGameFens, initPgn } from "@/lib/chess"; export default function ReviewPanelBody() { const [engine, setEngine] = useState(null); - const gameFens = useAtomValue(gameFensAtom); + const setGameEval = useSetAtom(gameEvalAtom); + const setBoardPgn = useSetAtom(boardPgnAtom); + const gamePgn = useAtomValue(gamePgnAtom); useEffect(() => { const engine = new Stockfish(); - engine.init().then(() => { - console.log("Engine initialized"); - }); + engine.init(); setEngine(engine); return () => { engine.shutdown(); - console.log("Engine shutdown"); }; }, []); - const handleAnalyse = () => { - if (engine?.isReady() && gameFens) { - engine.evaluateGame(gameFens); + const handleAnalyse = async () => { + setBoardPgn(initPgn); + const gameFens = getGameFens(gamePgn); + if (engine?.isReady() && gameFens.length) { + const newGameEval = await engine.evaluateGame(gameFens); + setGameEval(newGameEval); } }; @@ -50,7 +53,7 @@ export default function ReviewPanelBody() { - {false && } +
); } diff --git a/src/sections/index/reviewPanelToolbar.tsx b/src/sections/index/reviewPanelToolbar.tsx index 2b8454f..fd0b6f3 100644 --- a/src/sections/index/reviewPanelToolbar.tsx +++ b/src/sections/index/reviewPanelToolbar.tsx @@ -1,4 +1,11 @@ +import { useAtom, useAtomValue } from "jotai"; +import { boardPgnAtom, gamePgnAtom } from "./index.state"; +import { addNextMove, initPgn, undoLastMove } from "@/lib/chess"; + export default function ReviewPanelToolBar() { + const [boardPgn, setBoardPgn] = useAtom(boardPgnAtom); + const gamePgn = useAtomValue(gamePgnAtom); + return (
@@ -13,9 +20,27 @@ export default function ReviewPanelToolBar() { src="back_to_start.png" alt="Back to start" title="Back to start" + onClick={() => setBoardPgn(initPgn)} + /> + Back { + setBoardPgn(undoLastMove(boardPgn)); + }} + /> + Next { + const nextBoardPgn = addNextMove(boardPgn, gamePgn); + setBoardPgn(nextBoardPgn); + }} /> - Back - Next

Accuracies

- 0% - 0% + {gameEval.whiteAccuracy}% + {gameEval.blackAccuracy}%
@@ -14,12 +25,22 @@ export default function ReviewResult() {
- Qxf2+ was best + + {moveEval ? `${moveEval.bestMove} is best` : "Game is over"} +

Engine

+ {moveEval?.lines.map((line) => ( +
+ + {line.cp ? line.cp / 100 : `Mate in ${line.mate}`} + + {line.pv.slice(0, 7).join(", ")} +
+ ))}
diff --git a/src/sections/index/selectGame/gameOrigin.state.ts b/src/sections/index/selectGame/gameOrigin.state.ts deleted file mode 100644 index b18485b..0000000 --- a/src/sections/index/selectGame/gameOrigin.state.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from "jotai"; - -export const gameFensAtom = atom([]); diff --git a/src/sections/index/selectGame/inputGame.tsx b/src/sections/index/selectGame/inputGame.tsx index 6bf3a12..3fbc679 100644 --- a/src/sections/index/selectGame/inputGame.tsx +++ b/src/sections/index/selectGame/inputGame.tsx @@ -1,8 +1,6 @@ import { GameOrigin } from "@/types/enums"; -import { useSetAtom } from "jotai"; -import { gameFensAtom } from "./gameOrigin.state"; -import { useEffect, useState } from "react"; -import { Chess } from "chess.js"; +import { useAtom } from "jotai"; +import { gamePgnAtom } from "../index.state"; interface Props { gameOrigin: GameOrigin; @@ -10,17 +8,7 @@ interface Props { } export default function InputGame({ placeholder }: Props) { - const [gamePgn, setGamePgn] = useState(""); - const setGameFens = useSetAtom(gameFensAtom); - - useEffect(() => { - const chess = new Chess(); - chess.loadPgn(gamePgn); - const fens = chess.history({ verbose: true }).map((move) => { - return move.after; - }); - setGameFens(fens); - }, [gamePgn]); + const [gamePgn, setGamePgn] = useAtom(gamePgnAtom); return (