feat : show eval
This commit is contained in:
64
src/lib/chess.ts
Normal file
64
src/lib/chess.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
export const gameFensAtom = atom<string[]>([]);
|
|
||||||
@@ -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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user