feat : great & brilliant v0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { EvaluateGameParams, PositionEval } from "@/types/eval";
|
||||
import { Game } from "@/types/game";
|
||||
import { Chess } from "chess.js";
|
||||
import { Chess, PieceSymbol } from "chess.js";
|
||||
import { getPositionWinPercentage } from "./engine/helpers/winPercentage";
|
||||
|
||||
export const getEvaluateGameParams = (game: Chess): EvaluateGameParams => {
|
||||
@@ -76,12 +76,7 @@ export const moveLineUciToSan = (
|
||||
|
||||
return (moveUci: string): string => {
|
||||
try {
|
||||
const move = game.move({
|
||||
from: moveUci.slice(0, 2),
|
||||
to: moveUci.slice(2, 4),
|
||||
promotion: moveUci.slice(4, 5) || undefined,
|
||||
});
|
||||
|
||||
const move = game.move(uciMoveParams(moveUci));
|
||||
return move.san;
|
||||
} catch (e) {
|
||||
return moveUci;
|
||||
@@ -117,3 +112,68 @@ export const getWhoIsCheckmated = (fen: string): "w" | "b" | null => {
|
||||
if (!game.isCheckmate()) return null;
|
||||
return game.turn();
|
||||
};
|
||||
|
||||
export const uciMoveParams = (
|
||||
uciMove: string
|
||||
): {
|
||||
from: string;
|
||||
to: string;
|
||||
promotion?: string | undefined;
|
||||
} => ({
|
||||
from: uciMove.slice(0, 2),
|
||||
to: uciMove.slice(2, 4),
|
||||
promotion: uciMove.slice(4, 5) || undefined,
|
||||
});
|
||||
|
||||
export const getIsPieceSacrifice = (
|
||||
fen: string,
|
||||
playedMove: string,
|
||||
bestLinePvToPlay: string[]
|
||||
): boolean => {
|
||||
if (playedMove.slice(2, 4) !== bestLinePvToPlay[0].slice(2, 4)) return false;
|
||||
|
||||
const game = new Chess(fen);
|
||||
const whiteToPlay = game.turn() === "w";
|
||||
const startingMaterialDifference = getMaterialDifference(fen);
|
||||
game.move(uciMoveParams(playedMove));
|
||||
game.move(uciMoveParams(bestLinePvToPlay[0]));
|
||||
const endingMaterialDifference = getMaterialDifference(game.fen());
|
||||
|
||||
const materialDiff = endingMaterialDifference - startingMaterialDifference;
|
||||
const materialDiffPlayerRelative = whiteToPlay ? materialDiff : -materialDiff;
|
||||
|
||||
return materialDiffPlayerRelative < 0;
|
||||
};
|
||||
|
||||
export const getMaterialDifference = (fen: string): number => {
|
||||
const game = new Chess(fen);
|
||||
const board = game.board().flat();
|
||||
|
||||
return board.reduce((acc, square) => {
|
||||
if (!square) return acc;
|
||||
const piece = square.type;
|
||||
|
||||
if (square.color === "w") {
|
||||
return acc + getPieceValue(piece);
|
||||
}
|
||||
|
||||
return acc - getPieceValue(piece);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getPieceValue = (piece: PieceSymbol): number => {
|
||||
switch (piece) {
|
||||
case "p":
|
||||
return 1;
|
||||
case "n":
|
||||
return 3;
|
||||
case "b":
|
||||
return 3;
|
||||
case "r":
|
||||
return 5;
|
||||
case "q":
|
||||
return 9;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { PositionEval } from "@/types/eval";
|
||||
import { getPositionWinPercentage } from "./winPercentage";
|
||||
import { LineEval, PositionEval } from "@/types/eval";
|
||||
import {
|
||||
getLineWinPercentage,
|
||||
getPositionWinPercentage,
|
||||
} from "./winPercentage";
|
||||
import { MoveClassification } from "@/types/enums";
|
||||
import { openings } from "@/data/openings";
|
||||
import { getIsPieceSacrifice } from "@/lib/chess";
|
||||
|
||||
export const getMovesClassification = (
|
||||
rawPositions: PositionEval[],
|
||||
@@ -25,9 +29,54 @@ export const getMovesClassification = (
|
||||
};
|
||||
}
|
||||
|
||||
const uciMove = uciMoves[index - 1];
|
||||
const playedMove = uciMoves[index - 1];
|
||||
const bestMove = rawPositions[index - 1].bestMove;
|
||||
if (uciMove === bestMove) {
|
||||
|
||||
const lastPositionAlternativeLine: LineEval | undefined = rawPositions[
|
||||
index - 1
|
||||
].lines.filter((line) => line.pv[0] !== playedMove)?.[0];
|
||||
const lastPositionAlternativeLineWinPercentage = lastPositionAlternativeLine
|
||||
? getLineWinPercentage(lastPositionAlternativeLine)
|
||||
: undefined;
|
||||
|
||||
const bestLinePvToPlay = rawPosition.lines[0].pv;
|
||||
|
||||
const lastPositionWinPercentage = positionsWinPercentage[index - 1];
|
||||
const positionWinPercentage = positionsWinPercentage[index];
|
||||
const isWhiteMove = index % 2 === 1;
|
||||
|
||||
if (
|
||||
isBrilliantMove(
|
||||
positionWinPercentage,
|
||||
isWhiteMove,
|
||||
playedMove,
|
||||
bestLinePvToPlay,
|
||||
fens[index - 1]
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...rawPosition,
|
||||
opening: currentOpening,
|
||||
moveClassification: MoveClassification.Brilliant,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isGreatMove(
|
||||
lastPositionWinPercentage,
|
||||
positionWinPercentage,
|
||||
isWhiteMove,
|
||||
lastPositionAlternativeLineWinPercentage
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...rawPosition,
|
||||
opening: currentOpening,
|
||||
moveClassification: MoveClassification.Great,
|
||||
};
|
||||
}
|
||||
|
||||
if (playedMove === bestMove) {
|
||||
return {
|
||||
...rawPosition,
|
||||
opening: currentOpening,
|
||||
@@ -35,11 +84,7 @@ export const getMovesClassification = (
|
||||
};
|
||||
}
|
||||
|
||||
const lastPositionWinPercentage = positionsWinPercentage[index - 1];
|
||||
const positionWinPercentage = positionsWinPercentage[index];
|
||||
const isWhiteMove = index % 2 === 1;
|
||||
|
||||
const moveClassification = getMoveClassification(
|
||||
const moveClassification = getMoveBasicClassification(
|
||||
lastPositionWinPercentage,
|
||||
positionWinPercentage,
|
||||
isWhiteMove
|
||||
@@ -55,7 +100,7 @@ export const getMovesClassification = (
|
||||
return positions;
|
||||
};
|
||||
|
||||
const getMoveClassification = (
|
||||
const getMoveBasicClassification = (
|
||||
lastPositionWinPercentage: number,
|
||||
positionWinPercentage: number,
|
||||
isWhiteMove: boolean
|
||||
@@ -70,3 +115,83 @@ const getMoveClassification = (
|
||||
if (winPercentageDiff < -2) return MoveClassification.Good;
|
||||
return MoveClassification.Excellent;
|
||||
};
|
||||
|
||||
const isBrilliantMove = (
|
||||
positionWinPercentage: number,
|
||||
isWhiteMove: boolean,
|
||||
playedMove: string,
|
||||
bestLinePvToPlay: string[],
|
||||
fen: string
|
||||
): boolean => {
|
||||
const isPieceSacrifice = getIsPieceSacrifice(
|
||||
fen,
|
||||
playedMove,
|
||||
bestLinePvToPlay
|
||||
);
|
||||
|
||||
if (!isPieceSacrifice) return false;
|
||||
|
||||
const isNotLosing = isWhiteMove
|
||||
? positionWinPercentage > 50
|
||||
: positionWinPercentage < 50;
|
||||
const isAlternateCompletelyWinning = isWhiteMove
|
||||
? positionWinPercentage > 70
|
||||
: positionWinPercentage < 30;
|
||||
|
||||
return isNotLosing && !isAlternateCompletelyWinning;
|
||||
};
|
||||
|
||||
const isGreatMove = (
|
||||
lastPositionWinPercentage: number,
|
||||
positionWinPercentage: number,
|
||||
isWhiteMove: boolean,
|
||||
lastPositionAlternativeLineWinPercentage: number | undefined
|
||||
): boolean => {
|
||||
if (!lastPositionAlternativeLineWinPercentage) return false;
|
||||
|
||||
const winPercentageDiff =
|
||||
(positionWinPercentage - lastPositionWinPercentage) *
|
||||
(isWhiteMove ? 1 : -1);
|
||||
|
||||
if (winPercentageDiff < -2) return false;
|
||||
|
||||
const hasChangedGameOutcome = getHasChangedGameOutcome(
|
||||
lastPositionWinPercentage,
|
||||
positionWinPercentage,
|
||||
isWhiteMove
|
||||
);
|
||||
|
||||
const isTheOnlyGoodMove = getIsTheOnlyGoodMove(
|
||||
positionWinPercentage,
|
||||
lastPositionAlternativeLineWinPercentage,
|
||||
isWhiteMove
|
||||
);
|
||||
|
||||
return hasChangedGameOutcome && isTheOnlyGoodMove;
|
||||
};
|
||||
|
||||
const getHasChangedGameOutcome = (
|
||||
lastPositionWinPercentage: number,
|
||||
positionWinPercentage: number,
|
||||
isWhiteMove: boolean
|
||||
): boolean => {
|
||||
const winPercentageDiff =
|
||||
(positionWinPercentage - lastPositionWinPercentage) *
|
||||
(isWhiteMove ? 1 : -1);
|
||||
return (
|
||||
winPercentageDiff > 10 &&
|
||||
((lastPositionWinPercentage < 50 && positionWinPercentage > 50) ||
|
||||
(lastPositionWinPercentage > 50 && positionWinPercentage < 50))
|
||||
);
|
||||
};
|
||||
|
||||
const getIsTheOnlyGoodMove = (
|
||||
positionWinPercentage: number,
|
||||
lastPositionAlternativeLineWinPercentage: number,
|
||||
isWhiteMove: boolean
|
||||
): boolean => {
|
||||
const winPercentageDiff =
|
||||
(positionWinPercentage - lastPositionAlternativeLineWinPercentage) *
|
||||
(isWhiteMove ? 1 : -1);
|
||||
return winPercentageDiff > 5;
|
||||
};
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { ceilsNumber } from "@/lib/helpers";
|
||||
import { PositionEval } from "@/types/eval";
|
||||
import { LineEval, PositionEval } from "@/types/eval";
|
||||
|
||||
export const getPositionWinPercentage = (position: PositionEval): number => {
|
||||
if (position.lines[0].cp !== undefined) {
|
||||
return getWinPercentageFromCp(position.lines[0].cp);
|
||||
return getLineWinPercentage(position.lines[0]);
|
||||
};
|
||||
|
||||
export const getLineWinPercentage = (line: LineEval): number => {
|
||||
if (line.cp !== undefined) {
|
||||
return getWinPercentageFromCp(line.cp);
|
||||
}
|
||||
|
||||
if (position.lines[0].mate !== undefined) {
|
||||
return getWinPercentageFromMate(position.lines[0].mate);
|
||||
if (line.mate !== undefined) {
|
||||
return getWinPercentageFromMate(line.mate);
|
||||
}
|
||||
|
||||
throw new Error("No cp or mate in move");
|
||||
throw new Error("No cp or mate in line");
|
||||
};
|
||||
|
||||
const getWinPercentageFromMate = (mate: number): number => {
|
||||
|
||||
@@ -38,7 +38,7 @@ export abstract class UciEngine {
|
||||
this.throwErrorIfNotReady();
|
||||
}
|
||||
|
||||
if (multiPv < 1 || multiPv > 6) {
|
||||
if (multiPv < 2 || multiPv > 6) {
|
||||
throw new Error(`Invalid MultiPV value : ${multiPv}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,10 @@ SquareRenderer.displayName = "CustomSquareRenderer";
|
||||
export default SquareRenderer;
|
||||
|
||||
export const moveClassificationColors: Record<MoveClassification, string> = {
|
||||
[MoveClassification.Best]: "#3aab18",
|
||||
[MoveClassification.Book]: "#d5a47d",
|
||||
[MoveClassification.Brilliant]: "#26c2a3",
|
||||
[MoveClassification.Great]: "#749bbf",
|
||||
[MoveClassification.Best]: "#3aab18",
|
||||
[MoveClassification.Excellent]: "#3aab18",
|
||||
[MoveClassification.Good]: "#81b64c",
|
||||
[MoveClassification.Inaccuracy]: "#f7c631",
|
||||
|
||||
@@ -42,11 +42,13 @@ export default function MoveInfo() {
|
||||
}
|
||||
|
||||
const moveClassificationLabels: Record<MoveClassification, string> = {
|
||||
[MoveClassification.Blunder]: "a blunder",
|
||||
[MoveClassification.Mistake]: "a mistake",
|
||||
[MoveClassification.Inaccuracy]: "an inaccuracy",
|
||||
[MoveClassification.Good]: "good",
|
||||
[MoveClassification.Excellent]: "excellent",
|
||||
[MoveClassification.Best]: "the best move",
|
||||
[MoveClassification.Book]: "a book move",
|
||||
[MoveClassification.Brilliant]: "brilliant !!",
|
||||
[MoveClassification.Great]: "a great move !",
|
||||
[MoveClassification.Best]: "the best move",
|
||||
[MoveClassification.Excellent]: "excellent",
|
||||
[MoveClassification.Good]: "good",
|
||||
[MoveClassification.Inaccuracy]: "an inaccuracy",
|
||||
[MoveClassification.Mistake]: "a mistake",
|
||||
[MoveClassification.Blunder]: "a blunder",
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
|
||||
label="Number of lines"
|
||||
value={multiPv}
|
||||
setValue={setMultiPv}
|
||||
min={1}
|
||||
min={2}
|
||||
max={6}
|
||||
xs={6}
|
||||
/>
|
||||
|
||||
@@ -15,4 +15,6 @@ export enum MoveClassification {
|
||||
Excellent = "excellent",
|
||||
Best = "best",
|
||||
Book = "book",
|
||||
Great = "great",
|
||||
Brilliant = "brilliant",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user