From 7421258e4e524dd72e67610fb38c8a0153ad8a13 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Sun, 10 Mar 2024 03:17:52 +0100 Subject: [PATCH] feat : great & brilliant v0 --- public/icons/brilliant.png | Bin 0 -> 1272 bytes public/icons/great.png | Bin 0 -> 1557 bytes src/lib/chess.ts | 74 ++++++++- src/lib/engine/helpers/moveClassification.ts | 145 ++++++++++++++++-- src/lib/engine/helpers/winPercentage.ts | 16 +- src/lib/engine/uciEngine.ts | 2 +- .../analysis/board/squareRenderer.tsx | 4 +- .../analysis/reviewPanelBody/moveInfo.tsx | 14 +- .../engineSettings/engineSettingsDialog.tsx | 2 +- src/types/enums.ts | 2 + 10 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 public/icons/brilliant.png create mode 100644 public/icons/great.png diff --git a/public/icons/brilliant.png b/public/icons/brilliant.png new file mode 100644 index 0000000000000000000000000000000000000000..20fa69da5b601ba689ea0677ed8fe46c9084ab2b GIT binary patch literal 1272 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 { @@ -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", }