diff --git a/public/icons/brilliant.png b/public/icons/brilliant.png new file mode 100644 index 0000000..20fa69d Binary files /dev/null and b/public/icons/brilliant.png differ diff --git a/public/icons/great.png b/public/icons/great.png new file mode 100644 index 0000000..b3dcfdc Binary files /dev/null and b/public/icons/great.png differ diff --git a/src/lib/chess.ts b/src/lib/chess.ts index ad26e13..7586e08 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -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; + } +}; diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index dadda37..54fa9b8 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -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; +}; diff --git a/src/lib/engine/helpers/winPercentage.ts b/src/lib/engine/helpers/winPercentage.ts index 8f3074b..d20ab62 100644 --- a/src/lib/engine/helpers/winPercentage.ts +++ b/src/lib/engine/helpers/winPercentage.ts @@ -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 => { diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index 4f905af..d55e085 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -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}`); } diff --git a/src/sections/analysis/board/squareRenderer.tsx b/src/sections/analysis/board/squareRenderer.tsx index 33b17c4..98fbf9b 100644 --- a/src/sections/analysis/board/squareRenderer.tsx +++ b/src/sections/analysis/board/squareRenderer.tsx @@ -58,8 +58,10 @@ SquareRenderer.displayName = "CustomSquareRenderer"; export default SquareRenderer; export const moveClassificationColors: Record = { - [MoveClassification.Best]: "#3aab18", [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Brilliant]: "#26c2a3", + [MoveClassification.Great]: "#749bbf", + [MoveClassification.Best]: "#3aab18", [MoveClassification.Excellent]: "#3aab18", [MoveClassification.Good]: "#81b64c", [MoveClassification.Inaccuracy]: "#f7c631", diff --git a/src/sections/analysis/reviewPanelBody/moveInfo.tsx b/src/sections/analysis/reviewPanelBody/moveInfo.tsx index caffbff..b57f89a 100644 --- a/src/sections/analysis/reviewPanelBody/moveInfo.tsx +++ b/src/sections/analysis/reviewPanelBody/moveInfo.tsx @@ -42,11 +42,13 @@ export default function MoveInfo() { } const moveClassificationLabels: Record = { - [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", }; diff --git a/src/sections/engineSettings/engineSettingsDialog.tsx b/src/sections/engineSettings/engineSettingsDialog.tsx index 670d261..08f5517 100644 --- a/src/sections/engineSettings/engineSettingsDialog.tsx +++ b/src/sections/engineSettings/engineSettingsDialog.tsx @@ -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} /> diff --git a/src/types/enums.ts b/src/types/enums.ts index 0835cee..d658e2d 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -15,4 +15,6 @@ export enum MoveClassification { Excellent = "excellent", Best = "best", Book = "book", + Great = "great", + Brilliant = "brilliant", }