feat : show eval

This commit is contained in:
GuillaumeSD
2024-02-17 19:58:00 +01:00
parent 770ae13517
commit 98d99c0df0
10 changed files with 224 additions and 50 deletions

64
src/lib/chess.ts Normal file
View File

@@ -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;
};

View File

@@ -1,4 +1,4 @@
import { GameEval, MoveEval } from "@/types/eval"; import { GameEval, LineEval, MoveEval } from "@/types/eval";
export class Stockfish { export class Stockfish {
private worker: Worker; private worker: Worker;
@@ -26,16 +26,18 @@ export class Stockfish {
public async init(): Promise<void> { public async init(): Promise<void> {
await this.sendCommands(["uci"], "uciok"); await this.sendCommands(["uci"], "uciok");
await this.sendCommands( await this.sendCommands(
["setoption name MultiPV value 2", "isready"], ["setoption name MultiPV value 3", "isready"],
"readyok" "readyok"
); );
this.ready = true; this.ready = true;
console.log("Stockfish initialized");
} }
public shutdown(): void { public shutdown(): void {
this.ready = false; this.ready = false;
this.worker.postMessage("quit"); this.worker.postMessage("quit");
this.worker.terminate(); this.worker.terminate();
console.log("Stockfish shutdown");
} }
public isReady(): boolean { public isReady(): boolean {
@@ -68,16 +70,59 @@ export class Stockfish {
await this.sendCommands(["ucinewgame", "isready"], "readyok"); await this.sendCommands(["ucinewgame", "isready"], "readyok");
this.worker.postMessage("position startpos"); this.worker.postMessage("position startpos");
let whiteCpVsBestMove = 0;
let blackCpVsBestMove = 0;
const moves: MoveEval[] = []; const moves: MoveEval[] = [];
for (const fen of fens) { for (const fen of fens) {
console.log(`Evaluating position: ${fen}`); console.log(`Evaluating position: ${fen}`);
const result = await this.evaluatePosition(fen, depth); 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); moves.push(result);
} }
this.ready = true; this.ready = true;
console.log("Game evaluated"); 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<MoveEval> { public async evaluatePosition(fen: string, depth = 16): Promise<MoveEval> {
@@ -93,6 +138,7 @@ export class Stockfish {
bestMove: "", bestMove: "",
lines: [], lines: [],
}; };
const tempResults: Record<string, LineEval> = {};
for (const result of results) { for (const result of results) {
if (result.startsWith("bestmove")) { if (result.startsWith("bestmove")) {
@@ -104,22 +150,40 @@ export class Stockfish {
if (result.startsWith("info")) { if (result.startsWith("info")) {
const pv = this.getResultPv(result); 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"); const mate = this.getResultProperty(result, "mate");
if (pv) { tempResults[multiPv] = {
parsedResults.lines.push({
pv, pv,
score: score ? parseInt(score) : undefined, cp: cp ? parseInt(cp) : undefined,
mate: mate ? parseInt(mate) : undefined, mate: mate ? parseInt(mate) : undefined,
}); };
}
} }
} }
parsedResults.lines = Object.values(tempResults).sort(this.sortLines);
return parsedResults; 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( private getResultProperty(
result: string, result: string,
property: string property: string

View File

@@ -1,21 +1,26 @@
import { drawBoard } from "@/lib/board"; import { drawBoard } from "@/lib/board";
import { drawEvaluationBar } from "@/lib/evalBar"; import { drawEvaluationBar } from "@/lib/evalBar";
import { useAtomValue } from "jotai";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { boardPgnAtom } from "./index.state";
import { getLastFen } from "@/lib/chess";
export default function Board() { export default function Board() {
const boardRef = useRef<HTMLCanvasElement>(null); const boardRef = useRef<HTMLCanvasElement>(null);
const evalBarRef = useRef<HTMLCanvasElement>(null); const evalBarRef = useRef<HTMLCanvasElement>(null);
const boardPgn = useAtomValue(boardPgnAtom);
const boardFen = getLastFen(boardPgn);
useEffect(() => { useEffect(() => {
const ctx = boardRef.current?.getContext("2d"); const ctx = boardRef.current?.getContext("2d");
if (!ctx) return; if (!ctx) return;
drawBoard(ctx); drawBoard(ctx, boardFen);
const evalCtx = evalBarRef.current?.getContext("2d"); const evalCtx = evalBarRef.current?.getContext("2d");
if (!evalCtx) return; if (!evalCtx) return;
drawEvaluationBar(evalCtx); drawEvaluationBar(evalCtx);
}, []); }, [boardFen]);
return ( return (
<div id="board-outer-container" className="center"> <div id="board-outer-container" className="center">

View File

@@ -1,4 +1,7 @@
import { initPgn } from "@/lib/chess";
import { GameEval } from "@/types/eval"; import { GameEval } from "@/types/eval";
import { atom } from "jotai"; import { atom } from "jotai";
export const gameReviewAtom = atom<GameEval | undefined>(undefined); export const gameEvalAtom = atom<GameEval | undefined>(undefined);
export const gamePgnAtom = atom(initPgn);
export const boardPgnAtom = atom(initPgn);

View File

@@ -3,29 +3,32 @@ import ReviewResult from "./reviewResult";
import SelectDepth from "./selectDepth"; import SelectDepth from "./selectDepth";
import SelectGameOrigin from "./selectGame/selectGameOrigin"; import SelectGameOrigin from "./selectGame/selectGameOrigin";
import { Stockfish } from "@/lib/engine/stockfish"; import { Stockfish } from "@/lib/engine/stockfish";
import { useAtomValue } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { gameFensAtom } from "./selectGame/gameOrigin.state"; import { boardPgnAtom, gameEvalAtom, gamePgnAtom } from "./index.state";
import { getGameFens, initPgn } from "@/lib/chess";
export default function ReviewPanelBody() { export default function ReviewPanelBody() {
const [engine, setEngine] = useState<Stockfish | null>(null); const [engine, setEngine] = useState<Stockfish | null>(null);
const gameFens = useAtomValue(gameFensAtom); const setGameEval = useSetAtom(gameEvalAtom);
const setBoardPgn = useSetAtom(boardPgnAtom);
const gamePgn = useAtomValue(gamePgnAtom);
useEffect(() => { useEffect(() => {
const engine = new Stockfish(); const engine = new Stockfish();
engine.init().then(() => { engine.init();
console.log("Engine initialized");
});
setEngine(engine); setEngine(engine);
return () => { return () => {
engine.shutdown(); engine.shutdown();
console.log("Engine shutdown");
}; };
}, []); }, []);
const handleAnalyse = () => { const handleAnalyse = async () => {
if (engine?.isReady() && gameFens) { setBoardPgn(initPgn);
engine.evaluateGame(gameFens); 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() {
<b id="secondary-message" className="white" /> <b id="secondary-message" className="white" />
{false && <ReviewResult />} <ReviewResult />
</div> </div>
); );
} }

View File

@@ -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() { export default function ReviewPanelToolBar() {
const [boardPgn, setBoardPgn] = useAtom(boardPgnAtom);
const gamePgn = useAtomValue(gamePgnAtom);
return ( return (
<div id="review-panel-toolbar"> <div id="review-panel-toolbar">
<div id="review-panel-toolbar-buttons" className="center"> <div id="review-panel-toolbar-buttons" className="center">
@@ -13,9 +20,27 @@ export default function ReviewPanelToolBar() {
src="back_to_start.png" src="back_to_start.png"
alt="Back to start" alt="Back to start"
title="Back to start" title="Back to start"
onClick={() => setBoardPgn(initPgn)}
/>
<img
id="back-move-button"
src="back.png"
alt="Back"
title="Back"
onClick={() => {
setBoardPgn(undoLastMove(boardPgn));
}}
/>
<img
id="next-move-button"
src="next.png"
alt="Next"
title="Next"
onClick={() => {
const nextBoardPgn = addNextMove(boardPgn, gamePgn);
setBoardPgn(nextBoardPgn);
}}
/> />
<img id="back-move-button" src="back.png" alt="Back" title="Back" />
<img id="next-move-button" src="next.png" alt="Next" title="Next" />
<img <img
id="go-end-move-button" id="go-end-move-button"
src="go_to_end.png" src="go_to_end.png"

View File

@@ -1,12 +1,23 @@
import { useAtomValue } from "jotai";
import { boardPgnAtom, gameEvalAtom } from "./index.state";
import { getNextMoveIndex } from "@/lib/chess";
export default function ReviewResult() { export default function ReviewResult() {
const boardPgn = useAtomValue(boardPgnAtom);
const gameEval = useAtomValue(gameEvalAtom);
if (!gameEval) return null;
const evalIndex = getNextMoveIndex(boardPgn);
const moveEval = gameEval.moves[evalIndex];
return ( return (
<div id="report-cards"> <div id="report-cards">
<h2 id="accuracies-title" className="white"> <h2 id="accuracies-title" className="white">
Accuracies Accuracies
</h2> </h2>
<div id="accuracies"> <div id="accuracies">
<b id="white-accuracy">0%</b> <b id="white-accuracy">{gameEval.whiteAccuracy}%</b>
<b id="black-accuracy">0%</b> <b id="black-accuracy">{gameEval.blackAccuracy}%</b>
</div> </div>
<div id="classification-message-container"> <div id="classification-message-container">
@@ -14,12 +25,22 @@ export default function ReviewResult() {
<b id="classification-message" /> <b id="classification-message" />
</div> </div>
<b id="top-alternative-message">Qxf2+ was best</b> <b id="top-alternative-message">
{moveEval ? `${moveEval.bestMove} is best` : "Game is over"}
</b>
<div id="engine-suggestions"> <div id="engine-suggestions">
<h2 id="engine-suggestions-title" className="white"> <h2 id="engine-suggestions-title" className="white">
Engine Engine
</h2> </h2>
{moveEval?.lines.map((line) => (
<div key={line.pv[0]} style={{ color: "white" }}>
<span style={{ marginRight: "2em" }}>
{line.cp ? line.cp / 100 : `Mate in ${line.mate}`}
</span>
<span>{line.pv.slice(0, 7).join(", ")}</span>
</div>
))}
</div> </div>
<span id="opening-name" className="white" /> <span id="opening-name" className="white" />

View File

@@ -1,3 +0,0 @@
import { atom } from "jotai";
export const gameFensAtom = atom<string[]>([]);

View File

@@ -1,8 +1,6 @@
import { GameOrigin } from "@/types/enums"; import { GameOrigin } from "@/types/enums";
import { useSetAtom } from "jotai"; import { useAtom } from "jotai";
import { gameFensAtom } from "./gameOrigin.state"; import { gamePgnAtom } from "../index.state";
import { useEffect, useState } from "react";
import { Chess } from "chess.js";
interface Props { interface Props {
gameOrigin: GameOrigin; gameOrigin: GameOrigin;
@@ -10,17 +8,7 @@ interface Props {
} }
export default function InputGame({ placeholder }: Props) { export default function InputGame({ placeholder }: Props) {
const [gamePgn, setGamePgn] = useState(""); const [gamePgn, setGamePgn] = useAtom(gamePgnAtom);
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]);
return ( return (
<textarea <textarea
@@ -31,7 +19,9 @@ export default function InputGame({ placeholder }: Props) {
spellCheck="false" spellCheck="false"
placeholder={placeholder} placeholder={placeholder}
value={gamePgn} value={gamePgn}
onChange={(e) => setGamePgn(e.target.value)} onChange={(e) => {
setGamePgn(e.target.value);
}}
/> />
); );
} }

View File

@@ -5,10 +5,12 @@ export interface MoveEval {
export interface LineEval { export interface LineEval {
pv: string[]; pv: string[];
score?: number; cp?: number;
mate?: number; mate?: number;
} }
export interface GameEval { export interface GameEval {
moves: MoveEval[]; moves: MoveEval[];
whiteAccuracy: number;
blackAccuracy: number;
} }