Files
chesskit/src/lib/engine/helpers/accuracy.ts
2025-05-09 23:52:20 +02:00

93 lines
2.7 KiB
TypeScript

import {
ceilsNumber,
getHarmonicMean,
getStandardDeviation,
getWeightedMean,
} from "@/lib/math";
import { Accuracy, PositionEval } from "@/types/eval";
import { getPositionWinPercentage } from "./winPercentage";
export const computeAccuracy = (positions: PositionEval[]): Accuracy => {
const positionsWinPercentage = positions.map(getPositionWinPercentage);
const weights = getAccuracyWeights(positionsWinPercentage);
const movesAccuracy = getMovesAccuracy(positionsWinPercentage);
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 isWhiteMove = index % 2 === 0;
const winDiff = isWhiteMove
? Math.max(0, lastWinPercent - winPercent)
: Math.max(0, winPercent - lastWinPercent);
// Source: https://github.com/lichess-org/lila/blob/a320a93b68dabee862b8093b1b2acdfe132b9966/modules/analyse/src/main/AccuracyPercent.scala#L44
const rawAccuracy =
103.1668100711649 * Math.exp(-0.04354415386753951 * winDiff) -
3.166924740191411;
return Math.min(100, Math.max(0, rawAccuracy + 1));
});