|
6 | 6 | StockfishEvaluation, |
7 | 7 | } from 'src/types' |
8 | 8 | import { MAIA_MODELS } from './constants' |
| 9 | +import { describePosition } from './useDescriptionGenerator' |
9 | 10 |
|
10 | 11 | type ColorSanMapping = { |
11 | 12 | [move: string]: { |
@@ -33,205 +34,29 @@ export const useBoardDescription = ( |
33 | 34 | return '' |
34 | 35 | } |
35 | 36 |
|
36 | | - const isBlackTurn = currentNode.turn === 'b' |
37 | | - const playerColor = isBlackTurn ? 'Black' : 'White' |
38 | | - const opponent = isBlackTurn ? 'White' : 'Black' |
39 | | - const stockfish = moveEvaluation.stockfish |
40 | | - const maia = moveEvaluation.maia |
41 | | - const topMaiaMove = Object.entries(maia.policy).sort( |
42 | | - (a, b) => b[1] - a[1], |
43 | | - )[0] |
44 | | - |
45 | | - const topStockfishMoves = Object.entries(stockfish.cp_vec) |
46 | | - .sort((a, b) => (isBlackTurn ? a[1] - b[1] : b[1] - a[1])) |
47 | | - .slice(0, 3) |
48 | | - |
49 | | - const cp = stockfish.model_optimal_cp |
50 | | - const absCP = Math.abs(cp) |
51 | | - const cpAdvantage = cp > 0 ? 'White' : cp < 0 ? 'Black' : 'Neither player' |
52 | | - const topStockfishMove = topStockfishMoves[0] |
53 | | - |
54 | | - // Calculate winrate for more nuanced description (using centipawn to approximate winrate) |
55 | | - // Formula approximates winrate from CP value: 1/(1+10^(-cp/400)) |
56 | | - const rawWinrate = 1 / (1 + Math.pow(10, -cp / 400)) |
57 | | - const winrate = Math.max(0.01, Math.min(0.99, rawWinrate)) // Clamp between 1% and 99% |
58 | | - const toMoveWinrate = isBlackTurn ? 1 - winrate : winrate |
59 | | - const toMoveAdvantage = toMoveWinrate > 0.5 |
60 | | - |
61 | | - // Check if top Maia move matches top Stockfish move |
62 | | - const maiaMatchesStockfish = topMaiaMove[0] === topStockfishMove[0] |
63 | | - |
64 | | - // Get top few Maia moves and their cumulative probability |
65 | | - const top3MaiaMoves = Object.entries(maia.policy) |
66 | | - .sort((a, b) => b[1] - a[1]) |
67 | | - .slice(0, 3) |
68 | | - const top3MaiaProbability = |
69 | | - top3MaiaMoves.reduce((sum, [_, prob]) => sum + prob, 0) * 100 |
70 | | - |
71 | | - // Get second best moves to analyze move clarity |
72 | | - const secondBestMaiaMove = top3MaiaMoves[1] |
73 | | - const secondBestMaiaProbability = secondBestMaiaMove |
74 | | - ? secondBestMaiaMove[1] * 100 |
75 | | - : 0 |
76 | | - |
77 | | - // Calculate spread between first and second-best moves |
78 | | - const probabilitySpread = topMaiaMove[1] * 100 - secondBestMaiaProbability |
79 | | - |
80 | | - // Get move classifications |
81 | | - const blunderProbability = blunderMeter.blunderMoves.probability |
82 | | - const okProbability = blunderMeter.okMoves.probability |
83 | | - const goodProbability = blunderMeter.goodMoves.probability |
84 | | - |
85 | | - // Check for patterns in stockfish evaluation |
86 | | - const stockfishTop3Spread = |
87 | | - topStockfishMoves.length > 2 |
88 | | - ? Math.abs(topStockfishMoves[0][1] - topStockfishMoves[2][1]) |
89 | | - : 0 |
90 | | - |
91 | | - // Get move spreads to detect sharp positions |
92 | | - const moveCpSpread = Object.values(stockfish.cp_relative_vec).reduce( |
93 | | - (maxDiff, cp, _, arr) => { |
94 | | - const min = Math.min(...arr) |
95 | | - const max = Math.max(...arr) |
96 | | - return Math.max(maxDiff, max - min) |
97 | | - }, |
98 | | - 0, |
99 | | - ) |
100 | | - |
101 | | - // Calculate position complexity based on distribution of move quality |
102 | | - const isPositionComplicated = |
103 | | - (blunderProbability > 30 && okProbability > 20 && goodProbability < 50) || |
104 | | - moveCpSpread > 300 || |
105 | | - stockfishTop3Spread > 100 |
106 | | - |
107 | | - // Check for tactical position |
108 | | - const isTacticalPosition = moveCpSpread > 500 || stockfishTop3Spread > 150 |
109 | | - |
110 | | - // Check if there's a clear best move |
111 | | - const topMaiaProbability = topMaiaMove[1] * 100 |
112 | | - const isClearBestMove = topMaiaProbability > 70 || probabilitySpread > 40 |
113 | | - |
114 | | - // Check if there are multiple equally good moves |
115 | | - const hasMultipleGoodMoves = |
116 | | - top3MaiaProbability > 75 && topMaiaProbability < 50 |
117 | | - |
118 | | - // Calculate agreement between Maia rating levels |
119 | | - const maiaModelsAgree = Object.entries(currentNode.analysis.maia || {}) |
120 | | - .filter(([key]) => MAIA_MODELS.includes(key)) |
121 | | - .every(([_, evaluation]) => { |
122 | | - const topMove = Object.entries(evaluation.policy).sort( |
123 | | - (a, b) => b[1] - a[1], |
124 | | - )[0] |
125 | | - return topMove && topMove[0] === topMaiaMove[0] |
126 | | - }) |
127 | | - |
128 | | - // Check if evaluation is decisive |
129 | | - const isDecisiveAdvantage = absCP > 300 |
130 | | - const isOverwhelming = absCP > 800 |
131 | | - |
132 | | - // Check for high blunder probability |
133 | | - const isBlunderProne = blunderProbability > 50 |
134 | | - const isVeryBlunderProne = blunderProbability > 70 |
135 | | - |
136 | | - // Check if there's forced play |
137 | | - const isForcedPlay = topMaiaProbability > 85 && maiaMatchesStockfish |
138 | | - |
139 | | - // Check if position is balanced but with complexity |
140 | | - const isBalancedButComplex = absCP < 50 && isPositionComplicated |
141 | | - |
142 | | - // Generate descriptions |
143 | | - let evaluation = '' |
144 | | - let suggestion = '' |
145 | | - |
146 | | - // Evaluation description that considers whose turn it is |
147 | | - if (isOverwhelming) { |
148 | | - if (cpAdvantage === playerColor) { |
149 | | - evaluation = `${playerColor} has a completely winning position with a ${Math.round(toMoveWinrate * 100)}% win probability.` |
150 | | - } else { |
151 | | - evaluation = `${playerColor} faces a nearly lost position with only a ${Math.round(toMoveWinrate * 100)}% win probability.` |
152 | | - } |
153 | | - } else if (cp === 0) { |
154 | | - evaluation = isBalancedButComplex |
155 | | - ? 'The position is balanced but filled with complications.' |
156 | | - : 'The position is completely equal.' |
157 | | - } else if (absCP < 30) { |
158 | | - evaluation = `The evaluation is almost perfectly balanced with only the slightest edge ${cpAdvantage === playerColor ? 'for' : 'against'} ${playerColor}.` |
159 | | - } else if (absCP < 80) { |
160 | | - if (cpAdvantage === playerColor) { |
161 | | - evaluation = `${playerColor} has a slight but tangible advantage with a win probability of ${Math.round(toMoveWinrate * 100)}%.` |
162 | | - } else { |
163 | | - evaluation = `${playerColor} faces a slight disadvantage with a win probability of ${Math.round(toMoveWinrate * 100)}%.` |
| 37 | + const fen = currentNode.fen |
| 38 | + const whiteToMove = currentNode.turn === 'w' |
| 39 | + |
| 40 | + const stockfishEvals = moveEvaluation.stockfish.cp_vec |
| 41 | + const maiaEvals: Record<string, number[]> = {} |
| 42 | + const allMaiaAnalysis = currentNode.analysis.maia || {} |
| 43 | + |
| 44 | + Object.keys(moveEvaluation.maia.policy).forEach((move) => { |
| 45 | + maiaEvals[move] = new Array(MAIA_MODELS.length).fill(0) |
| 46 | + }) |
| 47 | + |
| 48 | + MAIA_MODELS.forEach((model, index) => { |
| 49 | + const modelAnalysis = allMaiaAnalysis[model] |
| 50 | + if (modelAnalysis?.policy) { |
| 51 | + Object.entries(modelAnalysis.policy).forEach(([move, probability]) => { |
| 52 | + if (!maiaEvals[move]) { |
| 53 | + maiaEvals[move] = new Array(MAIA_MODELS.length).fill(0) |
| 54 | + } |
| 55 | + maiaEvals[move][index] = probability |
| 56 | + }) |
164 | 57 | } |
165 | | - } else if (absCP < 150) { |
166 | | - if (cpAdvantage === playerColor) { |
167 | | - evaluation = `${playerColor} has a clear positional advantage that could be decisive with careful play.` |
168 | | - } else { |
169 | | - evaluation = `${playerColor} must play accurately as ${opponent} holds a clear positional advantage.` |
170 | | - } |
171 | | - } else if (absCP < 300) { |
172 | | - if (cpAdvantage === playerColor) { |
173 | | - evaluation = `${playerColor} has a significant advantage (${Math.round(toMoveWinrate * 100)}% win rate) that should be convertible with proper technique.` |
174 | | - } else { |
175 | | - evaluation = `${playerColor} faces a difficult position as ${opponent} has a significant advantage (${Math.round((1 - toMoveWinrate) * 100)}% win rate).` |
176 | | - } |
177 | | - } else if (absCP < 500) { |
178 | | - if (cpAdvantage === playerColor) { |
179 | | - evaluation = `${playerColor} is winning and only needs to avoid major blunders to convert.` |
180 | | - } else { |
181 | | - evaluation = `${playerColor} is in serious trouble and needs to find resilient defensive moves.` |
182 | | - } |
183 | | - } else { |
184 | | - if (cpAdvantage === playerColor) { |
185 | | - evaluation = `${playerColor} has a completely winning position with a ${Math.round(toMoveWinrate * 100)}% win probability.` |
186 | | - } else { |
187 | | - evaluation = `${playerColor} faces a nearly lost position with only a ${Math.round(toMoveWinrate * 100)}% win probability.` |
188 | | - } |
189 | | - } |
190 | | - |
191 | | - // Suggestion/description of move quality |
192 | | - if (isVeryBlunderProne) { |
193 | | - suggestion = `This critical position is extremely treacherous with a ${blunderProbability.toFixed(0)}% chance of ${playerColor} making a significant error.` |
194 | | - } else if (isBlunderProne && isTacticalPosition) { |
195 | | - suggestion = `The sharp tactical nature of this position creates many opportunities for mistakes (${blunderProbability.toFixed(0)}% blunder chance).` |
196 | | - } else if (isBlunderProne) { |
197 | | - suggestion = `This position is quite treacherous with ${blunderProbability.toFixed(0)}% chance of ${playerColor} making a significant mistake.` |
198 | | - } else if (isForcedPlay) { |
199 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
200 | | - suggestion = `${playerColor} must play ${moveSan}, as all other moves lead to a significantly worse position.` |
201 | | - } else if (isTacticalPosition && maiaMatchesStockfish) { |
202 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
203 | | - suggestion = `The tactical complexity demands precision, with ${moveSan} being the only move that maintains the balance.` |
204 | | - } else if (isPositionComplicated && hasMultipleGoodMoves) { |
205 | | - suggestion = `This complex position offers several equally promising continuations for ${playerColor}.` |
206 | | - } else if (isPositionComplicated) { |
207 | | - suggestion = `This is a complex position requiring careful calculation of the many reasonable options.` |
208 | | - } else if (isClearBestMove && maiaMatchesStockfish && maiaModelsAgree) { |
209 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
210 | | - suggestion = `Players of all levels agree ${moveSan} stands out as clearly best in this position.` |
211 | | - } else if (isClearBestMove && maiaMatchesStockfish) { |
212 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
213 | | - suggestion = `${playerColor} should play ${moveSan}, which both human intuition and concrete calculation confirm as best.` |
214 | | - } else if (isClearBestMove && maiaModelsAgree) { |
215 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
216 | | - suggestion = `Human players at all levels strongly prefer ${moveSan} (${topMaiaProbability.toFixed(0)}%), though the engine suggests otherwise.` |
217 | | - } else if (isClearBestMove) { |
218 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
219 | | - suggestion = `Maia strongly suggests ${moveSan} (${topMaiaProbability.toFixed(0)}% likely), though Stockfish calculates a different approach.` |
220 | | - } else if (goodProbability > 80) { |
221 | | - suggestion = `This is a forgiving position where almost any reasonable move by ${playerColor} maintains the evaluation.` |
222 | | - } else if (goodProbability > 60) { |
223 | | - suggestion = `Most moves ${playerColor} is likely to consider will maintain the current position assessment.` |
224 | | - } else if (maiaMatchesStockfish) { |
225 | | - const moveSan = colorSanMapping[topMaiaMove[0]]?.san || topMaiaMove[0] |
226 | | - suggestion = `Both human intuition and engine calculation agree that ${moveSan} is the best continuation here.` |
227 | | - } else if (hasMultipleGoodMoves) { |
228 | | - suggestion = `${playerColor} has several equally strong options, suggesting flexibility in planning.` |
229 | | - } else if (top3MaiaProbability < 50) { |
230 | | - suggestion = `This unusual position creates difficulties for human calculation, with no clearly favored continuation.` |
231 | | - } else { |
232 | | - suggestion = `There are several reasonable options for ${playerColor} to consider in this position.` |
233 | | - } |
| 58 | + }) |
234 | 59 |
|
235 | | - return `${evaluation} ${suggestion}` |
236 | | - }, [currentNode, moveEvaluation, blunderMeter, colorSanMapping]) |
| 60 | + return describePosition(fen, stockfishEvals, maiaEvals, whiteToMove) |
| 61 | + }, [currentNode, moveEvaluation]) |
237 | 62 | } |
0 commit comments