Skip to content

Commit 5018803

Browse files
feat: added board description + exclam icon for surprising good moves
1 parent 859f546 commit 5018803

6 files changed

Lines changed: 250 additions & 26 deletions

File tree

src/components/Analysis/Highlight.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export const Highlight: React.FC<Props> = ({
220220
})}
221221
</div>
222222
</div>
223-
<div className="flex flex-col items-start justify-start gap-1 bg-background-1/80 px-3 py-1.5 text-sm">
223+
<div className="flex flex-col items-start justify-start gap-1 bg-background-1/80 px-3 pb-3 text-sm md:py-1.5">
224224
<AnimatePresence mode="wait">
225225
{boardDescription ? (
226226
<motion.div

src/components/Board/AnalysisMovesContainer.tsx

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React, { useContext, useMemo, Fragment, useEffect } from 'react'
44
import { AnalysisGameControllerContext, WindowSizeContext } from 'src/contexts'
55
import { GameNode, AnalyzedGame, Termination, ClientBaseGame } from 'src/types'
6+
import { Tooltip } from 'react-tooltip'
67

78
interface Props {
89
game: ClientBaseGame | AnalyzedGame
@@ -12,9 +13,21 @@ interface Props {
1213

1314
function BlunderIcon() {
1415
return (
15-
<div className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#d73027] text-[10px] font-bold text-white">
16-
!
17-
</div>
16+
<>
17+
<div
18+
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#d73027] text-[10px] font-bold text-white"
19+
data-tooltip-id="blunder-tooltip"
20+
>
21+
!
22+
</div>
23+
<Tooltip
24+
id="blunder-tooltip"
25+
content="Critical Mistake"
26+
place="top"
27+
delayShow={300}
28+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
29+
/>
30+
</>
1831
)
1932
}
2033

@@ -50,6 +63,26 @@ function BestMoveIcon() {
5063
)
5164
}
5265

66+
function UnlikelyGoodMoveIcon() {
67+
return (
68+
<>
69+
<div
70+
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-[#2ca25f] text-[10px] font-bold text-white"
71+
data-tooltip-id="unlikely-good-tooltip"
72+
>
73+
!
74+
</div>
75+
<Tooltip
76+
id="unlikely-good-tooltip"
77+
content="Surprising Strong Move"
78+
place="top"
79+
delayShow={300}
80+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
81+
/>
82+
</>
83+
)
84+
}
85+
5386
export const AnalysisMovesContainer: React.FC<Props> = ({
5487
game,
5588
highlightIndices,
@@ -181,6 +214,7 @@ export const AnalysisMovesContainer: React.FC<Props> = ({
181214
>
182215
<span>{pair.whiteMove.san ?? pair.whiteMove.move}</span>
183216
{pair.whiteMove.blunder && <BlunderIcon />}
217+
{pair.whiteMove.unlikelyGoodMove && <UnlikelyGoodMoveIcon />}
184218
</div>
185219
)}
186220
{pair.blackMove && (
@@ -194,6 +228,7 @@ export const AnalysisMovesContainer: React.FC<Props> = ({
194228
>
195229
<span>{pair.blackMove.san ?? pair.blackMove.move}</span>
196230
{pair.blackMove.blunder && <BlunderIcon />}
231+
{pair.blackMove.unlikelyGoodMove && <UnlikelyGoodMoveIcon />}
197232
</div>
198233
)}
199234
</React.Fragment>
@@ -232,6 +267,7 @@ export const AnalysisMovesContainer: React.FC<Props> = ({
232267
>
233268
{whiteNode?.san ?? whiteNode?.move}
234269
{whiteNode?.blunder && <BlunderIcon />}
270+
{whiteNode?.unlikelyGoodMove && <UnlikelyGoodMoveIcon />}
235271
</div>
236272
{whiteNode?.getVariations().length ? (
237273
<FirstVariation
@@ -250,6 +286,7 @@ export const AnalysisMovesContainer: React.FC<Props> = ({
250286
>
251287
{blackNode?.san ?? blackNode?.move}
252288
{blackNode?.blunder && <BlunderIcon />}
289+
{blackNode?.unlikelyGoodMove && <UnlikelyGoodMoveIcon />}
253290
</div>
254291
{blackNode?.getVariations().length ? (
255292
<FirstVariation
@@ -336,7 +373,20 @@ function VariationTree({
336373
{node.san}
337374
<span className="inline-flex items-center">
338375
{node.blunder && (
339-
<span className="ml-0.5 text-[8px] text-[#d73027]">!</span>
376+
<span
377+
className="ml-0.5 text-[8px] text-[#d73027]"
378+
data-tooltip-id="variation-blunder-tooltip"
379+
>
380+
!
381+
</span>
382+
)}
383+
{node.unlikelyGoodMove && (
384+
<span
385+
className="ml-0.5 text-[8px] text-[#2ca25f]"
386+
data-tooltip-id="variation-unlikely-tooltip"
387+
>
388+
!?
389+
</span>
340390
)}
341391
</span>
342392
</span>
@@ -362,6 +412,20 @@ function VariationTree({
362412
))}
363413
</ul>
364414
) : null}
415+
<Tooltip
416+
id="variation-blunder-tooltip"
417+
content="Critical Mistake"
418+
place="top"
419+
delayShow={300}
420+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
421+
/>
422+
<Tooltip
423+
id="variation-unlikely-tooltip"
424+
content="Surprising Strong Move"
425+
place="top"
426+
delayShow={300}
427+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
428+
/>
365429
</li>
366430
)
367431
}
@@ -403,13 +467,40 @@ function InlineChain({
403467
{child.san}
404468
<span className="inline-flex items-center">
405469
{child.blunder && (
406-
<span className="ml-0.5 text-[8px] text-[#d73027]">!</span>
470+
<span
471+
className="ml-0.5 text-[8px] text-[#d73027]"
472+
data-tooltip-id="inline-blunder-tooltip"
473+
>
474+
!
475+
</span>
476+
)}
477+
{child.unlikelyGoodMove && (
478+
<span
479+
className="ml-0.5 text-[8px] text-[#2ca25f]"
480+
data-tooltip-id="inline-unlikely-tooltip"
481+
>
482+
!?
483+
</span>
407484
)}
408485
</span>
409486
</span>
410487
</Fragment>
411488
))}
412489
</span>
490+
<Tooltip
491+
id="inline-blunder-tooltip"
492+
content="Critical Mistake"
493+
place="top"
494+
delayShow={300}
495+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
496+
/>
497+
<Tooltip
498+
id="inline-unlikely-tooltip"
499+
content="Surprising Strong Move"
500+
place="top"
501+
delayShow={300}
502+
className="z-50 !bg-background-2 !px-2 !py-1 !text-xs"
503+
/>
413504
{current.getVariations().length > 1 && (
414505
<ul className="tree-ul list-none">
415506
{current.getVariations().map((child) => (

src/components/Board/GameplayInterface.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export const GameplayInterface: React.FC<React.PropsWithChildren<Props>> = (
163163

164164
const desktopLayout = (
165165
<>
166-
<div className="flex h-full flex-1 flex-col justify-center gap-1">
166+
<div className="flex h-full flex-1 flex-col justify-center gap-1 py-5 md:py-10">
167167
<div className="flex w-full flex-row items-center justify-center gap-1">
168168
<div
169169
style={{

src/hooks/useAnalysisController/useAnalysisController.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ export const useAnalysisController = (game: AnalyzedGame) => {
8989
maiaEval[model] = result[index]
9090
})
9191

92-
controller.currentNode.addMaiaAnalysis(maiaEval)
92+
controller.currentNode.addMaiaAnalysis(maiaEval, currentMaiaModel)
9393
setAnalysisState((state) => state + 1)
9494
})()
95-
}, [maiaStatus, controller.currentNode, analysisState])
95+
}, [maiaStatus, controller.currentNode, analysisState, currentMaiaModel])
9696

9797
useEffect(() => {
9898
if (!controller.currentNode) return
@@ -115,7 +115,10 @@ export const useAnalysisController = (game: AnalyzedGame) => {
115115
stopEvaluation()
116116
break
117117
}
118-
controller.currentNode.addStockfishAnalysis(evaluation)
118+
controller.currentNode.addStockfishAnalysis(
119+
evaluation,
120+
currentMaiaModel,
121+
)
119122
setAnalysisState((state) => state + 1)
120123
}
121124
})()
@@ -124,7 +127,13 @@ export const useAnalysisController = (game: AnalyzedGame) => {
124127
return () => {
125128
stopEvaluation()
126129
}
127-
}, [controller.currentNode, game.type, streamEvaluations, stopEvaluation])
130+
}, [
131+
controller.currentNode,
132+
game.type,
133+
streamEvaluations,
134+
stopEvaluation,
135+
currentMaiaModel,
136+
])
128137

129138
const moves = useMemo(() => {
130139
if (!controller.currentNode) return new Map<string, string[]>()
@@ -551,6 +560,13 @@ export const useAnalysisController = (game: AnalyzedGame) => {
551560
const cpAdvantage = cp > 0 ? 'White' : cp < 0 ? 'Black' : 'Neither player'
552561
const topStockfishMove = topStockfishMoves[0]
553562

563+
// Calculate winrate for more nuanced description (using centipawn to approximate winrate)
564+
// Formula approximates winrate from CP value: 1/(1+10^(-cp/400))
565+
const rawWinrate = 1 / (1 + Math.pow(10, -cp / 400))
566+
const winrate = Math.max(0.01, Math.min(0.99, rawWinrate)) // Clamp between 1% and 99%
567+
const toMoveWinrate = isBlackTurn ? 1 - winrate : winrate
568+
const toMoveAdvantage = toMoveWinrate > 0.5
569+
554570
// Check if top Maia move matches top Stockfish move
555571
const maiaMatchesStockfish = topMaiaMove[0] === topStockfishMove[0]
556572

@@ -638,25 +654,49 @@ export const useAnalysisController = (game: AnalyzedGame) => {
638654
let evaluation = ''
639655
let suggestion = ''
640656

641-
// Evaluation description
657+
// Evaluation description that considers whose turn it is
642658
if (isOverwhelming) {
643-
evaluation = `${cpAdvantage} is completely winning and should convert without difficulty.`
659+
if (cpAdvantage === playerColor) {
660+
evaluation = `${playerColor} has a completely winning position with a ${Math.round(toMoveWinrate * 100)}% win probability.`
661+
} else {
662+
evaluation = `${playerColor} faces a nearly lost position with only a ${Math.round(toMoveWinrate * 100)}% win probability.`
663+
}
644664
} else if (cp === 0) {
645665
evaluation = isBalancedButComplex
646666
? 'The position is balanced but filled with complications.'
647667
: 'The position is completely equal.'
648668
} else if (absCP < 30) {
649-
evaluation = `The evaluation is almost perfectly balanced with only the slightest edge for ${cpAdvantage}.`
669+
evaluation = `The evaluation is almost perfectly balanced with only the slightest edge ${cpAdvantage === playerColor ? 'for' : 'against'} ${playerColor}.`
650670
} else if (absCP < 80) {
651-
evaluation = `${cpAdvantage} has a slight but tangible advantage in this position.`
671+
if (cpAdvantage === playerColor) {
672+
evaluation = `${playerColor} has a slight but tangible advantage with a win probability of ${Math.round(toMoveWinrate * 100)}%.`
673+
} else {
674+
evaluation = `${playerColor} faces a slight disadvantage with a win probability of ${Math.round(toMoveWinrate * 100)}%.`
675+
}
652676
} else if (absCP < 150) {
653-
evaluation = `${cpAdvantage} has a clear positional advantage that could be decisive with careful play.`
677+
if (cpAdvantage === playerColor) {
678+
evaluation = `${playerColor} has a clear positional advantage that could be decisive with careful play.`
679+
} else {
680+
evaluation = `${playerColor} must play accurately as ${opponent} holds a clear positional advantage.`
681+
}
654682
} else if (absCP < 300) {
655-
evaluation = `${cpAdvantage} has a significant advantage that should be convertible with proper technique.`
683+
if (cpAdvantage === playerColor) {
684+
evaluation = `${playerColor} has a significant advantage (${Math.round(toMoveWinrate * 100)}% win rate) that should be convertible with proper technique.`
685+
} else {
686+
evaluation = `${playerColor} faces a difficult position as ${opponent} has a significant advantage (${Math.round((1 - toMoveWinrate) * 100)}% win rate).`
687+
}
656688
} else if (absCP < 500) {
657-
evaluation = `${cpAdvantage} has a winning position that only requires avoiding major blunders.`
689+
if (cpAdvantage === playerColor) {
690+
evaluation = `${playerColor} is winning and only needs to avoid major blunders to convert.`
691+
} else {
692+
evaluation = `${playerColor} is in serious trouble and needs to find resilient defensive moves.`
693+
}
658694
} else {
659-
evaluation = `${cpAdvantage} has a completely winning position that should be straightforward to convert.`
695+
if (cpAdvantage === playerColor) {
696+
evaluation = `${playerColor} has a completely winning position with a ${Math.round(toMoveWinrate * 100)}% win probability.`
697+
} else {
698+
evaluation = `${playerColor} faces a nearly lost position with only a ${Math.round(toMoveWinrate * 100)}% win probability.`
699+
}
660700
}
661701

662702
// Suggestion/description of move quality

src/pages/analysis/[...id].tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ const Analysis: React.FC<Props> = ({
391391
newFen,
392392
moveString,
393393
san,
394+
currentMaiaModel,
394395
)
395396
controller.goToNode(newVariation)
396397
}

0 commit comments

Comments
 (0)