feat : add accuracy calculation
This commit is contained in:
@@ -3,7 +3,11 @@ import { Game } from "@/types/game";
|
||||
import { Chess } from "chess.js";
|
||||
|
||||
export const getFens = (game: Chess): string[] => {
|
||||
return game.history({ verbose: true }).map((move) => move.before);
|
||||
const history = game.history({ verbose: true });
|
||||
const fens = history.map((move) => move.before);
|
||||
fens.push(history[history.length - 1].after);
|
||||
|
||||
return fens;
|
||||
};
|
||||
|
||||
export const getGameFromPgn = (pgn: string): Chess => {
|
||||
@@ -106,3 +110,9 @@ export const getEvaluationBarValue = (
|
||||
|
||||
return { whiteBarPercentage, label: pEval.toFixed(1) };
|
||||
};
|
||||
|
||||
export const getWhoIsCheckmated = (fen: string): "w" | "b" | null => {
|
||||
const game = new Chess(fen);
|
||||
if (!game.isCheckmate()) return null;
|
||||
return game.turn();
|
||||
};
|
||||
|
||||
111
src/lib/engine/helpers/accuracy.ts
Normal file
111
src/lib/engine/helpers/accuracy.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ceilsNumber,
|
||||
getHarmonicMean,
|
||||
getStandardDeviation,
|
||||
getWeightedMean,
|
||||
} from "@/lib/helpers";
|
||||
import { Accuracy, MoveEval } from "@/types/eval";
|
||||
|
||||
export const computeAccuracy = (moves: MoveEval[]): Accuracy => {
|
||||
const movesWinPercentage = moves.map(getPositionWinPercentage);
|
||||
|
||||
const weights = getAccuracyWeights(movesWinPercentage);
|
||||
|
||||
const movesAccuracy = getMovesAccuracy(movesWinPercentage);
|
||||
|
||||
const whiteAccuracy = getPlayerAccuracy(movesAccuracy, weights, "white");
|
||||
const blackAccuracy = getPlayerAccuracy(movesAccuracy, weights, "black");
|
||||
|
||||
return {
|
||||
white: whiteAccuracy,
|
||||
black: blackAccuracy,
|
||||
};
|
||||
};
|
||||
|
||||
const getPlayerAccuracy = (
|
||||
movesAccuracy: number[],
|
||||
weights: number[],
|
||||
player: "white" | "black"
|
||||
): number => {
|
||||
const remainder = player === "white" ? 0 : 1;
|
||||
const playerAccuracies = movesAccuracy.filter(
|
||||
(_, index) => index % 2 === remainder
|
||||
);
|
||||
const playerWeights = weights.filter((_, index) => index % 2 === remainder);
|
||||
|
||||
const weightedMean = getWeightedMean(playerAccuracies, playerWeights);
|
||||
const harmonicMean = getHarmonicMean(playerAccuracies);
|
||||
|
||||
return (weightedMean + harmonicMean) / 2;
|
||||
};
|
||||
|
||||
const getAccuracyWeights = (movesWinPercentage: number[]): number[] => {
|
||||
const windowSize = ceilsNumber(
|
||||
Math.ceil(movesWinPercentage.length / 10),
|
||||
2,
|
||||
8
|
||||
);
|
||||
|
||||
const windows: number[][] = [];
|
||||
const halfWindowSize = Math.round(windowSize / 2);
|
||||
|
||||
for (let i = 1; i < movesWinPercentage.length; i++) {
|
||||
const startIdx = i - halfWindowSize;
|
||||
const endIdx = i + halfWindowSize;
|
||||
|
||||
if (startIdx < 0) {
|
||||
windows.push(movesWinPercentage.slice(0, windowSize));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endIdx > movesWinPercentage.length) {
|
||||
windows.push(movesWinPercentage.slice(-windowSize));
|
||||
continue;
|
||||
}
|
||||
|
||||
windows.push(movesWinPercentage.slice(startIdx, endIdx));
|
||||
}
|
||||
|
||||
const weights = windows.map((window) => {
|
||||
const std = getStandardDeviation(window);
|
||||
return ceilsNumber(std, 0.5, 12);
|
||||
});
|
||||
|
||||
return weights;
|
||||
};
|
||||
|
||||
const getMovesAccuracy = (movesWinPercentage: number[]): number[] =>
|
||||
movesWinPercentage.slice(1).map((winPercent, index) => {
|
||||
const lastWinPercent = movesWinPercentage[index];
|
||||
const winDiff = Math.abs(lastWinPercent - winPercent);
|
||||
|
||||
const rawAccuracy =
|
||||
103.1668100711649 * Math.exp(-0.04354415386753951 * winDiff) -
|
||||
3.166924740191411;
|
||||
|
||||
return Math.min(100, Math.max(0, rawAccuracy + 1));
|
||||
});
|
||||
|
||||
const getPositionWinPercentage = (move: MoveEval): number => {
|
||||
if (move.lines[0].cp !== undefined) {
|
||||
return getWinPercentageFromCp(move.lines[0].cp);
|
||||
}
|
||||
|
||||
if (move.lines[0].mate !== undefined) {
|
||||
return getWinPercentageFromMate(move.lines[0].mate);
|
||||
}
|
||||
|
||||
throw new Error("No cp or mate in move");
|
||||
};
|
||||
|
||||
const getWinPercentageFromMate = (mate: number): number => {
|
||||
const mateInf = mate * Infinity;
|
||||
return getWinPercentageFromCp(mateInf);
|
||||
};
|
||||
|
||||
const getWinPercentageFromCp = (cp: number): number => {
|
||||
const cpCeiled = ceilsNumber(cp, -1000, 1000);
|
||||
const MULTIPLIER = -0.00368208;
|
||||
const winChances = 2 / (1 + Math.exp(MULTIPLIER * cpCeiled)) - 1;
|
||||
return 50 + 50 * winChances;
|
||||
};
|
||||
99
src/lib/engine/helpers/parseResults.ts
Normal file
99
src/lib/engine/helpers/parseResults.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { LineEval, MoveEval } from "@/types/eval";
|
||||
|
||||
export const parseEvaluationResults = (
|
||||
results: string[],
|
||||
whiteToPlay: boolean
|
||||
): MoveEval => {
|
||||
const parsedResults: MoveEval = {
|
||||
bestMove: "",
|
||||
lines: [],
|
||||
};
|
||||
const tempResults: Record<string, LineEval> = {};
|
||||
|
||||
for (const result of results) {
|
||||
if (result.startsWith("bestmove")) {
|
||||
const bestMove = getResultProperty(result, "bestmove");
|
||||
if (bestMove) {
|
||||
parsedResults.bestMove = bestMove;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.startsWith("info")) {
|
||||
const pv = getResultPv(result);
|
||||
const multiPv = getResultProperty(result, "multipv");
|
||||
const depth = getResultProperty(result, "depth");
|
||||
if (!pv || !multiPv || !depth) continue;
|
||||
|
||||
if (
|
||||
tempResults[multiPv] &&
|
||||
parseInt(depth) < tempResults[multiPv].depth
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cp = getResultProperty(result, "cp");
|
||||
const mate = getResultProperty(result, "mate");
|
||||
|
||||
tempResults[multiPv] = {
|
||||
pv,
|
||||
cp: cp ? parseInt(cp) : undefined,
|
||||
mate: mate ? parseInt(mate) : undefined,
|
||||
depth: parseInt(depth),
|
||||
multiPv: parseInt(multiPv),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
parsedResults.lines = Object.values(tempResults).sort(sortLines);
|
||||
|
||||
if (!whiteToPlay) {
|
||||
parsedResults.lines = parsedResults.lines.map((line) => ({
|
||||
...line,
|
||||
cp: line.cp ? -line.cp : line.cp,
|
||||
mate: line.mate ? -line.mate : line.mate,
|
||||
}));
|
||||
}
|
||||
|
||||
return parsedResults;
|
||||
};
|
||||
|
||||
const 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);
|
||||
};
|
||||
|
||||
const getResultProperty = (
|
||||
result: string,
|
||||
property: string
|
||||
): string | undefined => {
|
||||
const splitResult = result.split(" ");
|
||||
const propertyIndex = splitResult.indexOf(property);
|
||||
|
||||
if (propertyIndex === -1 || propertyIndex + 1 >= splitResult.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return splitResult[propertyIndex + 1];
|
||||
};
|
||||
|
||||
const getResultPv = (result: string): string[] | undefined => {
|
||||
const splitResult = result.split(" ");
|
||||
const pvIndex = splitResult.indexOf("pv");
|
||||
|
||||
if (pvIndex === -1 || pvIndex + 1 >= splitResult.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return splitResult.slice(pvIndex + 1);
|
||||
};
|
||||
@@ -2,9 +2,11 @@ import { EngineName } from "@/types/enums";
|
||||
import {
|
||||
EvaluatePositionWithUpdateParams,
|
||||
GameEval,
|
||||
LineEval,
|
||||
MoveEval,
|
||||
} from "@/types/eval";
|
||||
import { parseEvaluationResults } from "./helpers/parseResults";
|
||||
import { computeAccuracy } from "./helpers/accuracy";
|
||||
import { getWhoIsCheckmated } from "../chess";
|
||||
|
||||
export abstract class UciEngine {
|
||||
private worker: Worker;
|
||||
@@ -104,14 +106,31 @@ export abstract class UciEngine {
|
||||
|
||||
const moves: MoveEval[] = [];
|
||||
for (const fen of fens) {
|
||||
const whoIsCheckmated = getWhoIsCheckmated(fen);
|
||||
if (whoIsCheckmated) {
|
||||
moves.push({
|
||||
bestMove: "",
|
||||
lines: [
|
||||
{
|
||||
pv: [],
|
||||
depth: 0,
|
||||
multiPv: 1,
|
||||
mate: whoIsCheckmated === "w" ? -1 : 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const result = await this.evaluatePosition(fen, depth);
|
||||
moves.push(result);
|
||||
}
|
||||
|
||||
const accuracy = computeAccuracy(moves);
|
||||
|
||||
this.ready = true;
|
||||
return {
|
||||
moves,
|
||||
accuracy: { white: 82.34, black: 67.49 }, // TODO: Calculate accuracy
|
||||
moves: moves.slice(0, -1),
|
||||
accuracy,
|
||||
settings: {
|
||||
engine: this.engineName,
|
||||
date: new Date().toISOString(),
|
||||
@@ -131,7 +150,7 @@ export abstract class UciEngine {
|
||||
|
||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
||||
|
||||
return this.parseResults(results, whiteToPlay);
|
||||
return parseEvaluationResults(results, whiteToPlay);
|
||||
}
|
||||
|
||||
public async evaluatePositionWithUpdate({
|
||||
@@ -148,7 +167,7 @@ export abstract class UciEngine {
|
||||
const whiteToPlay = fen.split(" ")[1] === "w";
|
||||
|
||||
const onNewMessage = (messages: string[]) => {
|
||||
const parsedResults = this.parseResults(messages, whiteToPlay);
|
||||
const parsedResults = parseEvaluationResults(messages, whiteToPlay);
|
||||
setPartialEval(parsedResults);
|
||||
};
|
||||
|
||||
@@ -159,99 +178,4 @@ export abstract class UciEngine {
|
||||
onNewMessage
|
||||
);
|
||||
}
|
||||
|
||||
private parseResults(results: string[], whiteToPlay: boolean): MoveEval {
|
||||
const parsedResults: MoveEval = {
|
||||
bestMove: "",
|
||||
lines: [],
|
||||
};
|
||||
const tempResults: Record<string, LineEval> = {};
|
||||
|
||||
for (const result of results) {
|
||||
if (result.startsWith("bestmove")) {
|
||||
const bestMove = this.getResultProperty(result, "bestmove");
|
||||
if (bestMove) {
|
||||
parsedResults.bestMove = bestMove;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.startsWith("info")) {
|
||||
const pv = this.getResultPv(result);
|
||||
const multiPv = this.getResultProperty(result, "multipv");
|
||||
const depth = this.getResultProperty(result, "depth");
|
||||
if (!pv || !multiPv || !depth) continue;
|
||||
|
||||
if (
|
||||
tempResults[multiPv] &&
|
||||
parseInt(depth) < tempResults[multiPv].depth
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cp = this.getResultProperty(result, "cp");
|
||||
const mate = this.getResultProperty(result, "mate");
|
||||
|
||||
tempResults[multiPv] = {
|
||||
pv,
|
||||
cp: cp ? parseInt(cp) : undefined,
|
||||
mate: mate ? parseInt(mate) : undefined,
|
||||
depth: parseInt(depth),
|
||||
multiPv: parseInt(multiPv),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
parsedResults.lines = Object.values(tempResults).sort(this.sortLines);
|
||||
|
||||
if (!whiteToPlay) {
|
||||
parsedResults.lines = parsedResults.lines.map((line) => ({
|
||||
...line,
|
||||
cp: line.cp ? -line.cp : line.cp,
|
||||
mate: line.mate ? -line.mate : line.mate,
|
||||
}));
|
||||
}
|
||||
|
||||
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
|
||||
): string | undefined {
|
||||
const splitResult = result.split(" ");
|
||||
const propertyIndex = splitResult.indexOf(property);
|
||||
|
||||
if (propertyIndex === -1 || propertyIndex + 1 >= splitResult.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return splitResult[propertyIndex + 1];
|
||||
}
|
||||
|
||||
private getResultPv(result: string): string[] | undefined {
|
||||
const splitResult = result.split(" ");
|
||||
const pvIndex = splitResult.indexOf("pv");
|
||||
|
||||
if (pvIndex === -1 || pvIndex + 1 >= splitResult.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return splitResult.slice(pvIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,37 @@ export const getPaddedMonth = (month: number) => {
|
||||
export const capitalize = (s: string) => {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
export const ceilsNumber = (number: number, min: number, max: number) => {
|
||||
if (number > max) return max;
|
||||
if (number < min) return min;
|
||||
return number;
|
||||
};
|
||||
|
||||
export const getHarmonicMean = (array: number[]) => {
|
||||
const sum = array.reduce((acc, curr) => acc + 1 / curr, 0);
|
||||
return array.length / sum;
|
||||
};
|
||||
|
||||
export const getStandardDeviation = (array: number[]) => {
|
||||
const n = array.length;
|
||||
const mean = array.reduce((a, b) => a + b) / n;
|
||||
return Math.sqrt(
|
||||
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
|
||||
);
|
||||
};
|
||||
|
||||
export const getWeightedMean = (array: number[], weights: number[]) => {
|
||||
if (array.length > weights.length)
|
||||
throw new Error("Weights array is too short");
|
||||
|
||||
const weightedSum = array.reduce(
|
||||
(acc, curr, index) => acc + curr * weights[index],
|
||||
0
|
||||
);
|
||||
const weightSum = weights
|
||||
.slice(0, array.length)
|
||||
.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
return weightedSum / weightSum;
|
||||
};
|
||||
|
||||
@@ -86,9 +86,10 @@ export default function ReviewPanelBody() {
|
||||
|
||||
<Grid item container xs={12} justifyContent="center" alignItems="center">
|
||||
<List sx={{ maxWidth: "95%" }}>
|
||||
{engineLines.map((line) => (
|
||||
<LineEvaluation key={line.multiPv} line={line} />
|
||||
))}
|
||||
{!board.isCheckmate() &&
|
||||
engineLines.map((line) => (
|
||||
<LineEvaluation key={line.multiPv} line={line} />
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user