diff --git a/src/components/Analysis/Highlight.tsx b/src/components/Analysis/Highlight.tsx index 2edddae1..0751f69e 100644 --- a/src/components/Analysis/Highlight.tsx +++ b/src/components/Analysis/Highlight.tsx @@ -1,3 +1,4 @@ +import { Chess } from 'chess.ts' import { cpToWinrate } from 'src/lib' import { MoveTooltip } from './MoveTooltip' import { InteractiveDescription } from './InteractiveDescription' @@ -54,6 +55,35 @@ export const Highlight: React.FC = ({ isHomePage = false, }: Props) => { const { isMobile } = useContext(WindowSizeContext) + + // Check if current position is checkmate (independent of Stockfish analysis) + const isCurrentPositionCheckmate = currentNode + ? (() => { + try { + const chess = new Chess(currentNode.fen) + return chess.inCheckmate() + } catch { + return false + } + })() + : false + + // Helper function to format evaluation values + const formatEvaluation = ( + cp: number, + mateIn?: number, + isCheckmate?: boolean, + ) => { + if (isCheckmate) { + return 'Checkmate' + } + if (mateIn !== undefined) { + // Show +M2/-M2 to indicate whose mate it is + const sign = mateIn > 0 ? '+' : '-' + return `${sign}M${Math.abs(mateIn)}` + } + return `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}` + } const [tooltipData, setTooltipData] = useState<{ move: string maiaProb?: number @@ -210,6 +240,26 @@ export const Highlight: React.FC = ({ // Get the appropriate win rate const getWhiteWinRate = () => { + // Handle checkmate positions (check this first, even without Stockfish analysis) + if (isCurrentPositionCheckmate) { + // If it's checkmate, the current player has lost + const currentTurn = currentNode?.turn || 'w' + return currentTurn === 'w' ? '0.0%' : '100.0%' + } + + // Handle checkmate positions detected by Stockfish + if (moveEvaluation?.stockfish?.is_checkmate) { + // If it's checkmate, the current player has lost + const currentTurn = currentNode?.turn || 'w' + return currentTurn === 'w' ? '0.0%' : '100.0%' + } + + // Handle mate in X positions + if (moveEvaluation?.stockfish?.mate_in !== undefined) { + const mateIn = moveEvaluation.stockfish.mate_in + return mateIn > 0 ? '100.0%' : '0.0%' + } + if ( isInFirst10Ply && moveEvaluation?.stockfish?.model_optimal_cp !== undefined @@ -330,9 +380,15 @@ export const Highlight: React.FC = ({ : ''}

- {moveEvaluation?.stockfish - ? `${moveEvaluation.stockfish.model_optimal_cp > 0 ? '+' : ''}${moveEvaluation.stockfish.model_optimal_cp / 100}` - : '...'} + {isCurrentPositionCheckmate + ? 'Checkmate' + : moveEvaluation?.stockfish + ? formatEvaluation( + moveEvaluation.stockfish.model_optimal_cp, + moveEvaluation.stockfish.mate_in, + moveEvaluation.stockfish.is_checkmate, + ) + : '...'}

@@ -386,8 +442,9 @@ export const Highlight: React.FC = ({ {colorSanMapping[move]?.san ?? move}

- {cp > 0 ? '+' : null} - {`${(cp / 100).toFixed(2)}`} + {Math.abs(cp) >= 10000 + ? `${cp > 0 ? '+' : '-'}M${Math.max(1, Math.floor(Math.abs(10000 - Math.abs(cp)) / 100) + 1)}` + : `${cp > 0 ? '+' : ''}${(cp / 100).toFixed(2)}`}

) diff --git a/src/components/Settings/MaiaModelSettings.tsx b/src/components/Settings/MaiaModelSettings.tsx index b2c92ff8..d717ad24 100644 --- a/src/components/Settings/MaiaModelSettings.tsx +++ b/src/components/Settings/MaiaModelSettings.tsx @@ -119,9 +119,12 @@ export const MaiaModelSettings: React.FC = () => { return (
-

Maia Neural Network Model

+

+ Maia Neural Network Model +

- Manage your locally stored Maia chess engine model. The model is downloaded once and stored in your browser for offline use. + Manage your locally stored Maia chess engine model. The model is + downloaded once and stored in your browser for offline use.

@@ -129,8 +132,11 @@ export const MaiaModelSettings: React.FC = () => { {!storageInfo?.supported && (

- warning - IndexedDB storage is not supported in your browser. Model management features are unavailable. + + warning + + IndexedDB storage is not supported in your browser. Model + management features are unavailable.

)} @@ -140,19 +146,26 @@ export const MaiaModelSettings: React.FC = () => { {/* Status section */}
- + {statusDisplay.icon}

Model Status

-

{statusDisplay.text}

+

+ {statusDisplay.text} +

{status === 'downloading' && (
-
+
)} @@ -161,7 +174,9 @@ export const MaiaModelSettings: React.FC = () => { {/* Storage Information section */} {storageInfo && (
-

Storage Information

+

+ Storage Information +

{storageInfo.modelSize && (
@@ -196,7 +211,9 @@ export const MaiaModelSettings: React.FC = () => { onClick={handleRedownloadModel} className="flex items-center justify-center gap-2 rounded-md border border-red-500/30 bg-red-500/20 px-4 py-2 text-red-200 transition-all duration-200 hover:border-red-500/40 hover:bg-red-500/30" > - download + + download + Download Model )} @@ -208,7 +225,9 @@ export const MaiaModelSettings: React.FC = () => { disabled={status !== 'ready'} className="flex items-center justify-center gap-2 rounded-md border border-white/10 bg-white/5 px-4 py-2 text-white/90 backdrop-blur-sm transition-all duration-200 hover:border-white/20 hover:bg-white/10 disabled:opacity-50" > - refresh + + refresh + Re-download @@ -217,7 +236,9 @@ export const MaiaModelSettings: React.FC = () => { disabled={isDeleting || status !== 'ready'} className="flex items-center justify-center gap-2 rounded-md border border-red-500/30 bg-red-500/20 px-4 py-2 text-red-200 transition-all duration-200 hover:border-red-500/40 hover:bg-red-500/30 disabled:opacity-50" > - delete + + delete + {isDeleting ? 'Deleting...' : 'Delete Model'} diff --git a/src/components/Settings/SoundSettings.tsx b/src/components/Settings/SoundSettings.tsx index 30f902a3..f1cab354 100644 --- a/src/components/Settings/SoundSettings.tsx +++ b/src/components/Settings/SoundSettings.tsx @@ -33,7 +33,9 @@ export const SoundSettings: React.FC = () => { {/* Sound Toggle */}
- Enable Move Sounds + + Enable Move Sounds +

Play sounds when chess pieces are moved or captured

diff --git a/src/lib/analysis.ts b/src/lib/analysis.ts index 208d9a5f..4d445469 100644 --- a/src/lib/analysis.ts +++ b/src/lib/analysis.ts @@ -17,13 +17,29 @@ export function convertBackendEvalToStockfishEval( const cp_relative_vec: { [key: string]: number } = {} let model_optimal_cp = -Infinity let model_move = '' + let bestMateIn: number | undefined = undefined + // Detect mate values and convert them for (const move in possibleMoves) { const cp = possibleMoves[move] + + // Detect mate patterns (±10000 indicates mate) + let mateIn: number | undefined = undefined + if (Math.abs(cp) >= 10000) { + // Estimate mate in moves based on how close to 10000 it is + // The closer to 10000, the faster the mate + const mateDistance = Math.abs(10000 - Math.abs(cp)) + mateIn = + cp > 0 + ? Math.max(1, Math.floor(mateDistance / 100) + 1) + : -Math.max(1, Math.floor(mateDistance / 100) + 1) + } + cp_vec[move] = cp if (cp > model_optimal_cp) { model_optimal_cp = cp model_move = move + bestMateIn = mateIn } } @@ -45,7 +61,9 @@ export function convertBackendEvalToStockfishEval( for (const move in cp_vec_sorted) { const cp = cp_vec_sorted[move] - const winrate = cpToWinrate(cp, false) + // For mate positions, set winrate to 1.0 or 0.0 + const isMate = Math.abs(cp) >= 10000 + const winrate = isMate ? (cp > 0 ? 1.0 : 0.0) : cpToWinrate(cp, false) winrate_vec[move] = winrate if (winrate_vec[move] > max_winrate) { @@ -71,8 +89,13 @@ export function convertBackendEvalToStockfishEval( for (const move in cp_vec_sorted) { cp_vec_sorted[move] *= -1 } + if (bestMateIn !== undefined) { + bestMateIn *= -1 + } } + // We can't easily detect checkmate from backend data without FEN, + // so we'll leave is_checkmate as undefined for backend evaluations return { sent: true, depth: 20, @@ -82,6 +105,8 @@ export function convertBackendEvalToStockfishEval( cp_relative_vec: cp_relative_vec_sorted, winrate_vec: winrate_vec_sorted, winrate_loss_vec: winrate_loss_vec_sorted, + mate_in: bestMateIn, + is_checkmate: undefined, } } diff --git a/src/lib/engine/stockfish.ts b/src/lib/engine/stockfish.ts index 9d544652..f9158761 100644 --- a/src/lib/engine/stockfish.ts +++ b/src/lib/engine/stockfish.ts @@ -173,7 +173,11 @@ class Engine { return } + // Handle mate values properly instead of converting to ±10000 + let mateIn: number | undefined = undefined if (!isNaN(mate) && isNaN(cp)) { + // For mate scores, use a very high cp value for sorting but keep mate info + mateIn = mate cp = mate > 0 ? 10000 : -10000 } @@ -189,11 +193,16 @@ class Engine { For example: - If Stockfish reports CP = 100 (White's advantage) and it's White's turn, we keep CP = 100. - 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. + + The same logic applies to mate values - they need to be adjusted for the current player's perspective. */ const board = new Chess(this.fen) const isBlackTurn = board.turn() === 'b' if (isBlackTurn) { cp *= -1 + if (mateIn !== undefined) { + mateIn *= -1 + } } if (this.store[depth]) { @@ -215,7 +224,11 @@ class Engine { ? this.store[depth].model_optimal_cp - cp : cp - this.store[depth].model_optimal_cp - const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false) + const winrate = mateIn + ? mateIn > 0 + ? 1.0 + : 0.0 + : cpToWinrate(cp * (isBlackTurn ? -1 : 1), false) if (!this.store[depth].winrate_vec) { this.store[depth].winrate_vec = {} @@ -229,7 +242,11 @@ class Engine { winrateVec[move] = winrate } } else { - const winrate = cpToWinrate(cp * (isBlackTurn ? -1 : 1), false) + const winrate = mateIn + ? mateIn > 0 + ? 1.0 + : 0.0 + : cpToWinrate(cp * (isBlackTurn ? -1 : 1), false) this.store[depth] = { depth: depth, @@ -239,6 +256,7 @@ class Engine { cp_relative_vec: { [move]: 0 }, winrate_vec: { [move]: winrate }, winrate_loss_vec: { [move]: 0 }, + mate_in: mateIn, sent: false, } } @@ -279,6 +297,10 @@ class Engine { ) } + // Check if position is checkmate (no legal moves and king in check) + const board = new Chess(this.fen) + this.store[depth].is_checkmate = board.inCheckmate() + this.store[depth].sent = true if (this.evaluationResolver) { this.evaluationResolver(this.store[depth]) diff --git a/src/types/analysis.ts b/src/types/analysis.ts index 14897832..cc40250e 100644 --- a/src/types/analysis.ts +++ b/src/types/analysis.ts @@ -29,6 +29,8 @@ export interface StockfishEvaluation { cp_relative_vec: { [key: string]: number } winrate_vec?: { [key: string]: number } winrate_loss_vec?: { [key: string]: number } + mate_in?: number + is_checkmate?: boolean } export interface CachedEngineAnalysisEntry {