Skip to content

Commit d84496d

Browse files
fix: stockfish mate logic
1 parent e4883e2 commit d84496d

File tree

4 files changed

+124
-57
lines changed

4 files changed

+124
-57
lines changed

src/components/Analysis/Highlight.tsx

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,82 @@ export const Highlight: React.FC<Props> = ({
7070
})()
7171
: false
7272

73-
// Helper function to format evaluation values
74-
const formatEvaluation = (
75-
cp: number,
76-
mateIn?: number,
77-
isCheckmate?: boolean,
78-
) => {
79-
if (isCheckmate) {
73+
const currentTurn: 'w' | 'b' =
74+
currentNode?.turn || (recommendations.isBlackTurn ? 'b' : 'w')
75+
76+
const formatMateDisplay = (mateValue: number) => {
77+
const deliveringColor =
78+
mateValue > 0 ? currentTurn : currentTurn === 'w' ? 'b' : 'w'
79+
const prefix = deliveringColor === 'w' ? '+' : '-'
80+
return `${prefix}M${Math.abs(mateValue)}`
81+
}
82+
83+
const getStockfishEvalDisplay = () => {
84+
if (!moveEvaluation?.stockfish) {
85+
return '...'
86+
}
87+
88+
const { stockfish } = moveEvaluation
89+
const isBlackTurn = currentTurn === 'b'
90+
91+
if (stockfish.is_checkmate) {
8092
return 'Checkmate'
8193
}
82-
if (mateIn !== undefined) {
83-
// Show +M2/-M2 to indicate whose mate it is
84-
const sign = mateIn > 0 ? '+' : '-'
85-
return `${sign}M${Math.abs(mateIn)}`
94+
95+
const mateEntries = Object.entries(stockfish.mate_vec ?? {})
96+
const positiveMates = mateEntries.filter(([, mate]) => mate > 0)
97+
98+
if (positiveMates.length > 0) {
99+
const minMate = positiveMates.reduce(
100+
(min, [, mate]) => Math.min(min, mate),
101+
Infinity,
102+
)
103+
104+
if (isFinite(minMate)) {
105+
return formatMateDisplay(minMate)
106+
}
107+
}
108+
109+
const mateVec = stockfish.mate_vec ?? {}
110+
const cpEntries = Object.entries(stockfish.cp_vec)
111+
const nonMateEntries = cpEntries.filter(
112+
([move]) => mateVec[move] === undefined,
113+
)
114+
const bestCp = nonMateEntries.reduce<number | null>((acc, [, cp]) => {
115+
if (acc === null) {
116+
return cp
117+
}
118+
return isBlackTurn ? Math.min(acc, cp) : Math.max(acc, cp)
119+
}, null)
120+
121+
if (bestCp !== null) {
122+
return `${bestCp > 0 ? '+' : ''}${(bestCp / 100).toFixed(2)}`
86123
}
87-
return `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`
124+
125+
const opponentMates = mateEntries.filter(([, mate]) => mate < 0)
126+
if (opponentMates.length > 0) {
127+
const maxMate = opponentMates.reduce(
128+
(max, [, mate]) => Math.max(max, Math.abs(mate)),
129+
0,
130+
)
131+
132+
if (maxMate > 0) {
133+
return formatMateDisplay(-maxMate)
134+
}
135+
}
136+
137+
const fallbackCp = cpEntries.reduce<number | null>((acc, [, cp]) => {
138+
if (acc === null) {
139+
return cp
140+
}
141+
return isBlackTurn ? Math.min(acc, cp) : Math.max(acc, cp)
142+
}, null)
143+
144+
if (fallbackCp !== null) {
145+
return `${fallbackCp > 0 ? '+' : ''}${(fallbackCp / 100).toFixed(2)}`
146+
}
147+
148+
return '...'
88149
}
89150
const [tooltipData, setTooltipData] = useState<{
90151
move: string
@@ -246,23 +307,26 @@ export const Highlight: React.FC<Props> = ({
246307
return currentTurn === 'w' ? '0.0%' : '100.0%'
247308
}
248309

249-
if (moveEvaluation?.stockfish?.is_checkmate) {
310+
const stockfishEval = moveEvaluation?.stockfish
311+
312+
if (stockfishEval?.is_checkmate) {
250313
const currentTurn = currentNode?.turn || 'w'
251314
return currentTurn === 'w' ? '0.0%' : '100.0%'
252315
}
253316

254-
if (moveEvaluation?.stockfish?.mate_in !== undefined) {
255-
const mateIn = moveEvaluation.stockfish.mate_in
256-
return mateIn > 0 ? '100.0%' : '0.0%'
257-
}
258-
259317
if (
260-
isInFirst10Ply &&
261-
moveEvaluation?.stockfish?.model_optimal_cp !== undefined
318+
stockfishEval?.model_move &&
319+
stockfishEval.mate_vec &&
320+
stockfishEval.mate_vec[stockfishEval.model_move] !== undefined
262321
) {
263-
const stockfishWinRate = cpToWinrate(
264-
moveEvaluation.stockfish.model_optimal_cp,
265-
)
322+
const mateValue = stockfishEval.mate_vec[stockfishEval.model_move]
323+
const deliveringColor =
324+
mateValue > 0 ? currentTurn : currentTurn === 'w' ? 'b' : 'w'
325+
return deliveringColor === 'w' ? '100.0%' : '0.0%'
326+
}
327+
328+
if (isInFirst10Ply && stockfishEval?.model_optimal_cp !== undefined) {
329+
const stockfishWinRate = cpToWinrate(stockfishEval.model_optimal_cp)
266330
return `${Math.round(stockfishWinRate * 1000) / 10}%`
267331
} else if (moveEvaluation?.maia) {
268332
return `${Math.round(moveEvaluation.maia.value * 1000) / 10}%`
@@ -391,13 +455,7 @@ export const Highlight: React.FC<Props> = ({
391455
<p className="text-sm font-bold text-engine-1 md:text-sm lg:text-lg">
392456
{isCurrentPositionCheckmate
393457
? 'Checkmate'
394-
: moveEvaluation?.stockfish
395-
? formatEvaluation(
396-
moveEvaluation.stockfish.model_optimal_cp,
397-
moveEvaluation.stockfish.mate_in,
398-
moveEvaluation.stockfish.is_checkmate,
399-
)
400-
: '...'}
458+
: getStockfishEvalDisplay()}
401459
</p>
402460
</div>
403461

@@ -424,6 +482,11 @@ export const Highlight: React.FC<Props> = ({
424482
{recommendations.stockfish
425483
?.slice(0, 4)
426484
.map(({ move, cp, winrate, cp_relative }, index) => {
485+
const mateValue = moveEvaluation?.stockfish?.mate_vec?.[move]
486+
const moveEvalDisplay =
487+
mateValue !== undefined
488+
? formatMateDisplay(mateValue)
489+
: `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`
427490
return (
428491
<button
429492
key={index}
@@ -463,9 +526,7 @@ export const Highlight: React.FC<Props> = ({
463526
<p
464527
className={`text-right font-mono ${simplified ? 'text-sm' : 'text-sm md:text-xxs xl:text-xs'}`}
465528
>
466-
{Math.abs(cp) >= 10000
467-
? `${cp > 0 ? '+' : '-'}M${Math.max(1, Math.floor(Math.abs(10000 - Math.abs(cp)) / 100) + 1)}`
468-
: `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`}
529+
{moveEvalDisplay}
469530
</p>
470531
</button>
471532
)

src/lib/analysis.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function convertBackendEvalToStockfishEval(
1717
const cp_relative_vec: { [key: string]: number } = {}
1818
let model_optimal_cp = -Infinity
1919
let model_move = ''
20-
let bestMateIn: number | undefined = undefined
20+
const mate_vec: { [key: string]: number } = {}
2121

2222
// Detect mate values and convert them
2323
for (const move in possibleMoves) {
@@ -33,13 +33,13 @@ export function convertBackendEvalToStockfishEval(
3333
cp > 0
3434
? Math.max(1, Math.floor(mateDistance / 100) + 1)
3535
: -Math.max(1, Math.floor(mateDistance / 100) + 1)
36+
mate_vec[move] = mateIn
3637
}
3738

3839
cp_vec[move] = cp
3940
if (cp > model_optimal_cp) {
4041
model_optimal_cp = cp
4142
model_move = move
42-
bestMateIn = mateIn
4343
}
4444
}
4545

@@ -89,11 +89,20 @@ export function convertBackendEvalToStockfishEval(
8989
for (const move in cp_vec_sorted) {
9090
cp_vec_sorted[move] *= -1
9191
}
92-
if (bestMateIn !== undefined) {
93-
bestMateIn *= -1
92+
for (const move in mate_vec) {
93+
mate_vec[move] *= -1
9494
}
9595
}
9696

97+
const mate_vec_sorted =
98+
Object.keys(mate_vec).length > 0
99+
? Object.fromEntries(
100+
Object.keys(cp_vec_sorted)
101+
.filter((move) => mate_vec[move] !== undefined)
102+
.map((move) => [move, mate_vec[move]]),
103+
)
104+
: undefined
105+
97106
// We can't easily detect checkmate from backend data without FEN,
98107
// so we'll leave is_checkmate as undefined for backend evaluations
99108
return {
@@ -105,7 +114,7 @@ export function convertBackendEvalToStockfishEval(
105114
cp_relative_vec: cp_relative_vec_sorted,
106115
winrate_vec: winrate_vec_sorted,
107116
winrate_loss_vec: winrate_loss_vec_sorted,
108-
mate_in: bestMateIn,
117+
mate_vec: mate_vec_sorted,
109118
is_checkmate: undefined,
110119
}
111120
}

src/lib/engine/stockfish.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,6 @@ class Engine {
198198
const isBlackTurn = board.turn() === 'b'
199199
if (isBlackTurn) {
200200
cp *= -1
201-
if (mateIn !== undefined) {
202-
mateIn *= -1
203-
}
204201
}
205202

206203
if (this.store[depth]) {
@@ -222,15 +219,19 @@ class Engine {
222219
? this.store[depth].model_optimal_cp - cp
223220
: cp - this.store[depth].model_optimal_cp
224221

225-
const winrate = mateIn
226-
? mateIn > 0 // if white has a mate in sight
227-
? isBlackTurn
228-
? 0.0 // black has no chance of winning if this move is played
229-
: 1.0 // white has a 100% chance of winning if this move is played
230-
: isBlackTurn
231-
? 1.0 // black has a 100% chance of winning if this move is played
232-
: 0.0 // white has no chance of winning if this move is played
233-
: cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
222+
if (mateIn !== undefined) {
223+
if (!this.store[depth].mate_vec) {
224+
this.store[depth].mate_vec = {}
225+
}
226+
this.store[depth].mate_vec[move] = mateIn
227+
} else if (this.store[depth].mate_vec) {
228+
delete this.store[depth].mate_vec[move]
229+
if (Object.keys(this.store[depth].mate_vec).length === 0) {
230+
delete this.store[depth].mate_vec
231+
}
232+
}
233+
234+
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
234235

235236
if (!this.store[depth].winrate_vec) {
236237
this.store[depth].winrate_vec = {}
@@ -244,11 +245,7 @@ class Engine {
244245
winrateVec[move] = winrate
245246
}
246247
} else {
247-
const winrate = mateIn
248-
? mateIn > 0
249-
? 1.0
250-
: 0.0
251-
: cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
248+
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
252249

253250
this.store[depth] = {
254251
depth: depth,
@@ -258,7 +255,7 @@ class Engine {
258255
cp_relative_vec: { [move]: 0 },
259256
winrate_vec: { [move]: winrate },
260257
winrate_loss_vec: { [move]: 0 },
261-
mate_in: mateIn,
258+
mate_vec: mateIn !== undefined ? { [move]: mateIn } : undefined,
262259
sent: false,
263260
}
264261
}

src/types/analysis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface StockfishEvaluation {
2929
cp_relative_vec: { [key: string]: number }
3030
winrate_vec?: { [key: string]: number }
3131
winrate_loss_vec?: { [key: string]: number }
32-
mate_in?: number
32+
mate_vec?: { [key: string]: number }
3333
is_checkmate?: boolean
3434
}
3535

0 commit comments

Comments
 (0)