From b1612b1c32f341d721b1ce48fbbc927819ee5e38 Mon Sep 17 00:00:00 2001 From: Ashton Anderson Date: Fri, 27 Mar 2026 20:35:35 -0400 Subject: [PATCH] Fix stockfish move color ordering --- .../useAnalysisController.ts | 6 +- .../useMoveRecommendations.ts | 15 +- src/hooks/useAnalysisController/utils.ts | 129 +++++++++++------- 3 files changed, 92 insertions(+), 58 deletions(-) diff --git a/src/hooks/useAnalysisController/useAnalysisController.ts b/src/hooks/useAnalysisController/useAnalysisController.ts index ce594f0b..f81c585e 100644 --- a/src/hooks/useAnalysisController/useAnalysisController.ts +++ b/src/hooks/useAnalysisController/useAnalysisController.ts @@ -179,12 +179,12 @@ export const useAnalysisController = ( } if (moveEvaluation?.stockfish) { - const bestMove = Object.entries(moveEvaluation.stockfish.cp_vec)[0] + const bestMove = moveEvaluation.stockfish.model_move if (bestMove) { arrows.push({ brush: 'blue', - orig: bestMove[0].slice(0, 2) as Key, - dest: bestMove[0].slice(2, 4) as Key, + orig: bestMove.slice(0, 2) as Key, + dest: bestMove.slice(2, 4) as Key, modifiers: { lineWidth: 8 }, } as DrawShape) } diff --git a/src/hooks/useAnalysisController/useMoveRecommendations.ts b/src/hooks/useAnalysisController/useMoveRecommendations.ts index 7299fc86..e38ae1c3 100644 --- a/src/hooks/useAnalysisController/useMoveRecommendations.ts +++ b/src/hooks/useAnalysisController/useMoveRecommendations.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react' import { Chess } from 'chess.ts' import { MAIA_MODELS } from 'src/constants/common' import { GameNode, MaiaEvaluation, StockfishEvaluation } from 'src/types' +import { sortStockfishMoves } from './utils' export const useMoveRecommendations = ( currentNode: GameNode | null, @@ -23,6 +24,7 @@ export const useMoveRecommendations = ( cp: number winrate?: number winrate_loss?: number + cp_relative?: number }[] isBlackTurn?: boolean } = { @@ -44,10 +46,14 @@ export const useMoveRecommendations = ( const cp_relative_vec = moveEvaluation.stockfish.cp_relative_vec || {} const winrate_vec = moveEvaluation.stockfish.winrate_vec || {} const winrate_loss_vec = moveEvaluation.stockfish.winrate_loss_vec || {} + const sortedMoves = sortStockfishMoves( + moveEvaluation.stockfish, + Object.keys(cp_vec), + ) - const stockfish = Object.entries(cp_vec).map(([move, cp]) => ({ + const stockfish = sortedMoves.map((move) => ({ move, - cp, + cp: cp_vec[move] || 0, winrate: winrate_vec[move] || 0, winrate_loss: winrate_loss_vec[move] || 0, cp_relative: cp_relative_vec[move] || 0, @@ -80,7 +86,10 @@ export const useMoveRecommendations = ( // Get top 3 Stockfish moves if (stockfish) { - for (const move of Object.keys(stockfish.cp_vec).slice(0, 3)) { + for (const move of sortStockfishMoves( + stockfish, + Object.keys(stockfish.cp_vec), + ).slice(0, 3)) { if (candidates.find((c) => c[0] === move)) continue candidates.push([move, move]) } diff --git a/src/hooks/useAnalysisController/utils.ts b/src/hooks/useAnalysisController/utils.ts index e8917a25..9f0dd04c 100644 --- a/src/hooks/useAnalysisController/utils.ts +++ b/src/hooks/useAnalysisController/utils.ts @@ -14,6 +14,51 @@ type ColorSanMappingResult = { } } +const getStockfishMoveOrderingScore = ( + stockfish: StockfishEvaluation, + move: string, +): number => { + const winrateLoss = stockfish.winrate_loss_vec?.[move] + if (winrateLoss !== undefined) { + return winrateLoss + } + + const relativeEval = stockfish.cp_relative_vec?.[move] + if (relativeEval !== undefined) { + return relativeEval + } + + const cp = stockfish.cp_vec?.[move] + if (cp !== undefined) { + return cp + } + + return Number.NEGATIVE_INFINITY +} + +export const sortStockfishMoves = ( + stockfish: StockfishEvaluation, + moves: string[], +): string[] => + [...moves].sort((a, b) => { + const scoreDiff = + getStockfishMoveOrderingScore(stockfish, b) - + getStockfishMoveOrderingScore(stockfish, a) + + if (scoreDiff !== 0) { + return scoreDiff + } + + const cpDiff = + (stockfish.cp_vec?.[b] ?? Number.NEGATIVE_INFINITY) - + (stockfish.cp_vec?.[a] ?? Number.NEGATIVE_INFINITY) + if (cpDiff !== 0) { + return cpDiff + } + + return a.localeCompare(b) + }) + // Unified function to calculate color for a single move export const calculateMoveColor = ( stockfish: StockfishEvaluation | undefined, @@ -56,6 +101,7 @@ export const generateColorSanMapping = ( const chess = new Chess(fen) const moves = chess.moves({ verbose: true }) + const moveKeys = moves.map((m) => `${m.from}${m.to}${m.promotion || ''}`) moves.forEach((m) => { const moveKey = `${m.from}${m.to}${m.promotion || ''}` mapping[moveKey] = { @@ -81,51 +127,37 @@ export const generateColorSanMapping = ( stockfish.winrate_loss_vec && Object.keys(stockfish.winrate_loss_vec).length > 0 ) { - const goodMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter((move) => { + const goodMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => { const winrateLoss = stockfish.winrate_loss_vec?.[move] return ( winrateLoss !== undefined && winrateLoss >= -MOVE_CLASSIFICATION_THRESHOLDS.INACCURACY_THRESHOLD ) - }) - .sort((a, b) => { - const aLoss = stockfish.winrate_loss_vec?.[a] || 0 - const bLoss = stockfish.winrate_loss_vec?.[b] || 0 - return bLoss - aLoss - }) - - const okMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter((move) => { + }), + ) + const okMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => { const winrateLoss = stockfish.winrate_loss_vec?.[move] return ( winrateLoss !== undefined && winrateLoss >= -MOVE_CLASSIFICATION_THRESHOLDS.BLUNDER_THRESHOLD && winrateLoss < -MOVE_CLASSIFICATION_THRESHOLDS.INACCURACY_THRESHOLD ) - }) - .sort((a, b) => { - const aLoss = stockfish.winrate_loss_vec?.[a] || 0 - const bLoss = stockfish.winrate_loss_vec?.[b] || 0 - return bLoss - aLoss - }) - - const blunderMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter((move) => { + }), + ) + const blunderMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => { const winrateLoss = stockfish.winrate_loss_vec?.[move] return ( winrateLoss !== undefined && winrateLoss < -MOVE_CLASSIFICATION_THRESHOLDS.BLUNDER_THRESHOLD ) - }) - .sort((a, b) => { - const aLoss = stockfish.winrate_loss_vec?.[a] || 0 - const bLoss = stockfish.winrate_loss_vec?.[b] || 0 - return bLoss - aLoss - }) + }), + ) goodMoves.forEach((move, i) => { mapping[move].color = COLORS.good[Math.min(i, COLORS.good.length - 1)] @@ -140,30 +172,23 @@ export const generateColorSanMapping = ( COLORS.blunder[Math.min(i, COLORS.blunder.length - 1)] }) } else { - const goodMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter((move) => stockfish.cp_relative_vec[move] >= -50) - .sort( - (a, b) => stockfish.cp_relative_vec[b] - stockfish.cp_relative_vec[a], - ) - - const okMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter( - (move) => + const goodMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => stockfish.cp_relative_vec[move] >= -50), + ) + const okMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => { + return ( stockfish.cp_relative_vec[move] >= -150 && - stockfish.cp_relative_vec[move] < -50, - ) - .sort( - (a, b) => stockfish.cp_relative_vec[b] - stockfish.cp_relative_vec[a], - ) - - const blunderMoves = moves - .map((m) => `${m.from}${m.to}${m.promotion || ''}`) - .filter((move) => stockfish.cp_relative_vec[move] < -150) - .sort( - (a, b) => stockfish.cp_relative_vec[b] - stockfish.cp_relative_vec[a], - ) + stockfish.cp_relative_vec[move] < -50 + ) + }), + ) + const blunderMoves = sortStockfishMoves( + stockfish, + moveKeys.filter((move) => stockfish.cp_relative_vec[move] < -150), + ) goodMoves.forEach((move, i) => { mapping[move].color = COLORS.good[Math.min(i, COLORS.good.length - 1)]