Skip to content

feat: ✨ added elo estimation feature #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/lib/engine/helpers/estimateElo.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
};
4 changes: 4 additions & 0 deletions src/lib/engine/uciEngine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EngineName } from "@/types/enums";
import {
EstimatedElo,
EvaluateGameParams,
EvaluatePositionWithUpdateParams,
GameEval,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 13 additions & 5 deletions src/sections/analysis/panelBody/analysisTab/accuracies.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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" }}
Expand All @@ -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>
);
Expand Down
3 changes: 2 additions & 1 deletion src/sections/analysis/panelBody/analysisTab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export default function AnalysisTab(props: GridProps) {
: { overflow: "hidden", overflowY: "auto", ...props.sx }
}
>
<Accuracies />
<Accuracies params={"accurecy"} />
<Accuracies params={"rating"} />

<MoveInfo />

Expand Down
5 changes: 5 additions & 0 deletions src/types/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +35,7 @@ export interface EngineSettings {
export interface GameEval {
positions: PositionEval[];
accuracy: Accuracy;
estimatedElo: EstimatedElo;
settings: EngineSettings;
}

Expand Down