feat : add elo estimation feature (#6)
* feat: ✨ add elo estimation feature
This commit is contained in:
54
src/lib/engine/helpers/estimateElo.ts
Normal file
54
src/lib/engine/helpers/estimateElo.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ceilsNumber } from "@/lib/math";
|
||||
import { EstimatedElo, PositionEval } from "@/types/eval";
|
||||
|
||||
export const estimateEloFromEngineOutput = (
|
||||
positions: PositionEval[]
|
||||
): EstimatedElo => {
|
||||
try {
|
||||
if (!positions || positions.length === 0) {
|
||||
return { white: null, black: null };
|
||||
}
|
||||
|
||||
let totalCPLWhite = 0;
|
||||
let totalCPLBlack = 0;
|
||||
let moveCount = 0;
|
||||
let previousCp = null;
|
||||
let flag = true;
|
||||
for (const moveAnalysis of positions) {
|
||||
if (moveAnalysis.lines && moveAnalysis.lines.length > 0) {
|
||||
const bestLine = moveAnalysis.lines[0];
|
||||
if (bestLine.cp !== undefined) {
|
||||
if (previousCp !== null) {
|
||||
const diff = Math.abs(bestLine.cp - previousCp);
|
||||
if (flag) {
|
||||
totalCPLWhite += ceilsNumber(diff, -1000, 1000);
|
||||
} else {
|
||||
totalCPLBlack += ceilsNumber(diff, -1000, 1000);
|
||||
}
|
||||
flag = !flag;
|
||||
moveCount++;
|
||||
}
|
||||
previousCp = bestLine.cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moveCount === 0) {
|
||||
return { white: null, black: null };
|
||||
}
|
||||
|
||||
const averageCPLWhite = totalCPLWhite / Math.ceil(moveCount / 2);
|
||||
const averageCPLBlack = totalCPLBlack / Math.floor(moveCount / 2);
|
||||
|
||||
const estimateElo = (averageCPL: number) =>
|
||||
3100 * Math.exp(-0.01 * averageCPL);
|
||||
|
||||
const whiteElo = estimateElo(Math.abs(averageCPLWhite));
|
||||
const blackElo = estimateElo(Math.abs(averageCPLBlack));
|
||||
|
||||
return { white: whiteElo, black: blackElo };
|
||||
} catch (error) {
|
||||
console.error("Error estimating Elo: ", error);
|
||||
return { white: null, black: null };
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EngineName } from "@/types/enums";
|
||||
import {
|
||||
EstimatedElo,
|
||||
EvaluateGameParams,
|
||||
EvaluatePositionWithUpdateParams,
|
||||
GameEval,
|
||||
@@ -13,6 +14,7 @@ import { computeAccuracy } from "./helpers/accuracy";
|
||||
import { getIsStalemate, getWhoIsCheckmated } from "../chess";
|
||||
import { getLichessEval } from "../lichess";
|
||||
import { getMovesClassification } from "./helpers/moveClassification";
|
||||
import { estimateEloFromEngineOutput } from "./helpers/estimateElo";
|
||||
import { EngineWorker, WorkerJob } from "@/types/engine";
|
||||
|
||||
export class UciEngine {
|
||||
@@ -276,10 +278,12 @@ export class UciEngine {
|
||||
fens
|
||||
);
|
||||
const accuracy = computeAccuracy(positions);
|
||||
const estimatedElo: EstimatedElo = estimateEloFromEngineOutput(positions);
|
||||
|
||||
this.isReady = true;
|
||||
return {
|
||||
positions: positionsWithClassification,
|
||||
estimatedElo,
|
||||
accuracy,
|
||||
settings: {
|
||||
engine: this.engineName,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Grid2 as Grid, Typography } from "@mui/material";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { gameEvalAtom } from "../../states";
|
||||
type props = {
|
||||
params: "accurecy" | "rating";
|
||||
};
|
||||
|
||||
export default function Accuracies() {
|
||||
export default function Accuracies(props: props) {
|
||||
const gameEval = useAtomValue(gameEvalAtom);
|
||||
|
||||
if (!gameEval) return null;
|
||||
@@ -24,11 +27,14 @@ export default function Accuracies() {
|
||||
fontWeight="bold"
|
||||
border="1px solid #424242"
|
||||
>
|
||||
{`${gameEval?.accuracy.white.toFixed(1)} %`}
|
||||
{props.params === "accurecy"
|
||||
? `${gameEval?.accuracy.white.toFixed(1)} %`
|
||||
: `${Math.round(gameEval?.estimatedElo.white as number)}`}
|
||||
</Typography>
|
||||
|
||||
<Typography align="center">Accuracies</Typography>
|
||||
|
||||
<Typography align="center">
|
||||
{props.params === "accurecy" ? "Accuracies" : "Estimated Elo"}
|
||||
</Typography>
|
||||
<Typography
|
||||
align="center"
|
||||
sx={{ backgroundColor: "black", color: "white" }}
|
||||
@@ -38,7 +44,9 @@ export default function Accuracies() {
|
||||
fontWeight="bold"
|
||||
border="1px solid #424242"
|
||||
>
|
||||
{`${gameEval?.accuracy.black.toFixed(1)} %`}
|
||||
{props.params === "accurecy"
|
||||
? `${gameEval?.accuracy.black.toFixed(1)} %`
|
||||
: `${Math.round(gameEval?.estimatedElo.black as number)}`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,8 @@ export default function AnalysisTab(props: GridProps) {
|
||||
: { overflow: "hidden", overflowY: "auto", ...props.sx }
|
||||
}
|
||||
>
|
||||
<Accuracies />
|
||||
<Accuracies params={"accurecy"} />
|
||||
<Accuracies params={"rating"} />
|
||||
|
||||
<MoveInfo />
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface Accuracy {
|
||||
black: number;
|
||||
}
|
||||
|
||||
export interface EstimatedElo {
|
||||
white: number | null;
|
||||
black: number | null;
|
||||
}
|
||||
export interface EngineSettings {
|
||||
engine: EngineName;
|
||||
depth: number;
|
||||
@@ -31,6 +35,7 @@ export interface EngineSettings {
|
||||
export interface GameEval {
|
||||
positions: PositionEval[];
|
||||
accuracy: Accuracy;
|
||||
estimatedElo: EstimatedElo;
|
||||
settings: EngineSettings;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user