From 1501333c6e7a093c293e76f75805df1487eba84f Mon Sep 17 00:00:00 2001 From: Pratham Shah Date: Sun, 13 Apr 2025 15:36:34 +0530 Subject: [PATCH 1/2] feat: :sparkles: added elo esstimation feature --- src/lib/engine/helpers/estimateElo.ts | 54 +++++++++++++++++++ src/lib/engine/uciEngine.ts | 4 ++ .../panelBody/analysisTab/accuracies.tsx | 18 +++++-- .../analysis/panelBody/analysisTab/index.tsx | 3 +- src/types/eval.ts | 5 ++ 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/lib/engine/helpers/estimateElo.ts diff --git a/src/lib/engine/helpers/estimateElo.ts b/src/lib/engine/helpers/estimateElo.ts new file mode 100644 index 0000000..3d3d6cb --- /dev/null +++ b/src/lib/engine/helpers/estimateElo.ts @@ -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 }; + } +}; diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index b3dc409..9a9759a 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -1,5 +1,6 @@ import { EngineName } from "@/types/enums"; import { + EstimatedElo, EvaluateGameParams, EvaluatePositionWithUpdateParams, GameEval, @@ -14,6 +15,7 @@ import { getIsStalemate, getWhoIsCheckmated } from "../chess"; import { getLichessEval } from "../lichess"; import { getMovesClassification } from "./helpers/moveClassification"; import { EngineWorker } from "@/types/engine"; +import { estimateEloFromEngineOutput } from "./helpers/estimateElo"; export class UciEngine { private worker: EngineWorker; @@ -187,10 +189,12 @@ export class UciEngine { fens ); const accuracy = computeAccuracy(positions); + const estimatedElo: EstimatedElo = estimateEloFromEngineOutput(positions); this.ready = true; return { positions: positionsWithClassification, + estimatedElo, accuracy, settings: { engine: this.engineName, diff --git a/src/sections/analysis/panelBody/analysisTab/accuracies.tsx b/src/sections/analysis/panelBody/analysisTab/accuracies.tsx index 70894b3..2dbad7d 100644 --- a/src/sections/analysis/panelBody/analysisTab/accuracies.tsx +++ b/src/sections/analysis/panelBody/analysisTab/accuracies.tsx @@ -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)}`} - Accuracies - + + {props.params === "accurecy" ? "Accuracies" : "Estimated Elo"} + - {`${gameEval?.accuracy.black.toFixed(1)} %`} + {props.params === "accurecy" + ? `${gameEval?.accuracy.black.toFixed(1)} %` + : `${Math.round(gameEval?.estimatedElo.black as number)}`} ); diff --git a/src/sections/analysis/panelBody/analysisTab/index.tsx b/src/sections/analysis/panelBody/analysisTab/index.tsx index bf61ef2..02f2755 100644 --- a/src/sections/analysis/panelBody/analysisTab/index.tsx +++ b/src/sections/analysis/panelBody/analysisTab/index.tsx @@ -57,7 +57,8 @@ export default function AnalysisTab(props: GridProps) { : { overflow: "hidden", overflowY: "auto", ...props.sx } } > - + + diff --git a/src/types/eval.ts b/src/types/eval.ts index 7c51fab..67ebd67 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -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; } From 4ce46207253298fc52e3109ec5ea4d0a1d86be6a Mon Sep 17 00:00:00 2001 From: Pratham Shah Date: Sun, 13 Apr 2025 15:39:18 +0530 Subject: [PATCH 2/2] feat: :sparkles: add script for fixing linting errors --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c977c01..98f3ed5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "next build", "start": "next start", "lint": "next lint && tsc --noEmit", + "lint:fix": "eslint --fix --ext .ts,.tsx .", "deploy": "firebase deploy --project=freechessproject --only hosting" }, "dependencies": {