From 50d203277ee279f1eb34c7d07003193274fd29fc Mon Sep 17 00:00:00 2001 From: Arne Malaka Date: Fri, 8 May 2026 23:44:52 +0200 Subject: [PATCH 1/3] Reworked move classification with adjusted logic and new "miss"-category Co-authored-by: Copilot --- public/icons/{opening.png => book.png} | Bin public/icons/{splendid.png => brilliant.png} | Bin public/icons/{okay.png => good.png} | Bin public/icons/{perfect.png => great.png} | Bin public/icons/miss.png | Bin 0 -> 1778 bytes src/components/board/index.tsx | 7 +- src/constants.ts | 13 +- src/lib/chess.ts | 22 +- src/lib/engine/helpers/moveClassification.ts | 253 ++++-------------- src/pages/index.tsx | 2 +- .../panelBody/analysisTab/moveInfo.tsx | 23 +- .../movesClassificationsRecap/index.tsx | 9 +- .../classificationTab/movesPanel/moveItem.tsx | 2 +- .../analysis/panelBody/graphTab/index.tsx | 7 +- src/types/enums.ts | 19 +- 15 files changed, 120 insertions(+), 237 deletions(-) rename public/icons/{opening.png => book.png} (100%) rename public/icons/{splendid.png => brilliant.png} (100%) rename public/icons/{okay.png => good.png} (100%) rename public/icons/{perfect.png => great.png} (100%) create mode 100644 public/icons/miss.png diff --git a/public/icons/opening.png b/public/icons/book.png similarity index 100% rename from public/icons/opening.png rename to public/icons/book.png diff --git a/public/icons/splendid.png b/public/icons/brilliant.png similarity index 100% rename from public/icons/splendid.png rename to public/icons/brilliant.png diff --git a/public/icons/okay.png b/public/icons/good.png similarity index 100% rename from public/icons/okay.png rename to public/icons/good.png diff --git a/public/icons/perfect.png b/public/icons/great.png similarity index 100% rename from public/icons/perfect.png rename to public/icons/great.png diff --git a/public/icons/miss.png b/public/icons/miss.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ef1ecca3d0768c6134dd1e722a6d3a1225159f GIT binary patch literal 1778 zcmV(yFzP7|>ACmwohu=3(E2q$2s)ow+mj-t+h2+`F^$JzE9id6~Iq z&i|a>`QQK7IU|hGR8?547^y5K06H~QBp-bNv0nyuq52TYR$x;qvWoB?BJToAsJTKj zcw1&?8vt4>dz5QdM0C{JG7_#__GB>y`_T(KNbECUp8$CX-v_l|Em#Fj*AoFFU<`Wj~R3(xyGfxnGr6RgI7 zLBtH(d7nuwU@_v8&qEa7BuTz6GshRyZ4!}Q$R0pd6-1y~6`Jq8OfL8?Q4C2EBjOF( z)J9NIP4GOQz=u|JjQrLYn4VUr@J_qzSS-^VM~?Vx@y}B%>|?ba10Ev(DOeY#Dv+?L z60RjZy<784pbUZGSR3@65U=|-D~lkMpzpdLn)ihF6?ctt9U3Dv6C{+YJB>x<7F zq)<3R*o>eboIo>lV)=YKtc+^L;2g|Cp)g1;qL2>=!>43s?rg>+TCJwry72*;=5L5R zNRrUwrN+SBJFu?|^;Nk1d)WLS*r8zzJP&T)f=9jtAO0wuKLe#Sp05X0wRj$=-Xd>5 z1t0t;fMH@nWO~{X@)Cvo7LvsFg4qaubRu@v%K2zg1Sr!5l`&Y1LvhqsgD{toJ+NQb0I8}93mry5JMMC~+O zy$qWlfTT;48Qjj#!J+4z{aNe!|9lC4v(VP$Iz1vNNQ~F|q#QlD>@*bz&r-yuompO!m1pXHPRk5vU%}si~q*uJJI&+fJNlkL{^KF7F(o)r86+ z*Ox(N-z$@D{M%RD=O8$1ooT%J8oYS{?%&+&(ISR8Q9N%u(s*U2BPdUT@AqynGl4g1aODr< z!=;EQAe1&%bhgD^TU{BRk5$`a-e{8X|_4|9eDIB zy}^8VbNNiE8_=6;Az@s&^>MB{aCbnaedPfeXCPi`yXZ}W$aVKHLWASLb-YGp%@&5G z#Q})!L@3t{*roJgFyN^4l|_Nq&%xP~ZN5=yaZnR+fY>Dia)~I0et8(2s&C&KBYj

ryg$38L0F)cv= zBIjgww&7IbNArZu7Df!~okcAaimtf))1`JjH!6J%f=m)O$I?4+`h>e_=!8^^7{X=? zKbi+HTB~_-^yD&XPf;isR69#!Rag1GcE(l0p4Tp!-nuwAL-d36JXpF0XMO?sT<Y28?Nu6X#>Yj z7uwUx+buHgu~Wo3ghFax{bc(dI}Ly=E+$&7`Z6};aKOc86WK8`xckf(a_FY5-MFg#tJ()SaKvdsJE1pA4NU7)( z_?cXSs&&`By?RQaTwD!&L#gN^CZrY55!H9f%<%=Sl|7_C9Q;3DqIW?45C4YvH*Ej@ U#8fd+!T58 = { - [MoveClassification.Opening]: "#dbac86", - [MoveClassification.Forced]: "#dbac86", - [MoveClassification.Splendid]: "#19d4af", - [MoveClassification.Perfect]: "#3894eb", + // Standard classifications: [MoveClassification.Best]: "#22ac38", [MoveClassification.Excellent]: "#22ac38", - [MoveClassification.Okay]: "#74b038", + [MoveClassification.Good]: "#74b038", [MoveClassification.Inaccuracy]: "#f2be1f", [MoveClassification.Mistake]: "#e69f00", [MoveClassification.Blunder]: "#df5353", + // Special classifications: + [MoveClassification.Brilliant]: "#19d4af", + [MoveClassification.Great]: "#3894eb", + [MoveClassification.Book]: "#dbac86", + [MoveClassification.Forced]: "#dbac86", + [MoveClassification.Miss]: "#ff7769", }; export const DEFAULT_ENGINE: EngineName = EngineName.Stockfish18Lite; diff --git a/src/lib/chess.ts b/src/lib/chess.ts index af3f0f5f..88165682 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -176,19 +176,25 @@ export const uciMoveParams = ( promotion: uciMove.slice(4, 5) || undefined, }); -export const isSimplePieceRecapture = ( +// Also counting pieces of higher value that can be taken with a lower value piece as hanging +// e.g. a rook threatened by a pawn is considered hanging +export const isHangingPieceCapture = ( fen: string, - uciMoves: [string, string] + playedMove: string ): boolean => { - const game = new Chess(fen); - const moves = uciMoves.map((uciMove) => uciMoveParams(uciMove)); + const chess = new Chess(fen); + const move = chess.move(uciMoveParams(playedMove)); + + if (!move.captured) return false; + + const capturedValue = getPieceValue(move.captured); + const capturingValue = getPieceValue(move.piece); - if (moves[0].to !== moves[1].to) return false; + if (capturingValue < capturedValue) return true; - const piece = game.get(moves[0].to); - if (piece) return true; + const isDefended = chess.moves({ verbose: true }).some((m) => m.to === move.to); - return false; + return !isDefended; }; export const getIsPieceSacrifice = ( diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index 551364f8..67703cd5 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -1,11 +1,18 @@ -import { LineEval, PositionEval } from "@/types/eval"; +import { PositionEval } from "@/types/eval"; import { getLineWinPercentage, getPositionWinPercentage, } from "./winPercentage"; import { MoveClassification } from "@/types/enums"; import { openings } from "@/data/openings"; -import { getIsPieceSacrifice, isSimplePieceRecapture } from "@/lib/chess"; +import { getIsPieceSacrifice, isHangingPieceCapture } from "@/lib/chess"; + +// Thresholds for move quality classification (in win percentage points) +// Chess.com seems to adjust these dynamically based on the player's strength... Maybe we could do something similar in the future +const BLUNDER_THRESHOLD = -20; +const MISTAKE_THRESHOLD = -10; +const INACCURACY_THRESHOLD = -5; +const EXCELLENT_THRESHOLD = -2; export const getMovesClassification = ( rawPositions: PositionEval[], @@ -19,19 +26,24 @@ export const getMovesClassification = ( if (index === 0) return rawPosition; const currentFen = fens[index].split(" ")[0]; + + // Book move: known opening position const opening = openings.find((opening) => opening.fen === currentFen); if (opening) { currentOpening = opening.name; return { ...rawPosition, opening: opening.name, - moveClassification: MoveClassification.Opening, + moveClassification: MoveClassification.Book, }; } + const playedMove = uciMoves[index - 1]; const prevPosition = rawPositions[index - 1]; + const alternativeLine = prevPosition.lines.find((line) => line.pv[0] !== playedMove); - if (prevPosition.lines.length === 1) { + // Forced move: only one legal response available + if (!alternativeLine) { return { ...rawPosition, opening: currentOpening, @@ -39,219 +51,72 @@ export const getMovesClassification = ( }; } - const playedMove = uciMoves[index - 1]; - - const lastPositionAlternativeLine: LineEval | undefined = - prevPosition.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; + const lastWinPct = positionsWinPercentage[index - 1]; + const currentWinPct = positionsWinPercentage[index]; + const winPctChange = (currentWinPct - lastWinPct) * (isWhiteMove ? 1 : -1); - if ( - isSplendidMove( - lastPositionWinPercentage, - positionWinPercentage, - isWhiteMove, - playedMove, - bestLinePvToPlay, - fens[index - 1], - lastPositionAlternativeLineWinPercentage - ) - ) { + // Miss (for now only): You could have picked up a hanging piece but failed to do so + if (isHangingPieceCapture(fens[index - 1], alternativeLine.pv[0]) && winPctChange < MISTAKE_THRESHOLD) { return { ...rawPosition, opening: currentOpening, - moveClassification: MoveClassification.Splendid, + moveClassification: MoveClassification.Miss, }; } - const fenTwoMovesAgo = index > 1 ? fens[index - 2] : null; - const uciNextTwoMoves: [string, string] | null = - index > 1 ? [uciMoves[index - 2], uciMoves[index - 1]] : null; - - if ( - isPerfectMove( - lastPositionWinPercentage, - positionWinPercentage, - isWhiteMove, - lastPositionAlternativeLineWinPercentage, - fenTwoMovesAgo, - uciNextTwoMoves - ) - ) { - return { - ...rawPosition, - opening: currentOpening, - moveClassification: MoveClassification.Perfect, - }; - } + const alternativeWinPct = getLineWinPercentage(alternativeLine); + const alternativeWinPctChange = (alternativeWinPct - lastWinPct) * (isWhiteMove ? 1 : -1); if (playedMove === prevPosition.bestMove) { + const alternativesCollapseSignificantly = alternativeWinPctChange < winPctChange - 10; + const hangingPieceCapture = isHangingPieceCapture(fens[index - 1], playedMove); + // Sometimes close to checkmate winPctChange becomes a bad metric, so we also use: + const alternativeIsUselessSacrifice = + getIsPieceSacrifice(fens[index - 1], alternativeLine.pv[0], alternativeLine.pv.slice(1)) && + alternativeWinPctChange < BLUNDER_THRESHOLD; + + // Best: The move played is the engine's top choice, but not necessarily a brilliant move + if (hangingPieceCapture || !alternativesCollapseSignificantly || alternativeIsUselessSacrifice) { + return { + ...rawPosition, + opening: currentOpening, + moveClassification: MoveClassification.Best, + }; + } + + // Brilliant: The move played involves a piece sacrifice and is the only good move (alternatives collapse significantly) + if (getIsPieceSacrifice(fens[index - 1], playedMove, rawPosition.lines[0].pv)) { + return { + ...rawPosition, + opening: currentOpening, + moveClassification: MoveClassification.Brilliant, + }; + } + + // Great: The move played is the only good move (alternatives collapse significantly) return { ...rawPosition, opening: currentOpening, - moveClassification: MoveClassification.Best, + moveClassification: MoveClassification.Great, }; } - - const moveClassification = getMoveBasicClassification( - lastPositionWinPercentage, - positionWinPercentage, - isWhiteMove - ); - + + // Standard classifications return { ...rawPosition, opening: currentOpening, - moveClassification, + moveClassification: classifyByWinPctChange(winPctChange), }; }); return positions; }; -const getMoveBasicClassification = ( - lastPositionWinPercentage: number, - positionWinPercentage: number, - isWhiteMove: boolean -): MoveClassification => { - const winPercentageDiff = - (positionWinPercentage - lastPositionWinPercentage) * - (isWhiteMove ? 1 : -1); - - if (winPercentageDiff < -20) return MoveClassification.Blunder; - if (winPercentageDiff < -10) return MoveClassification.Mistake; - if (winPercentageDiff < -5) return MoveClassification.Inaccuracy; - if (winPercentageDiff < -2) return MoveClassification.Okay; +const classifyByWinPctChange = (winPctChange: number): MoveClassification => { + if (winPctChange < BLUNDER_THRESHOLD) return MoveClassification.Blunder; + if (winPctChange < MISTAKE_THRESHOLD) return MoveClassification.Mistake; + if (winPctChange < INACCURACY_THRESHOLD) return MoveClassification.Inaccuracy; + if (winPctChange < EXCELLENT_THRESHOLD) return MoveClassification.Good; return MoveClassification.Excellent; }; - -const isSplendidMove = ( - lastPositionWinPercentage: number, - positionWinPercentage: number, - isWhiteMove: boolean, - playedMove: string, - bestLinePvToPlay: string[], - fen: string, - lastPositionAlternativeLineWinPercentage: number | undefined -): boolean => { - if (!lastPositionAlternativeLineWinPercentage) return false; - - const winPercentageDiff = - (positionWinPercentage - lastPositionWinPercentage) * - (isWhiteMove ? 1 : -1); - if (winPercentageDiff < -2) return false; - - const isPieceSacrifice = getIsPieceSacrifice( - fen, - playedMove, - bestLinePvToPlay - ); - if (!isPieceSacrifice) return false; - - if ( - isLosingOrAlternateCompletelyWinning( - positionWinPercentage, - lastPositionAlternativeLineWinPercentage, - isWhiteMove - ) - ) { - return false; - } - - return true; -}; - -const isLosingOrAlternateCompletelyWinning = ( - positionWinPercentage: number, - lastPositionAlternativeLineWinPercentage: number, - isWhiteMove: boolean -): boolean => { - const isLosing = isWhiteMove - ? positionWinPercentage < 50 - : positionWinPercentage > 50; - const isAlternateCompletelyWinning = isWhiteMove - ? lastPositionAlternativeLineWinPercentage > 97 - : lastPositionAlternativeLineWinPercentage < 3; - - return isLosing || isAlternateCompletelyWinning; -}; - -const isPerfectMove = ( - lastPositionWinPercentage: number, - positionWinPercentage: number, - isWhiteMove: boolean, - lastPositionAlternativeLineWinPercentage: number | undefined, - fenTwoMovesAgo: string | null, - uciMoves: [string, string] | null -): boolean => { - if (!lastPositionAlternativeLineWinPercentage) return false; - - const winPercentageDiff = - (positionWinPercentage - lastPositionWinPercentage) * - (isWhiteMove ? 1 : -1); - if (winPercentageDiff < -2) return false; - - if ( - fenTwoMovesAgo && - uciMoves && - isSimplePieceRecapture(fenTwoMovesAgo, uciMoves) - ) - return false; - - if ( - isLosingOrAlternateCompletelyWinning( - positionWinPercentage, - lastPositionAlternativeLineWinPercentage, - isWhiteMove - ) - ) { - 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 > 10; -}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0b1dff5b..9c1abb09 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -59,7 +59,7 @@ export default function GameAnalysis() { style={{ maxWidth: "1200px", }} - rowGap={2} + rowGap={1.5} height={{ xs: tab === 1 ? "40rem" : "auto", lg: "calc(95vh - 60px)" }} display="flex" flexDirection="column" diff --git a/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx index 4e84e516..d7f748a5 100644 --- a/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx +++ b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx @@ -44,11 +44,11 @@ export default function MoveInfo() { const moveClassification = position.eval?.moveClassification; const showBestMoveLabel = + moveClassification !== MoveClassification.Brilliant && + moveClassification !== MoveClassification.Great && moveClassification !== MoveClassification.Best && - moveClassification !== MoveClassification.Opening && - moveClassification !== MoveClassification.Forced && - moveClassification !== MoveClassification.Splendid && - moveClassification !== MoveClassification.Perfect; + moveClassification !== MoveClassification.Book && + moveClassification !== MoveClassification.Forced; return ( = { - [MoveClassification.Opening]: "an opening move", - [MoveClassification.Forced]: "forced", - [MoveClassification.Splendid]: "splendid !!", - [MoveClassification.Perfect]: "the only good move !", + // Standard classifications: [MoveClassification.Best]: "the best move", - [MoveClassification.Excellent]: "excellent", - [MoveClassification.Okay]: "an okay move", + [MoveClassification.Excellent]: "an excellent move", + [MoveClassification.Good]: "a good move", [MoveClassification.Inaccuracy]: "an inaccuracy", [MoveClassification.Mistake]: "a mistake", [MoveClassification.Blunder]: "a blunder", + // Special classifications: + [MoveClassification.Brilliant]: "a brilliant move", + [MoveClassification.Great]: "a great move", + [MoveClassification.Book]: "a book move", + [MoveClassification.Forced]: "a forced move", + [MoveClassification.Miss]: "a miss", }; diff --git a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx index eddb5527..8fb3d32f 100644 --- a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx @@ -51,13 +51,14 @@ export default function MovesClassificationsRecap() { } export const sortedMoveClassfications = [ - MoveClassification.Splendid, - MoveClassification.Perfect, + MoveClassification.Brilliant, + MoveClassification.Great, MoveClassification.Best, MoveClassification.Excellent, - MoveClassification.Okay, - MoveClassification.Opening, + MoveClassification.Good, + MoveClassification.Book, MoveClassification.Inaccuracy, MoveClassification.Mistake, + MoveClassification.Miss, MoveClassification.Blunder, ]; diff --git a/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx index c4898bb4..0fd8c6a7 100644 --- a/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx @@ -105,7 +105,7 @@ const getMoveColor = (moveClassification?: MoveClassification) => { }; const moveClassificationsToIgnore: MoveClassification[] = [ - MoveClassification.Okay, + MoveClassification.Good, MoveClassification.Excellent, MoveClassification.Forced, ]; diff --git a/src/sections/analysis/panelBody/graphTab/index.tsx b/src/sections/analysis/panelBody/graphTab/index.tsx index bac5f8cf..63ce6a23 100644 --- a/src/sections/analysis/panelBody/graphTab/index.tsx +++ b/src/sections/analysis/panelBody/graphTab/index.tsx @@ -65,10 +65,11 @@ export default function GraphTab(props: GridProps) { if ( [ - MoveClassification.Splendid, - MoveClassification.Perfect, - MoveClassification.Blunder, + MoveClassification.Brilliant, + MoveClassification.Great, MoveClassification.Mistake, + MoveClassification.Blunder, + MoveClassification.Miss, ].includes(moveClass) || (moveClass === MoveClassification.Best && bestDotIndices.has(payload.moveNb)) diff --git a/src/types/enums.ts b/src/types/enums.ts index 3f9f76d9..b2fe7304 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -19,16 +19,19 @@ export enum EngineName { } export enum MoveClassification { - Blunder = "blunder", - Mistake = "mistake", - Inaccuracy = "inaccuracy", - Okay = "okay", - Excellent = "excellent", + // Standard classifications: Best = "best", + Excellent = "excellent", + Good = "good", + Inaccuracy = "inaccuracy", + Mistake = "mistake", + Blunder = "blunder", + // Special classifications: + Brilliant = "brilliant", + Great = "great", + Book = "book", Forced = "forced", - Opening = "opening", - Perfect = "perfect", - Splendid = "splendid", + Miss = "miss", } export enum Color { From 55adc46038f4d500bef9b110047adf261a0f9208 Mon Sep 17 00:00:00 2001 From: Arne Malaka Date: Sat, 9 May 2026 15:15:42 +0200 Subject: [PATCH 2/3] Rollback classification name changes and "Miss"-classification --- public/icons/miss.png | Bin 1778 -> 0 bytes public/icons/{good.png => okay.png} | Bin public/icons/{book.png => opening.png} | Bin public/icons/{great.png => perfect.png} | Bin public/icons/{brilliant.png => splendid.png} | Bin src/components/board/index.tsx | 6 +++--- src/constants.ts | 9 ++++---- src/lib/engine/helpers/moveClassification.ts | 20 +++++------------- .../panelBody/analysisTab/moveInfo.tsx | 19 ++++++++--------- .../movesClassificationsRecap/index.tsx | 9 ++++---- .../classificationTab/movesPanel/moveItem.tsx | 2 +- .../analysis/panelBody/graphTab/index.tsx | 5 ++--- src/types/enums.ts | 9 ++++---- 13 files changed, 32 insertions(+), 47 deletions(-) delete mode 100644 public/icons/miss.png rename public/icons/{good.png => okay.png} (100%) rename public/icons/{book.png => opening.png} (100%) rename public/icons/{great.png => perfect.png} (100%) rename public/icons/{brilliant.png => splendid.png} (100%) diff --git a/public/icons/miss.png b/public/icons/miss.png deleted file mode 100644 index f5ef1ecca3d0768c6134dd1e722a6d3a1225159f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1778 zcmV(yFzP7|>ACmwohu=3(E2q$2s)ow+mj-t+h2+`F^$JzE9id6~Iq z&i|a>`QQK7IU|hGR8?547^y5K06H~QBp-bNv0nyuq52TYR$x;qvWoB?BJToAsJTKj zcw1&?8vt4>dz5QdM0C{JG7_#__GB>y`_T(KNbECUp8$CX-v_l|Em#Fj*AoFFU<`Wj~R3(xyGfxnGr6RgI7 zLBtH(d7nuwU@_v8&qEa7BuTz6GshRyZ4!}Q$R0pd6-1y~6`Jq8OfL8?Q4C2EBjOF( z)J9NIP4GOQz=u|JjQrLYn4VUr@J_qzSS-^VM~?Vx@y}B%>|?ba10Ev(DOeY#Dv+?L z60RjZy<784pbUZGSR3@65U=|-D~lkMpzpdLn)ihF6?ctt9U3Dv6C{+YJB>x<7F zq)<3R*o>eboIo>lV)=YKtc+^L;2g|Cp)g1;qL2>=!>43s?rg>+TCJwry72*;=5L5R zNRrUwrN+SBJFu?|^;Nk1d)WLS*r8zzJP&T)f=9jtAO0wuKLe#Sp05X0wRj$=-Xd>5 z1t0t;fMH@nWO~{X@)Cvo7LvsFg4qaubRu@v%K2zg1Sr!5l`&Y1LvhqsgD{toJ+NQb0I8}93mry5JMMC~+O zy$qWlfTT;48Qjj#!J+4z{aNe!|9lC4v(VP$Iz1vNNQ~F|q#QlD>@*bz&r-yuompO!m1pXHPRk5vU%}si~q*uJJI&+fJNlkL{^KF7F(o)r86+ z*Ox(N-z$@D{M%RD=O8$1ooT%J8oYS{?%&+&(ISR8Q9N%u(s*U2BPdUT@AqynGl4g1aODr< z!=;EQAe1&%bhgD^TU{BRk5$`a-e{8X|_4|9eDIB zy}^8VbNNiE8_=6;Az@s&^>MB{aCbnaedPfeXCPi`yXZ}W$aVKHLWASLb-YGp%@&5G z#Q})!L@3t{*roJgFyN^4l|_Nq&%xP~ZN5=yaZnR+fY>Dia)~I0et8(2s&C&KBYj

ryg$38L0F)cv= zBIjgww&7IbNArZu7Df!~okcAaimtf))1`JjH!6J%f=m)O$I?4+`h>e_=!8^^7{X=? zKbi+HTB~_-^yD&XPf;isR69#!Rag1GcE(l0p4Tp!-nuwAL-d36JXpF0XMO?sT<Y28?Nu6X#>Yj z7uwUx+buHgu~Wo3ghFax{bc(dI}Ly=E+$&7`Z6};aKOc86WK8`xckf(a_FY5-MFg#tJ()SaKvdsJE1pA4NU7)( z_?cXSs&&`By?RQaTwD!&L#gN^CZrY55!H9f%<%=Sl|7_C9Q;3DqIW?45C4YvH*Ej@ U#8fd+!T58 = { // Standard classifications: [MoveClassification.Best]: "#22ac38", [MoveClassification.Excellent]: "#22ac38", - [MoveClassification.Good]: "#74b038", + [MoveClassification.Okay]: "#74b038", [MoveClassification.Inaccuracy]: "#f2be1f", [MoveClassification.Mistake]: "#e69f00", [MoveClassification.Blunder]: "#df5353", // Special classifications: - [MoveClassification.Brilliant]: "#19d4af", - [MoveClassification.Great]: "#3894eb", - [MoveClassification.Book]: "#dbac86", + [MoveClassification.Splendid]: "#19d4af", + [MoveClassification.Perfect]: "#3894eb", + [MoveClassification.Opening]: "#dbac86", [MoveClassification.Forced]: "#dbac86", - [MoveClassification.Miss]: "#ff7769", }; export const DEFAULT_ENGINE: EngineName = EngineName.Stockfish18Lite; diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index 67703cd5..2034d542 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -34,7 +34,7 @@ export const getMovesClassification = ( return { ...rawPosition, opening: opening.name, - moveClassification: MoveClassification.Book, + moveClassification: MoveClassification.Opening, }; } @@ -55,16 +55,6 @@ export const getMovesClassification = ( const lastWinPct = positionsWinPercentage[index - 1]; const currentWinPct = positionsWinPercentage[index]; const winPctChange = (currentWinPct - lastWinPct) * (isWhiteMove ? 1 : -1); - - // Miss (for now only): You could have picked up a hanging piece but failed to do so - if (isHangingPieceCapture(fens[index - 1], alternativeLine.pv[0]) && winPctChange < MISTAKE_THRESHOLD) { - return { - ...rawPosition, - opening: currentOpening, - moveClassification: MoveClassification.Miss, - }; - } - const alternativeWinPct = getLineWinPercentage(alternativeLine); const alternativeWinPctChange = (alternativeWinPct - lastWinPct) * (isWhiteMove ? 1 : -1); @@ -90,7 +80,7 @@ export const getMovesClassification = ( return { ...rawPosition, opening: currentOpening, - moveClassification: MoveClassification.Brilliant, + moveClassification: MoveClassification.Splendid, }; } @@ -98,10 +88,10 @@ export const getMovesClassification = ( return { ...rawPosition, opening: currentOpening, - moveClassification: MoveClassification.Great, + moveClassification: MoveClassification.Perfect, }; } - + // Standard classifications return { ...rawPosition, @@ -117,6 +107,6 @@ const classifyByWinPctChange = (winPctChange: number): MoveClassification => { if (winPctChange < BLUNDER_THRESHOLD) return MoveClassification.Blunder; if (winPctChange < MISTAKE_THRESHOLD) return MoveClassification.Mistake; if (winPctChange < INACCURACY_THRESHOLD) return MoveClassification.Inaccuracy; - if (winPctChange < EXCELLENT_THRESHOLD) return MoveClassification.Good; + if (winPctChange < EXCELLENT_THRESHOLD) return MoveClassification.Okay; return MoveClassification.Excellent; }; diff --git a/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx index d7f748a5..fb695e25 100644 --- a/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx +++ b/src/sections/analysis/panelBody/analysisTab/moveInfo.tsx @@ -44,10 +44,10 @@ export default function MoveInfo() { const moveClassification = position.eval?.moveClassification; const showBestMoveLabel = - moveClassification !== MoveClassification.Brilliant && - moveClassification !== MoveClassification.Great && + moveClassification !== MoveClassification.Splendid && + moveClassification !== MoveClassification.Perfect && moveClassification !== MoveClassification.Best && - moveClassification !== MoveClassification.Book && + moveClassification !== MoveClassification.Opening && moveClassification !== MoveClassification.Forced; return ( @@ -114,15 +114,14 @@ export default function MoveInfo() { const moveClassificationLabels: Record = { // Standard classifications: [MoveClassification.Best]: "the best move", - [MoveClassification.Excellent]: "an excellent move", - [MoveClassification.Good]: "a good move", + [MoveClassification.Excellent]: "excellent", + [MoveClassification.Okay]: "an okay move", [MoveClassification.Inaccuracy]: "an inaccuracy", [MoveClassification.Mistake]: "a mistake", [MoveClassification.Blunder]: "a blunder", // Special classifications: - [MoveClassification.Brilliant]: "a brilliant move", - [MoveClassification.Great]: "a great move", - [MoveClassification.Book]: "a book move", - [MoveClassification.Forced]: "a forced move", - [MoveClassification.Miss]: "a miss", + [MoveClassification.Splendid]: "splendid !!", + [MoveClassification.Perfect]: "the only good move !", + [MoveClassification.Opening]: "an opening move", + [MoveClassification.Forced]: "forced", }; diff --git a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx index 8fb3d32f..eddb5527 100644 --- a/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesClassificationsRecap/index.tsx @@ -51,14 +51,13 @@ export default function MovesClassificationsRecap() { } export const sortedMoveClassfications = [ - MoveClassification.Brilliant, - MoveClassification.Great, + MoveClassification.Splendid, + MoveClassification.Perfect, MoveClassification.Best, MoveClassification.Excellent, - MoveClassification.Good, - MoveClassification.Book, + MoveClassification.Okay, + MoveClassification.Opening, MoveClassification.Inaccuracy, MoveClassification.Mistake, - MoveClassification.Miss, MoveClassification.Blunder, ]; diff --git a/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx index 0fd8c6a7..c4898bb4 100644 --- a/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx +++ b/src/sections/analysis/panelBody/classificationTab/movesPanel/moveItem.tsx @@ -105,7 +105,7 @@ const getMoveColor = (moveClassification?: MoveClassification) => { }; const moveClassificationsToIgnore: MoveClassification[] = [ - MoveClassification.Good, + MoveClassification.Okay, MoveClassification.Excellent, MoveClassification.Forced, ]; diff --git a/src/sections/analysis/panelBody/graphTab/index.tsx b/src/sections/analysis/panelBody/graphTab/index.tsx index 63ce6a23..ea653a10 100644 --- a/src/sections/analysis/panelBody/graphTab/index.tsx +++ b/src/sections/analysis/panelBody/graphTab/index.tsx @@ -65,11 +65,10 @@ export default function GraphTab(props: GridProps) { if ( [ - MoveClassification.Brilliant, - MoveClassification.Great, + MoveClassification.Splendid, + MoveClassification.Perfect, MoveClassification.Mistake, MoveClassification.Blunder, - MoveClassification.Miss, ].includes(moveClass) || (moveClass === MoveClassification.Best && bestDotIndices.has(payload.moveNb)) diff --git a/src/types/enums.ts b/src/types/enums.ts index b2fe7304..fae6ae20 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -22,16 +22,15 @@ export enum MoveClassification { // Standard classifications: Best = "best", Excellent = "excellent", - Good = "good", + Okay = "okay", Inaccuracy = "inaccuracy", Mistake = "mistake", Blunder = "blunder", // Special classifications: - Brilliant = "brilliant", - Great = "great", - Book = "book", + Splendid = "splendid", + Perfect = "perfect", + Opening = "opening", Forced = "forced", - Miss = "miss", } export enum Color { From 6945d9e5717396d9df8cd4fc0dc1d47bafee0b05 Mon Sep 17 00:00:00 2001 From: Arne Malaka Date: Sat, 9 May 2026 15:18:04 +0200 Subject: [PATCH 3/3] Integrated moveClassification turn bug fix --- src/lib/engine/helpers/moveClassification.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index 2034d542..54547357 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -51,7 +51,8 @@ export const getMovesClassification = ( }; } - const isWhiteMove = index % 2 === 1; + const sideToMove = fens[index - 1].split(" ")[1]; + const isWhiteMove = sideToMove === "w"; const lastWinPct = positionsWinPercentage[index - 1]; const currentWinPct = positionsWinPercentage[index]; const winPctChange = (currentWinPct - lastWinPct) * (isWhiteMove ? 1 : -1);