feat : add move classification
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
getWeightedMean,
|
||||
} from "@/lib/helpers";
|
||||
import { Accuracy, MoveEval } from "@/types/eval";
|
||||
import { getPositionWinPercentage } from "./winPercentage";
|
||||
|
||||
export const computeAccuracy = (moves: MoveEval[]): Accuracy => {
|
||||
const movesWinPercentage = moves.map(getPositionWinPercentage);
|
||||
@@ -85,27 +86,3 @@ const getMovesAccuracy = (movesWinPercentage: number[]): number[] =>
|
||||
|
||||
return Math.min(100, Math.max(0, rawAccuracy + 1));
|
||||
});
|
||||
|
||||
const getPositionWinPercentage = (move: MoveEval): number => {
|
||||
if (move.lines[0].cp !== undefined) {
|
||||
return getWinPercentageFromCp(move.lines[0].cp);
|
||||
}
|
||||
|
||||
if (move.lines[0].mate !== undefined) {
|
||||
return getWinPercentageFromMate(move.lines[0].mate);
|
||||
}
|
||||
|
||||
throw new Error("No cp or mate in move");
|
||||
};
|
||||
|
||||
const getWinPercentageFromMate = (mate: number): number => {
|
||||
const mateInf = mate * Infinity;
|
||||
return getWinPercentageFromCp(mateInf);
|
||||
};
|
||||
|
||||
const getWinPercentageFromCp = (cp: number): number => {
|
||||
const cpCeiled = ceilsNumber(cp, -1000, 1000);
|
||||
const MULTIPLIER = -0.00368208;
|
||||
const winChances = 2 / (1 + Math.exp(MULTIPLIER * cpCeiled)) - 1;
|
||||
return 50 + 50 * winChances;
|
||||
};
|
||||
|
||||
72
src/lib/engine/helpers/moveClassification.ts
Normal file
72
src/lib/engine/helpers/moveClassification.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { MoveEval } from "@/types/eval";
|
||||
import { getPositionWinPercentage } from "./winPercentage";
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
import { openings } from "@/data/openings";
|
||||
|
||||
export const getMovesClassification = (
|
||||
rawMoves: MoveEval[],
|
||||
uciMoves: string[],
|
||||
fens: string[]
|
||||
): MoveEval[] => {
|
||||
const positionsWinPercentage = rawMoves.map(getPositionWinPercentage);
|
||||
let currentOpening: string | undefined = undefined;
|
||||
|
||||
const moves = rawMoves.map((rawMove, index) => {
|
||||
if (index === 0) return rawMove;
|
||||
|
||||
const currentFen = fens[index].split(" ")[0];
|
||||
const opening = openings.find((opening) => opening.fen === currentFen);
|
||||
if (opening) {
|
||||
currentOpening = opening.name;
|
||||
return {
|
||||
...rawMove,
|
||||
opening: opening.name,
|
||||
moveClassification: MoveClassification.Book,
|
||||
};
|
||||
}
|
||||
|
||||
const uciMove = uciMoves[index - 1];
|
||||
const bestMove = rawMoves[index - 1].bestMove;
|
||||
if (uciMove === bestMove) {
|
||||
return {
|
||||
...rawMove,
|
||||
opening: currentOpening,
|
||||
moveClassification: MoveClassification.Best,
|
||||
};
|
||||
}
|
||||
|
||||
const lastPositionWinPercentage = positionsWinPercentage[index - 1];
|
||||
const positionWinPercentage = positionsWinPercentage[index];
|
||||
const isWhiteMove = index % 2 === 1;
|
||||
|
||||
const moveClassification = getMoveClassification(
|
||||
lastPositionWinPercentage,
|
||||
positionWinPercentage,
|
||||
isWhiteMove
|
||||
);
|
||||
|
||||
return {
|
||||
...rawMove,
|
||||
opening: currentOpening,
|
||||
moveClassification,
|
||||
};
|
||||
});
|
||||
|
||||
return moves;
|
||||
};
|
||||
|
||||
const getMoveClassification = (
|
||||
lastPositionWinPercentage: number,
|
||||
positionWinPercentage: number,
|
||||
isWhiteMove: boolean
|
||||
): MoveClassification => {
|
||||
const winPercentageDiff =
|
||||
(positionWinPercentage - lastPositionWinPercentage) *
|
||||
(isWhiteMove ? 1 : -1);
|
||||
|
||||
if (winPercentageDiff < -15) return MoveClassification.Blunder;
|
||||
if (winPercentageDiff < -10) return MoveClassification.Mistake;
|
||||
if (winPercentageDiff < -5) return MoveClassification.Inaccuracy;
|
||||
if (winPercentageDiff < 0) return MoveClassification.Good;
|
||||
return MoveClassification.Excellent;
|
||||
};
|
||||
@@ -5,7 +5,6 @@ export const parseEvaluationResults = (
|
||||
whiteToPlay: boolean
|
||||
): MoveEval => {
|
||||
const parsedResults: MoveEval = {
|
||||
bestMove: "",
|
||||
lines: [],
|
||||
};
|
||||
const tempResults: Record<string, LineEval> = {};
|
||||
|
||||
26
src/lib/engine/helpers/winPercentage.ts
Normal file
26
src/lib/engine/helpers/winPercentage.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ceilsNumber } from "@/lib/helpers";
|
||||
import { MoveEval } from "@/types/eval";
|
||||
|
||||
export const getPositionWinPercentage = (move: MoveEval): number => {
|
||||
if (move.lines[0].cp !== undefined) {
|
||||
return getWinPercentageFromCp(move.lines[0].cp);
|
||||
}
|
||||
|
||||
if (move.lines[0].mate !== undefined) {
|
||||
return getWinPercentageFromMate(move.lines[0].mate);
|
||||
}
|
||||
|
||||
throw new Error("No cp or mate in move");
|
||||
};
|
||||
|
||||
const getWinPercentageFromMate = (mate: number): number => {
|
||||
const mateInf = mate * Infinity;
|
||||
return getWinPercentageFromCp(mateInf);
|
||||
};
|
||||
|
||||
const getWinPercentageFromCp = (cp: number): number => {
|
||||
const cpCeiled = ceilsNumber(cp, -1000, 1000);
|
||||
const MULTIPLIER = -0.00368208;
|
||||
const winChances = 2 / (1 + Math.exp(MULTIPLIER * cpCeiled)) - 1;
|
||||
return 50 + 50 * winChances;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EngineName } from "@/types/enums";
|
||||
import {
|
||||
EvaluateGameParams,
|
||||
EvaluatePositionWithUpdateParams,
|
||||
GameEval,
|
||||
MoveEval,
|
||||
@@ -8,6 +9,7 @@ import { parseEvaluationResults } from "./helpers/parseResults";
|
||||
import { computeAccuracy } from "./helpers/accuracy";
|
||||
import { getWhoIsCheckmated } from "../chess";
|
||||
import { getLichessEval } from "../lichess";
|
||||
import { getMovesClassification } from "./helpers/moveClassification";
|
||||
|
||||
export abstract class UciEngine {
|
||||
private worker: Worker;
|
||||
@@ -93,11 +95,12 @@ export abstract class UciEngine {
|
||||
});
|
||||
}
|
||||
|
||||
public async evaluateGame(
|
||||
fens: string[],
|
||||
public async evaluateGame({
|
||||
fens,
|
||||
uciMoves,
|
||||
depth = 16,
|
||||
multiPv = this.multiPv
|
||||
): Promise<GameEval> {
|
||||
multiPv = this.multiPv,
|
||||
}: EvaluateGameParams): Promise<GameEval> {
|
||||
this.throwErrorIfNotReady();
|
||||
await this.setMultiPv(multiPv);
|
||||
this.ready = false;
|
||||
@@ -110,7 +113,6 @@ export abstract class UciEngine {
|
||||
const whoIsCheckmated = getWhoIsCheckmated(fen);
|
||||
if (whoIsCheckmated) {
|
||||
moves.push({
|
||||
bestMove: "",
|
||||
lines: [
|
||||
{
|
||||
pv: [],
|
||||
@@ -126,11 +128,16 @@ export abstract class UciEngine {
|
||||
moves.push(result);
|
||||
}
|
||||
|
||||
const movesWithClassification = getMovesClassification(
|
||||
moves,
|
||||
uciMoves,
|
||||
fens
|
||||
);
|
||||
const accuracy = computeAccuracy(moves);
|
||||
|
||||
this.ready = true;
|
||||
return {
|
||||
moves: moves.slice(0, -1),
|
||||
moves: movesWithClassification,
|
||||
accuracy,
|
||||
settings: {
|
||||
engine: this.engineName,
|
||||
|
||||
Reference in New Issue
Block a user