Skip to content

Commit 1fa395c

Browse files
feat: port to use winrate loss instead of centipawn loss for move categorization
1 parent 5d6cd47 commit 1fa395c

File tree

10 files changed

+311
-59
lines changed

10 files changed

+311
-59
lines changed

src/api/analysis/analysis.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
AnalysisTournamentGame,
1313
} from 'src/types'
1414
import { buildUrl } from '../utils'
15-
15+
import { cpToWinrate } from 'src/utils/stockfish'
1616
import { AvailableMoves } from 'src/types/training'
1717

1818
function buildGameTree(moves: any[], initialFen: string) {
@@ -444,6 +444,32 @@ function convertMoveMapToStockfishEval(
444444
Object.entries(cp_relative_vec).sort(([, a], [, b]) => b - a),
445445
)
446446

447+
const winrate_vec: { [key: string]: number } = {}
448+
let max_winrate = -Infinity
449+
450+
for (const move in cp_vec_sorted) {
451+
const cp = cp_vec_sorted[move]
452+
const winrate = cpToWinrate(cp, false)
453+
winrate_vec[move] = winrate
454+
455+
if (winrate_vec[move] > max_winrate) {
456+
max_winrate = winrate_vec[move]
457+
}
458+
}
459+
460+
const winrate_loss_vec: { [key: string]: number } = {}
461+
for (const move in winrate_vec) {
462+
winrate_loss_vec[move] = winrate_vec[move] - max_winrate
463+
}
464+
465+
const winrate_vec_sorted = Object.fromEntries(
466+
Object.entries(winrate_vec).sort(([, a], [, b]) => b - a),
467+
)
468+
469+
const winrate_loss_vec_sorted = Object.fromEntries(
470+
Object.entries(winrate_loss_vec).sort(([, a], [, b]) => b - a),
471+
)
472+
447473
if (turn === 'b') {
448474
model_optimal_cp *= -1
449475
for (const move in cp_vec_sorted) {
@@ -458,6 +484,8 @@ function convertMoveMapToStockfishEval(
458484
model_optimal_cp: model_optimal_cp,
459485
cp_vec: cp_vec_sorted,
460486
cp_relative_vec: cp_relative_vec_sorted,
487+
winrate_vec: winrate_vec_sorted,
488+
winrate_loss_vec: winrate_loss_vec_sorted,
461489
}
462490
}
463491

src/components/Analysis/AnalysisGameList.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
6161
}
6262
}, [analysisTournamentList, currentId, listKeys])
6363

64-
console.log(currentId)
65-
6664
const [selected, setSelected] = useState<
6765
'tournament' | 'pgn' | 'play' | 'hand' | 'brain'
6866
>(

src/components/Analysis/BlunderMeter.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,7 @@ function MovesList({
174174
colorSanMapping: ColorSanMapping
175175
}) {
176176
const filteredMoves = () => {
177-
if (moves.length > 0 && moves[0].probability < 10) {
178-
return moves.slice(0, 6).slice(0, 1)
179-
}
180-
return moves.slice(0, 6).filter((move) => move.probability >= 10)
177+
return moves.slice(0, 6).filter((move) => move.probability >= 8)
181178
}
182179

183180
return (
@@ -259,10 +256,7 @@ function Meter({
259256
moves: { move: string; probability: number }[]
260257
}) {
261258
const filteredMoves = () => {
262-
if (moves.length > 0 && moves[0].probability < 10) {
263-
return moves.slice(0, 6).slice(0, 1)
264-
}
265-
return moves.slice(0, 6).filter((move) => move.probability >= 10)
259+
return moves.slice(0, 6).filter((move) => move.probability >= 8)
266260
}
267261

268262
return (

src/components/Analysis/Highlight.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ interface Props {
1111
colorSanMapping: ColorSanMapping
1212
recommendations: {
1313
maia?: { move: string; prob: number }[]
14-
stockfish?: { move: string; cp: number }[]
14+
stockfish?: {
15+
move: string
16+
cp: number
17+
winrate?: number
18+
winrate_loss?: number
19+
}[]
20+
isBlackTurn?: boolean
1521
}
1622
hover: (move?: string) => void
1723
makeMove: (move: string) => void
@@ -40,12 +46,17 @@ export const Highlight: React.FC<Props> = ({
4046
source: 'maia' | 'stockfish',
4147
prob?: number,
4248
cp?: number,
49+
winrate?: number,
4350
) => {
4451
const matchingMove = findMatchingMove(move, source)
4552
const maiaProb =
4653
source === 'maia' ? prob : (matchingMove as { prob: number })?.prob
4754
const stockfishCp =
4855
source === 'stockfish' ? cp : (matchingMove as { cp: number })?.cp
56+
const stockfishWinrate =
57+
source === 'stockfish'
58+
? winrate
59+
: (matchingMove as { winrate?: number })?.winrate
4960

5061
return (
5162
<div className="flex flex-col gap-1 overflow-hidden rounded border border-background-2 bg-backdrop">
@@ -75,6 +86,12 @@ export const Highlight: React.FC<Props> = ({
7586
</span>
7687
</div>
7788
)}
89+
{stockfishWinrate !== undefined && (
90+
<div className="flex w-full items-center justify-between gap-2 font-mono">
91+
<span className="text-engine-3">Win Rate:</span>
92+
<span>{(stockfishWinrate * 100).toFixed(1)}%</span>
93+
</div>
94+
)}
7895
</div>
7996
</div>
8097
)
@@ -119,8 +136,8 @@ export const Highlight: React.FC<Props> = ({
119136
className="!z-50 !bg-none !p-0 !opacity-100"
120137
render={({ content }) => {
121138
if (!content) return null
122-
const { move, source, prob, cp } = JSON.parse(content)
123-
return getTooltipContent(move, source, prob, cp)
139+
const { move, source, prob, cp, winrate } = JSON.parse(content)
140+
return getTooltipContent(move, source, prob, cp, winrate)
124141
}}
125142
/>
126143
<div className="grid grid-rows-2 items-center justify-center p-3">
@@ -155,7 +172,7 @@ export const Highlight: React.FC<Props> = ({
155172
<div className="grid grid-rows-2 flex-col items-center justify-center p-3">
156173
{recommendations.stockfish
157174
?.slice(0, 4)
158-
.map(({ move, cp }, index) => {
175+
.map(({ move, cp, winrate }, index) => {
159176
return (
160177
<button
161178
key={index}
@@ -171,6 +188,7 @@ export const Highlight: React.FC<Props> = ({
171188
move,
172189
source: 'stockfish',
173190
cp,
191+
winrate,
174192
})}
175193
>
176194
<p className="w-[42px] text-right font-mono text-xs md:text-sm">

src/components/Board/AnalysisMovesContainer.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,38 @@ function BlunderIcon() {
1818
)
1919
}
2020

21+
function InaccuracyIcon() {
22+
return (
23+
<div className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#f46d43] text-[10px] font-bold text-white">
24+
?
25+
</div>
26+
)
27+
}
28+
29+
function GoodMoveIcon() {
30+
return (
31+
<div className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#74add1] text-[10px] font-bold text-white">
32+
!
33+
</div>
34+
)
35+
}
36+
37+
function ExcellentMoveIcon() {
38+
return (
39+
<div className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#4575b4] text-[10px] font-bold text-white">
40+
!!
41+
</div>
42+
)
43+
}
44+
45+
function BestMoveIcon() {
46+
return (
47+
<div className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#1a9850] text-[10px] font-bold text-white">
48+
49+
</div>
50+
)
51+
}
52+
2153
export const AnalysisMovesContainer: React.FC<Props> = ({
2254
game,
2355
highlightIndices,
@@ -276,6 +308,11 @@ function VariationTree({
276308
>
277309
{node.moveNumber}. {node.turn === 'w' ? '...' : ''}
278310
{node.san}
311+
<span className="inline-flex items-center">
312+
{node.blunder && (
313+
<span className="ml-0.5 text-[8px] text-[#d73027]">!</span>
314+
)}
315+
</span>
279316
</span>
280317
{variations.length === 1 ? (
281318
<span className="inline">
@@ -338,6 +375,11 @@ function InlineChain({
338375
>
339376
{child.moveNumber}. {child.turn === 'w' ? '...' : ''}
340377
{child.san}
378+
<span className="inline-flex items-center">
379+
{child.blunder && (
380+
<span className="ml-0.5 text-[8px] text-[#d73027]">!</span>
381+
)}
382+
</span>
341383
</span>
342384
</Fragment>
343385
))}

src/hooks/useAnalysisController/useAnalysisController.ts

+67-34
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import {
1616
useLocalStorage,
1717
} from '..'
1818

19+
const COLORS = {
20+
good: ['#238b45', '#41ab5d', '#74c476', '#a1d99b', '#c7e9c0'],
21+
ok: ['#ec7014', '#feb24c', '#fed976', '#ffeda0', '#ffffcc'].reverse(),
22+
blunder: ['#cb181d', '#ef3b2c', '#fb6a4a', '#fc9272', '#fcbba1'].reverse(),
23+
}
24+
25+
// Constants for move classification based on winrate
26+
const BLUNDER_THRESHOLD = 0.1 // 10% winrate drop
27+
const INACCURACY_THRESHOLD = 0.05 // 5% winrate drop
1928
const MAIA_MODELS = [
2029
'maia_kdd_1100',
2130
'maia_kdd_1200',
@@ -28,22 +37,6 @@ const MAIA_MODELS = [
2837
'maia_kdd_1900',
2938
]
3039

31-
// const MAIA_COLORS = ['#fe7f6d', '#f08a4c', '#ecaa4f', '#eccd4f']
32-
const STOCKFISH_COLORS = [
33-
'#1a9850',
34-
'#91cf60',
35-
'#d9ef8b',
36-
'#fee08b',
37-
'#fc8d59',
38-
'#d73027',
39-
]
40-
41-
const COLORS = {
42-
good: ['#238b45', '#41ab5d', '#74c476', '#a1d99b', '#c7e9c0'],
43-
ok: ['#ec7014', '#feb24c', '#fed976', '#ffeda0', '#ffffcc'].reverse(),
44-
blunder: ['#cb181d', '#ef3b2c', '#fb6a4a', '#fc9272', '#fcbba1'].reverse(),
45-
}
46-
4740
export const useAnalysisController = (game: AnalyzedGame) => {
4841
const controller = useAnalysisGameController(
4942
game.tree as GameTree,
@@ -67,7 +60,6 @@ export const useAnalysisController = (game: AnalyzedGame) => {
6760
MAIA_MODELS[0],
6861
)
6962

70-
// Ensure the selected model is valid for the current context
7163
useEffect(() => {
7264
if (!MAIA_MODELS.includes(currentMaiaModel)) {
7365
setCurrentMaiaModel(MAIA_MODELS[0])
@@ -268,20 +260,40 @@ export const useAnalysisController = (game: AnalyzedGame) => {
268260
okMoves: { probability: 0, moves: [] },
269261
goodMoves: { probability: 0, moves: [] },
270262
}
271-
for (const [move, prob] of Object.entries(maia.policy)) {
272-
const loss = stockfish.cp_relative_vec[move]
273-
if (loss === undefined) continue
274-
const probability = prob * 100
275-
276-
if (loss >= -50) {
277-
goodMoveProbability += probability
278-
goodMoveChanceInfo.push({ move, probability })
279-
} else if (loss >= -150) {
280-
okMoveProbability += probability
281-
okMoveChanceInfo.push({ move, probability })
282-
} else {
283-
blunderMoveProbability += probability
284-
blunderMoveChanceInfo.push({ move, probability })
263+
264+
if (stockfish.winrate_loss_vec) {
265+
for (const [move, prob] of Object.entries(maia.policy)) {
266+
const winrate_loss = stockfish.winrate_loss_vec[move]
267+
if (winrate_loss === undefined) continue
268+
const probability = prob * 100
269+
270+
if (winrate_loss >= -INACCURACY_THRESHOLD) {
271+
goodMoveProbability += probability
272+
goodMoveChanceInfo.push({ move, probability })
273+
} else if (winrate_loss >= -BLUNDER_THRESHOLD) {
274+
okMoveProbability += probability
275+
okMoveChanceInfo.push({ move, probability })
276+
} else {
277+
blunderMoveProbability += probability
278+
blunderMoveChanceInfo.push({ move, probability })
279+
}
280+
}
281+
} else {
282+
for (const [move, prob] of Object.entries(maia.policy)) {
283+
const loss = stockfish.cp_relative_vec[move]
284+
if (loss === undefined) continue
285+
const probability = prob * 100
286+
287+
if (loss >= -50) {
288+
goodMoveProbability += probability
289+
goodMoveChanceInfo.push({ move, probability })
290+
} else if (loss >= -150) {
291+
okMoveProbability += probability
292+
okMoveChanceInfo.push({ move, probability })
293+
} else {
294+
blunderMoveProbability += probability
295+
blunderMoveChanceInfo.push({ move, probability })
296+
}
285297
}
286298
}
287299

@@ -350,10 +362,21 @@ export const useAnalysisController = (game: AnalyzedGame) => {
350362

351363
const moveRecommendations = useMemo(() => {
352364
if (!moveEvaluation) return {}
365+
366+
const isBlackTurn = controller.currentNode?.turn === 'b'
367+
353368
const recommendations: {
354369
maia?: { move: string; prob: number }[]
355-
stockfish?: { move: string; cp: number }[]
356-
} = {}
370+
stockfish?: {
371+
move: string
372+
cp: number
373+
winrate?: number
374+
winrate_loss?: number
375+
}[]
376+
isBlackTurn?: boolean
377+
} = {
378+
isBlackTurn,
379+
}
357380

358381
if (moveEvaluation?.maia) {
359382
const policy = moveEvaluation.maia.policy
@@ -367,16 +390,21 @@ export const useAnalysisController = (game: AnalyzedGame) => {
367390

368391
if (moveEvaluation?.stockfish) {
369392
const cp_vec = moveEvaluation.stockfish.cp_vec
393+
const winrate_vec = moveEvaluation.stockfish.winrate_vec || {}
394+
const winrate_loss_vec = moveEvaluation.stockfish.winrate_loss_vec || {}
395+
370396
const stockfish = Object.entries(cp_vec).map(([move, cp]) => ({
371397
move,
372398
cp,
399+
winrate: winrate_vec[move] || 0,
400+
winrate_loss: winrate_loss_vec[move] || 0,
373401
}))
374402

375403
recommendations.stockfish = stockfish
376404
}
377405

378406
return recommendations
379-
}, [moveEvaluation])
407+
}, [moveEvaluation, controller.currentNode])
380408

381409
const moveMap = useMemo(() => {
382410
if (!moveEvaluation?.maia || !moveEvaluation?.stockfish) {
@@ -387,8 +415,13 @@ export const useAnalysisController = (game: AnalyzedGame) => {
387415
Object.entries(moveEvaluation.maia.policy).slice(0, 3),
388416
)
389417

418+
// Get the Stockfish moves in their sorted order (best to worst for the current player)
390419
const stockfishMoves = Object.entries(moveEvaluation.stockfish.cp_vec)
420+
421+
// Top moves are the first 3 in the sorted order
391422
const topStockfish = Object.fromEntries(stockfishMoves.slice(0, 3))
423+
424+
// Worst moves are the last 2 in the sorted order
392425
const worstStockfish = Object.fromEntries(stockfishMoves.slice(-2))
393426

394427
const moves = Array.from(

0 commit comments

Comments
 (0)