Skip to content

Commit e992679

Browse files
feat: ✨ added elo esstimation feature
1 parent ef8e25c commit e992679

File tree

5 files changed

+77
-10
lines changed

5 files changed

+77
-10
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ceilsNumber } from "@/lib/math";
2+
import { EstimatedElo, PositionEval } from "@/types/eval";
3+
4+
export const estimateEloFromEngineOutput = (positions: PositionEval[]): EstimatedElo => {
5+
try {
6+
7+
if (!positions || positions.length === 0) {
8+
return { white: null, black: null };
9+
}
10+
11+
let totalCPLWhite = 0;
12+
let totalCPLBlack = 0;
13+
let moveCount = 0;
14+
let previousCp = null;
15+
let flag = true;
16+
for (const moveAnalysis of positions) {
17+
if (moveAnalysis.lines && moveAnalysis.lines.length > 0) {
18+
19+
const bestLine = moveAnalysis.lines[0];
20+
if (bestLine.cp !== undefined) {
21+
if(previousCp !== null) {
22+
const diff = Math.abs(bestLine.cp - previousCp);
23+
if (flag){
24+
totalCPLWhite += ceilsNumber(diff, -1000, 1000);
25+
}
26+
else {
27+
totalCPLBlack += ceilsNumber(diff, -1000, 1000);
28+
}
29+
flag = !flag;
30+
moveCount++;
31+
}
32+
previousCp = bestLine.cp;
33+
34+
}
35+
}
36+
}
37+
38+
if (moveCount === 0) {
39+
return { white: null, black: null };
40+
}
41+
42+
const averageCPLWhite = totalCPLWhite / Math.ceil(moveCount/2);
43+
const averageCPLBlack = totalCPLBlack / Math.floor(moveCount/2);
44+
45+
const estimateElo = (averageCPL: number) => 3100 * Math.exp(-0.01 * averageCPL);
46+
47+
const whiteElo = estimateElo(Math.abs(averageCPLWhite));
48+
const blackElo = estimateElo(Math.abs(averageCPLBlack));
49+
50+
return { white: whiteElo, black: blackElo };
51+
} catch (error) {
52+
console.error("Error estimating Elo: ", error);
53+
return { white: null, black: null };
54+
}
55+
}

src/lib/engine/uciEngine.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EngineName } from "@/types/enums";
22
import {
3+
EstimatedElo,
34
EvaluateGameParams,
45
EvaluatePositionWithUpdateParams,
56
GameEval,
@@ -14,6 +15,7 @@ import { getIsStalemate, getWhoIsCheckmated } from "../chess";
1415
import { getLichessEval } from "../lichess";
1516
import { getMovesClassification } from "./helpers/moveClassification";
1617
import { EngineWorker } from "@/types/engine";
18+
import { estimateEloFromEngineOutput } from "./helpers/estimateElo";
1719

1820
export class UciEngine {
1921
private worker: EngineWorker;
@@ -141,7 +143,7 @@ export class UciEngine {
141143

142144
await this.sendCommands(["ucinewgame", "isready"], "readyok");
143145
this.worker.uci("position startpos");
144-
146+
145147
const positions: PositionEval[] = [];
146148
for (const fen of fens) {
147149
const whoIsCheckmated = getWhoIsCheckmated(fen);
@@ -180,17 +182,19 @@ export class UciEngine {
180182
99 - Math.exp(-4 * (fens.indexOf(fen) / fens.length)) * 99
181183
);
182184
}
183-
185+
184186
const positionsWithClassification = getMovesClassification(
185187
positions,
186188
uciMoves,
187189
fens
188190
);
189191
const accuracy = computeAccuracy(positions);
190-
192+
const estimatedElo: EstimatedElo = estimateEloFromEngineOutput(positions);
193+
191194
this.ready = true;
192195
return {
193196
positions: positionsWithClassification,
197+
estimatedElo,
194198
accuracy,
195199
settings: {
196200
engine: this.engineName,

src/sections/analysis/panelBody/analysisTab/accuracies.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Grid2 as Grid, Typography } from "@mui/material";
22
import { useAtomValue } from "jotai";
33
import { gameEvalAtom } from "../../states";
4+
type props = {
5+
params: "accurecy" | "rating";
6+
}
47

5-
export default function Accuracies() {
8+
export default function Accuracies(props: props) {
69
const gameEval = useAtomValue(gameEvalAtom);
710

811
if (!gameEval) return null;
@@ -24,11 +27,10 @@ export default function Accuracies() {
2427
fontWeight="bold"
2528
border="1px solid #424242"
2629
>
27-
{`${gameEval?.accuracy.white.toFixed(1)} %`}
30+
{ props.params === "accurecy" ? `${gameEval?.accuracy.white.toFixed(1)} %` : `${Math.round(gameEval?.estimatedElo.white as number)}`}
2831
</Typography>
2932

30-
<Typography align="center">Accuracies</Typography>
31-
33+
<Typography align="center">{ props.params === "accurecy" ? "Accuracies" : "Estimated Elo"}</Typography>
3234
<Typography
3335
align="center"
3436
sx={{ backgroundColor: "black", color: "white" }}
@@ -38,7 +40,7 @@ export default function Accuracies() {
3840
fontWeight="bold"
3941
border="1px solid #424242"
4042
>
41-
{`${gameEval?.accuracy.black.toFixed(1)} %`}
43+
{props.params === "accurecy" ? `${gameEval?.accuracy.black.toFixed(1)} %` : `${Math.round(gameEval?.estimatedElo.black as number)}`}
4244
</Typography>
4345
</Grid>
4446
);

src/sections/analysis/panelBody/analysisTab/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ export default function AnalysisTab(props: GridProps) {
5757
: { overflow: "hidden", overflowY: "auto", ...props.sx }
5858
}
5959
>
60-
<Accuracies />
61-
60+
<Accuracies params={"accurecy"}/>
61+
<Accuracies params={"rating"}/>
62+
6263
<MoveInfo />
6364

6465
<Opening />

src/types/eval.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export interface Accuracy {
2121
black: number;
2222
}
2323

24+
export interface EstimatedElo {
25+
white: number | null;
26+
black: number | null;
27+
}
2428
export interface EngineSettings {
2529
engine: EngineName;
2630
depth: number;
@@ -31,6 +35,7 @@ export interface EngineSettings {
3135
export interface GameEval {
3236
positions: PositionEval[];
3337
accuracy: Accuracy;
38+
estimatedElo: EstimatedElo;
3439
settings: EngineSettings;
3540
}
3641

0 commit comments

Comments
 (0)