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 { EngineName } from "@/types/enums";
|
||||||
import {
|
import {
|
||||||
|
EstimatedElo,
|
||||||
EvaluateGameParams,
|
EvaluateGameParams,
|
||||||
EvaluatePositionWithUpdateParams,
|
EvaluatePositionWithUpdateParams,
|
||||||
GameEval,
|
GameEval,
|
||||||
@@ -13,6 +14,7 @@ import { computeAccuracy } from "./helpers/accuracy";
|
|||||||
import { getIsStalemate, getWhoIsCheckmated } from "../chess";
|
import { getIsStalemate, getWhoIsCheckmated } from "../chess";
|
||||||
import { getLichessEval } from "../lichess";
|
import { getLichessEval } from "../lichess";
|
||||||
import { getMovesClassification } from "./helpers/moveClassification";
|
import { getMovesClassification } from "./helpers/moveClassification";
|
||||||
|
import { estimateEloFromEngineOutput } from "./helpers/estimateElo";
|
||||||
import { EngineWorker, WorkerJob } from "@/types/engine";
|
import { EngineWorker, WorkerJob } from "@/types/engine";
|
||||||
|
|
||||||
export class UciEngine {
|
export class UciEngine {
|
||||||
@@ -276,10 +278,12 @@ export class UciEngine {
|
|||||||
fens
|
fens
|
||||||
);
|
);
|
||||||
const accuracy = computeAccuracy(positions);
|
const accuracy = computeAccuracy(positions);
|
||||||
|
const estimatedElo: EstimatedElo = estimateEloFromEngineOutput(positions);
|
||||||
|
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
return {
|
return {
|
||||||
positions: positionsWithClassification,
|
positions: positionsWithClassification,
|
||||||
|
estimatedElo,
|
||||||
accuracy,
|
accuracy,
|
||||||
settings: {
|
settings: {
|
||||||
engine: this.engineName,
|
engine: this.engineName,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Grid2 as Grid, Typography } from "@mui/material";
|
import { Grid2 as Grid, Typography } from "@mui/material";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { gameEvalAtom } from "../../states";
|
import { gameEvalAtom } from "../../states";
|
||||||
|
type props = {
|
||||||
|
params: "accurecy" | "rating";
|
||||||
|
};
|
||||||
|
|
||||||
export default function Accuracies() {
|
export default function Accuracies(props: props) {
|
||||||
const gameEval = useAtomValue(gameEvalAtom);
|
const gameEval = useAtomValue(gameEvalAtom);
|
||||||
|
|
||||||
if (!gameEval) return null;
|
if (!gameEval) return null;
|
||||||
@@ -24,11 +27,14 @@ export default function Accuracies() {
|
|||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
border="1px solid #424242"
|
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>
|
||||||
|
|
||||||
<Typography align="center">Accuracies</Typography>
|
<Typography align="center">
|
||||||
|
{props.params === "accurecy" ? "Accuracies" : "Estimated Elo"}
|
||||||
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
align="center"
|
align="center"
|
||||||
sx={{ backgroundColor: "black", color: "white" }}
|
sx={{ backgroundColor: "black", color: "white" }}
|
||||||
@@ -38,7 +44,9 @@ export default function Accuracies() {
|
|||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
border="1px solid #424242"
|
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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export default function AnalysisTab(props: GridProps) {
|
|||||||
: { overflow: "hidden", overflowY: "auto", ...props.sx }
|
: { overflow: "hidden", overflowY: "auto", ...props.sx }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Accuracies />
|
<Accuracies params={"accurecy"} />
|
||||||
|
<Accuracies params={"rating"} />
|
||||||
|
|
||||||
<MoveInfo />
|
<MoveInfo />
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export interface Accuracy {
|
|||||||
black: number;
|
black: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EstimatedElo {
|
||||||
|
white: number | null;
|
||||||
|
black: number | null;
|
||||||
|
}
|
||||||
export interface EngineSettings {
|
export interface EngineSettings {
|
||||||
engine: EngineName;
|
engine: EngineName;
|
||||||
depth: number;
|
depth: number;
|
||||||
@@ -31,6 +35,7 @@ export interface EngineSettings {
|
|||||||
export interface GameEval {
|
export interface GameEval {
|
||||||
positions: PositionEval[];
|
positions: PositionEval[];
|
||||||
accuracy: Accuracy;
|
accuracy: Accuracy;
|
||||||
|
estimatedElo: EstimatedElo;
|
||||||
settings: EngineSettings;
|
settings: EngineSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user