Skip to content

Commit f82e2fa

Browse files
feat: top human move quality badges + show/hide badges setting
1 parent c59cc54 commit f82e2fa

7 files changed

Lines changed: 243 additions & 24 deletions

File tree

src/components/Analysis/BroadcastAnalysis.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ export const BroadcastAnalysis: React.FC<Props> = ({
4545
const [promotionFromTo, setPromotionFromTo] = useState<
4646
[string, string] | null
4747
>(null)
48+
const destinationBadges = useMemo(() => {
49+
if (
50+
!analysisController.showTopMoveBadges ||
51+
!analysisController.topHumanMoveBadge
52+
) {
53+
return []
54+
}
55+
56+
return [
57+
{
58+
square: analysisController.topHumanMoveBadge.square,
59+
classification: analysisController.topHumanMoveBadge.classification,
60+
},
61+
]
62+
}, [
63+
analysisController.showTopMoveBadges,
64+
analysisController.topHumanMoveBadge,
65+
])
4866

4967
useEffect(() => {
5068
setHoverArrow(null)
@@ -379,6 +397,7 @@ export const BroadcastAnalysis: React.FC<Props> = ({
379397
onPlayerMakeMove={onPlayerMakeMove}
380398
goToNode={analysisController.goToNode}
381399
gameTree={game.tree}
400+
destinationBadges={destinationBadges}
382401
/>
383402
{promotionFromTo ? (
384403
<PromotionOverlay
@@ -416,6 +435,8 @@ export const BroadcastAnalysis: React.FC<Props> = ({
416435
<ConfigurableScreens
417436
currentMaiaModel={analysisController.currentMaiaModel}
418437
setCurrentMaiaModel={analysisController.setCurrentMaiaModel}
438+
showTopMoveBadges={analysisController.showTopMoveBadges}
439+
setShowTopMoveBadges={analysisController.setShowTopMoveBadges}
419440
launchContinue={launchContinue}
420441
MAIA_MODELS={MAIA_MODELS}
421442
game={game}
@@ -479,6 +500,7 @@ export const BroadcastAnalysis: React.FC<Props> = ({
479500
onPlayerMakeMove={onPlayerMakeMove}
480501
goToNode={analysisController.goToNode}
481502
gameTree={game.tree}
503+
destinationBadges={destinationBadges}
482504
/>
483505
{promotionFromTo ? (
484506
<PromotionOverlay
@@ -518,6 +540,8 @@ export const BroadcastAnalysis: React.FC<Props> = ({
518540
<ConfigurableScreens
519541
currentMaiaModel={analysisController.currentMaiaModel}
520542
setCurrentMaiaModel={analysisController.setCurrentMaiaModel}
543+
showTopMoveBadges={analysisController.showTopMoveBadges}
544+
setShowTopMoveBadges={analysisController.setShowTopMoveBadges}
521545
launchContinue={launchContinue}
522546
MAIA_MODELS={MAIA_MODELS}
523547
game={game}

src/components/Analysis/ConfigurableScreens.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
interface Props {
1414
currentMaiaModel: string
1515
setCurrentMaiaModel: (model: string) => void
16+
showTopMoveBadges: boolean
17+
setShowTopMoveBadges: (show: boolean) => void
1618
launchContinue: () => void
1719
MAIA_MODELS: string[]
1820
game: AnalyzedGame
@@ -44,6 +46,8 @@ interface Props {
4446
export const ConfigurableScreens: React.FC<Props> = ({
4547
currentMaiaModel,
4648
setCurrentMaiaModel,
49+
showTopMoveBadges,
50+
setShowTopMoveBadges,
4751
launchContinue,
4852
MAIA_MODELS,
4953
game,
@@ -154,6 +158,8 @@ export const ConfigurableScreens: React.FC<Props> = ({
154158
<ConfigureAnalysis
155159
currentMaiaModel={currentMaiaModel}
156160
setCurrentMaiaModel={setCurrentMaiaModel}
161+
showTopMoveBadges={showTopMoveBadges}
162+
setShowTopMoveBadges={setShowTopMoveBadges}
157163
launchContinue={launchContinue}
158164
MAIA_MODELS={MAIA_MODELS}
159165
game={game}

src/components/Analysis/ConfigureAnalysis.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ContinueAgainstMaia } from 'src/components'
66
interface Props {
77
currentMaiaModel: string
88
setCurrentMaiaModel: (model: string) => void
9+
showTopMoveBadges: boolean
10+
setShowTopMoveBadges: (show: boolean) => void
911
launchContinue: () => void
1012
MAIA_MODELS: string[]
1113
game: AnalyzedGame
@@ -24,6 +26,8 @@ interface Props {
2426
export const ConfigureAnalysis: React.FC<Props> = ({
2527
currentMaiaModel,
2628
setCurrentMaiaModel,
29+
showTopMoveBadges,
30+
setShowTopMoveBadges,
2731
launchContinue,
2832
MAIA_MODELS,
2933
game,
@@ -59,6 +63,28 @@ export const ConfigureAnalysis: React.FC<Props> = ({
5963
</span>
6064
</div>
6165
</div>
66+
<div className="mt-1 flex w-full items-center justify-between rounded border border-glass-border bg-glass px-2.5 py-2">
67+
<div className="flex flex-col">
68+
<span className="text-xs text-white/90">Show board badges</span>
69+
<span className="text-[11px] text-white/60">
70+
Show ? / ?? marker on top human move destination
71+
</span>
72+
</div>
73+
<label
74+
htmlFor="analysis-top-move-badges-toggle"
75+
className="relative inline-flex cursor-pointer items-center"
76+
>
77+
<input
78+
id="analysis-top-move-badges-toggle"
79+
type="checkbox"
80+
checked={showTopMoveBadges}
81+
onChange={() => setShowTopMoveBadges(!showTopMoveBadges)}
82+
className="peer sr-only"
83+
/>
84+
<div className="peer h-5 w-9 rounded-full bg-white/10 after:absolute after:left-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-white/20 after:bg-white after:transition-all after:content-[''] peer-checked:bg-red-500/40 peer-checked:after:translate-x-full peer-checked:after:border-white peer-checked:after:bg-red-400"></div>
85+
<span className="sr-only">Toggle top move board badges</span>
86+
</label>
87+
</div>
6288
{onAnalyzeEntireGame && (
6389
<button
6490
onClick={onAnalyzeEntireGame}

src/components/Analysis/StreamAnalysis.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ export const StreamAnalysis: React.FC<Props> = ({
4848
const [promotionFromTo, setPromotionFromTo] = useState<
4949
[string, string] | null
5050
>(null)
51+
const destinationBadges = useMemo(() => {
52+
if (
53+
!analysisController.showTopMoveBadges ||
54+
!analysisController.topHumanMoveBadge
55+
) {
56+
return []
57+
}
58+
59+
return [
60+
{
61+
square: analysisController.topHumanMoveBadge.square,
62+
classification: analysisController.topHumanMoveBadge.classification,
63+
},
64+
]
65+
}, [
66+
analysisController.showTopMoveBadges,
67+
analysisController.topHumanMoveBadge,
68+
])
5169

5270
useEffect(() => {
5371
setHoverArrow(null)
@@ -381,6 +399,7 @@ export const StreamAnalysis: React.FC<Props> = ({
381399
onPlayerMakeMove={onPlayerMakeMove}
382400
goToNode={analysisController.goToNode}
383401
gameTree={game.tree}
402+
destinationBadges={destinationBadges}
384403
/>
385404
{promotionFromTo ? (
386405
<PromotionOverlay
@@ -428,6 +447,8 @@ export const StreamAnalysis: React.FC<Props> = ({
428447
<ConfigurableScreens
429448
currentMaiaModel={analysisController.currentMaiaModel}
430449
setCurrentMaiaModel={analysisController.setCurrentMaiaModel}
450+
showTopMoveBadges={analysisController.showTopMoveBadges}
451+
setShowTopMoveBadges={analysisController.setShowTopMoveBadges}
431452
launchContinue={launchContinue}
432453
MAIA_MODELS={MAIA_MODELS}
433454
game={game}
@@ -491,6 +512,7 @@ export const StreamAnalysis: React.FC<Props> = ({
491512
onPlayerMakeMove={onPlayerMakeMove}
492513
goToNode={analysisController.goToNode}
493514
gameTree={game.tree}
515+
destinationBadges={destinationBadges}
494516
/>
495517
{promotionFromTo ? (
496518
<PromotionOverlay
@@ -530,6 +552,8 @@ export const StreamAnalysis: React.FC<Props> = ({
530552
<ConfigurableScreens
531553
currentMaiaModel={analysisController.currentMaiaModel}
532554
setCurrentMaiaModel={analysisController.setCurrentMaiaModel}
555+
showTopMoveBadges={analysisController.showTopMoveBadges}
556+
setShowTopMoveBadges={analysisController.setShowTopMoveBadges}
533557
launchContinue={launchContinue}
534558
MAIA_MODELS={MAIA_MODELS}
535559
game={game}

src/components/Board/GameBoard.tsx

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@ import { defaults } from 'chessground/state'
55
import type { Key } from 'chessground/types'
66
import Chessground from '@react-chess/chessground'
77
import { BaseGame, GameNode, Color } from 'src/types'
8+
import { MoveClassificationIcon } from 'src/components/Common/MoveIcons'
89
import type { DrawBrushes, DrawShape } from 'chessground/draw'
910
import { useCallback, useMemo, Dispatch, SetStateAction } from 'react'
1011

12+
interface MoveClassification {
13+
blunder: boolean
14+
inaccuracy: boolean
15+
excellent: boolean
16+
bestMove: boolean
17+
}
18+
19+
interface DestinationBadge {
20+
square: string
21+
classification: MoveClassification
22+
}
23+
1124
interface Props {
1225
game?: BaseGame
1326
currentNode: GameNode
@@ -20,6 +33,25 @@ interface Props {
2033
brushes?: DrawBrushes
2134
goToNode?: (node: GameNode) => void
2235
gameTree?: any
36+
destinationBadges?: DestinationBadge[]
37+
}
38+
39+
const getBoardSquarePosition = (square: string, orientation: Color) => {
40+
if (!/^[a-h][1-8]$/.test(square)) {
41+
return null
42+
}
43+
44+
const file = square.charCodeAt(0) - 'a'.charCodeAt(0)
45+
const rank = parseInt(square[1], 10)
46+
47+
if (Number.isNaN(rank)) {
48+
return null
49+
}
50+
51+
return {
52+
left: orientation === 'white' ? file : 7 - file,
53+
top: orientation === 'white' ? 8 - rank : rank - 1,
54+
}
2355
}
2456

2557
export const GameBoard: React.FC<Props> = ({
@@ -34,8 +66,10 @@ export const GameBoard: React.FC<Props> = ({
3466
onPlayerMakeMove,
3567
setCurrentSquare,
3668
onSelectSquare,
69+
destinationBadges = [],
3770
}: Props) => {
3871
const { playMoveSound } = useSound()
72+
3973
const after = useCallback(
4074
(from: string, to: string) => {
4175
if (onPlayerMakeMove) onPlayerMakeMove([from, to])
@@ -113,31 +147,65 @@ export const GameBoard: React.FC<Props> = ({
113147
}, [currentNode, game, orientation])
114148

115149
return (
116-
<Chessground
117-
contained
118-
config={{
119-
movable: {
120-
free: false,
121-
dests: availableMoves as any,
150+
<div className="relative h-full w-full">
151+
<Chessground
152+
contained
153+
config={{
154+
movable: {
155+
free: false,
156+
dests: availableMoves as any,
157+
events: {
158+
after,
159+
},
160+
},
122161
events: {
123-
after,
162+
select: (key) => {
163+
onSelectSquare && onSelectSquare(key)
164+
setCurrentSquare && setCurrentSquare(key)
165+
},
124166
},
125-
},
126-
events: {
127-
select: (key) => {
128-
onSelectSquare && onSelectSquare(key)
129-
setCurrentSquare && setCurrentSquare(key)
167+
drawable: {
168+
autoShapes: shapes || [],
169+
brushes: { ...defaults().drawable.brushes, ...brushes },
130170
},
131-
},
132-
drawable: {
133-
autoShapes: shapes || [],
134-
brushes: { ...defaults().drawable.brushes, ...brushes },
135-
},
136-
fen: boardConfig.fen,
137-
lastMove: boardConfig.lastMove as Key[],
138-
check: boardConfig.check,
139-
orientation: boardConfig.orientation,
140-
}}
141-
/>
171+
fen: boardConfig.fen,
172+
lastMove: boardConfig.lastMove as Key[],
173+
check: boardConfig.check,
174+
orientation: boardConfig.orientation,
175+
}}
176+
/>
177+
{destinationBadges.length > 0 ? (
178+
<div className="pointer-events-none absolute inset-0 z-[40]">
179+
{destinationBadges.map((badge, index) => {
180+
const squarePosition = getBoardSquarePosition(
181+
badge.square,
182+
orientation,
183+
)
184+
if (!squarePosition) {
185+
return null
186+
}
187+
188+
return (
189+
<div
190+
key={`${badge.square}-${index}`}
191+
className="absolute h-[12.5%] w-[12.5%] overflow-hidden"
192+
style={{
193+
left: `${squarePosition.left * 12.5}%`,
194+
top: `${squarePosition.top * 12.5}%`,
195+
}}
196+
>
197+
<div className="absolute right-0 top-0">
198+
<MoveClassificationIcon
199+
classification={badge.classification}
200+
size="small"
201+
className="!ml-0 !h-3.5 !w-3.5 !text-[9px] pointer-events-none"
202+
/>
203+
</div>
204+
</div>
205+
)
206+
})}
207+
</div>
208+
) : null}
209+
</div>
142210
)
143211
}

0 commit comments

Comments
 (0)