Skip to content

Commit 9306092

Browse files
Merge pull request #217 from CSSLab/211-stockfish-mate-logic
Implement proper 'Mate in X' and checkmate detection
2 parents 146ce90 + 70790ef commit 9306092

4 files changed

Lines changed: 114 additions & 8 deletions

File tree

src/components/Analysis/Highlight.tsx

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Chess } from 'chess.ts'
12
import { cpToWinrate } from 'src/lib'
23
import { MoveTooltip } from './MoveTooltip'
34
import { InteractiveDescription } from './InteractiveDescription'
@@ -54,6 +55,35 @@ export const Highlight: React.FC<Props> = ({
5455
isHomePage = false,
5556
}: Props) => {
5657
const { isMobile } = useContext(WindowSizeContext)
58+
59+
// Check if current position is checkmate (independent of Stockfish analysis)
60+
const isCurrentPositionCheckmate = currentNode
61+
? (() => {
62+
try {
63+
const chess = new Chess(currentNode.fen)
64+
return chess.inCheckmate()
65+
} catch {
66+
return false
67+
}
68+
})()
69+
: false
70+
71+
// Helper function to format evaluation values
72+
const formatEvaluation = (
73+
cp: number,
74+
mateIn?: number,
75+
isCheckmate?: boolean,
76+
) => {
77+
if (isCheckmate) {
78+
return 'Checkmate'
79+
}
80+
if (mateIn !== undefined) {
81+
// Show +M2/-M2 to indicate whose mate it is
82+
const sign = mateIn > 0 ? '+' : '-'
83+
return `${sign}M${Math.abs(mateIn)}`
84+
}
85+
return `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`
86+
}
5787
const [tooltipData, setTooltipData] = useState<{
5888
move: string
5989
maiaProb?: number
@@ -210,6 +240,26 @@ export const Highlight: React.FC<Props> = ({
210240

211241
// Get the appropriate win rate
212242
const getWhiteWinRate = () => {
243+
// Handle checkmate positions (check this first, even without Stockfish analysis)
244+
if (isCurrentPositionCheckmate) {
245+
// If it's checkmate, the current player has lost
246+
const currentTurn = currentNode?.turn || 'w'
247+
return currentTurn === 'w' ? '0.0%' : '100.0%'
248+
}
249+
250+
// Handle checkmate positions detected by Stockfish
251+
if (moveEvaluation?.stockfish?.is_checkmate) {
252+
// If it's checkmate, the current player has lost
253+
const currentTurn = currentNode?.turn || 'w'
254+
return currentTurn === 'w' ? '0.0%' : '100.0%'
255+
}
256+
257+
// Handle mate in X positions
258+
if (moveEvaluation?.stockfish?.mate_in !== undefined) {
259+
const mateIn = moveEvaluation.stockfish.mate_in
260+
return mateIn > 0 ? '100.0%' : '0.0%'
261+
}
262+
213263
if (
214264
isInFirst10Ply &&
215265
moveEvaluation?.stockfish?.model_optimal_cp !== undefined
@@ -330,9 +380,15 @@ export const Highlight: React.FC<Props> = ({
330380
: ''}
331381
</p>
332382
<p className="text-base font-bold text-engine-1 md:text-sm lg:text-lg">
333-
{moveEvaluation?.stockfish
334-
? `${moveEvaluation.stockfish.model_optimal_cp > 0 ? '+' : ''}${moveEvaluation.stockfish.model_optimal_cp / 100}`
335-
: '...'}
383+
{isCurrentPositionCheckmate
384+
? 'Checkmate'
385+
: moveEvaluation?.stockfish
386+
? formatEvaluation(
387+
moveEvaluation.stockfish.model_optimal_cp,
388+
moveEvaluation.stockfish.mate_in,
389+
moveEvaluation.stockfish.is_checkmate,
390+
)
391+
: '...'}
336392
</p>
337393
</div>
338394

@@ -386,8 +442,9 @@ export const Highlight: React.FC<Props> = ({
386442
{colorSanMapping[move]?.san ?? move}
387443
</p>
388444
<p className="text-right font-mono text-sm md:text-xxs xl:text-xs">
389-
{cp > 0 ? '+' : null}
390-
{`${(cp / 100).toFixed(2)}`}
445+
{Math.abs(cp) >= 10000
446+
? `${cp > 0 ? '+' : '-'}M${Math.max(1, Math.floor(Math.abs(10000 - Math.abs(cp)) / 100) + 1)}`
447+
: `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`}
391448
</p>
392449
</button>
393450
)

src/lib/analysis.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,29 @@ 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
2021

22+
// Detect mate values and convert them
2123
for (const move in possibleMoves) {
2224
const cp = possibleMoves[move]
25+
26+
// Detect mate patterns (±10000 indicates mate)
27+
let mateIn: number | undefined = undefined
28+
if (Math.abs(cp) >= 10000) {
29+
// Estimate mate in moves based on how close to 10000 it is
30+
// The closer to 10000, the faster the mate
31+
const mateDistance = Math.abs(10000 - Math.abs(cp))
32+
mateIn =
33+
cp > 0
34+
? Math.max(1, Math.floor(mateDistance / 100) + 1)
35+
: -Math.max(1, Math.floor(mateDistance / 100) + 1)
36+
}
37+
2338
cp_vec[move] = cp
2439
if (cp > model_optimal_cp) {
2540
model_optimal_cp = cp
2641
model_move = move
42+
bestMateIn = mateIn
2743
}
2844
}
2945

@@ -45,7 +61,9 @@ export function convertBackendEvalToStockfishEval(
4561

4662
for (const move in cp_vec_sorted) {
4763
const cp = cp_vec_sorted[move]
48-
const winrate = cpToWinrate(cp, false)
64+
// For mate positions, set winrate to 1.0 or 0.0
65+
const isMate = Math.abs(cp) >= 10000
66+
const winrate = isMate ? (cp > 0 ? 1.0 : 0.0) : cpToWinrate(cp, false)
4967
winrate_vec[move] = winrate
5068

5169
if (winrate_vec[move] > max_winrate) {
@@ -71,8 +89,13 @@ export function convertBackendEvalToStockfishEval(
7189
for (const move in cp_vec_sorted) {
7290
cp_vec_sorted[move] *= -1
7391
}
92+
if (bestMateIn !== undefined) {
93+
bestMateIn *= -1
94+
}
7495
}
7596

97+
// We can't easily detect checkmate from backend data without FEN,
98+
// so we'll leave is_checkmate as undefined for backend evaluations
7699
return {
77100
sent: true,
78101
depth: 20,
@@ -82,6 +105,8 @@ export function convertBackendEvalToStockfishEval(
82105
cp_relative_vec: cp_relative_vec_sorted,
83106
winrate_vec: winrate_vec_sorted,
84107
winrate_loss_vec: winrate_loss_vec_sorted,
108+
mate_in: bestMateIn,
109+
is_checkmate: undefined,
85110
}
86111
}
87112

src/lib/engine/stockfish.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,11 @@ class Engine {
173173
return
174174
}
175175

176+
// Handle mate values properly instead of converting to ±10000
177+
let mateIn: number | undefined = undefined
176178
if (!isNaN(mate) && isNaN(cp)) {
179+
// For mate scores, use a very high cp value for sorting but keep mate info
180+
mateIn = mate
177181
cp = mate > 0 ? 10000 : -10000
178182
}
179183

@@ -189,11 +193,16 @@ class Engine {
189193
For example:
190194
- If Stockfish reports CP = 100 (White's advantage) and it's White's turn, we keep CP = 100.
191195
- If Stockfish reports CP = 100 (White's advantage) and it's Black's turn, we change CP to -100, indicating that Black is at a disadvantage.
196+
197+
The same logic applies to mate values - they need to be adjusted for the current player's perspective.
192198
*/
193199
const board = new Chess(this.fen)
194200
const isBlackTurn = board.turn() === 'b'
195201
if (isBlackTurn) {
196202
cp *= -1
203+
if (mateIn !== undefined) {
204+
mateIn *= -1
205+
}
197206
}
198207

199208
if (this.store[depth]) {
@@ -215,7 +224,11 @@ class Engine {
215224
? this.store[depth].model_optimal_cp - cp
216225
: cp - this.store[depth].model_optimal_cp
217226

218-
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
227+
const winrate = mateIn
228+
? mateIn > 0
229+
? 1.0
230+
: 0.0
231+
: cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
219232

220233
if (!this.store[depth].winrate_vec) {
221234
this.store[depth].winrate_vec = {}
@@ -229,7 +242,11 @@ class Engine {
229242
winrateVec[move] = winrate
230243
}
231244
} else {
232-
const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
245+
const winrate = mateIn
246+
? mateIn > 0
247+
? 1.0
248+
: 0.0
249+
: cpToWinrate(cp * (isBlackTurn ? -1 : 1), false)
233250

234251
this.store[depth] = {
235252
depth: depth,
@@ -239,6 +256,7 @@ class Engine {
239256
cp_relative_vec: { [move]: 0 },
240257
winrate_vec: { [move]: winrate },
241258
winrate_loss_vec: { [move]: 0 },
259+
mate_in: mateIn,
242260
sent: false,
243261
}
244262
}
@@ -279,6 +297,10 @@ class Engine {
279297
)
280298
}
281299

300+
// Check if position is checkmate (no legal moves and king in check)
301+
const board = new Chess(this.fen)
302+
this.store[depth].is_checkmate = board.inCheckmate()
303+
282304
this.store[depth].sent = true
283305
if (this.evaluationResolver) {
284306
this.evaluationResolver(this.store[depth])

src/types/analysis.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ 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
33+
is_checkmate?: boolean
3234
}
3335

3436
export interface CachedEngineAnalysisEntry {

0 commit comments

Comments
 (0)