Skip to content

Commit cdaf284

Browse files
feat: add player material information
1 parent 8be27be commit cdaf284

5 files changed

Lines changed: 194 additions & 3 deletions

File tree

src/components/Analysis/BroadcastAnalysis.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ export const BroadcastAnalysis: React.FC<Props> = ({
333333
analysisController.orientation === 'white' ? 'black' : 'white'
334334
}
335335
termination={game.termination?.winner}
336+
currentFen={analysisController.currentNode?.fen}
337+
orientation={analysisController.orientation}
336338
clock={(() => {
337339
const clock =
338340
analysisController.orientation === 'white'
@@ -393,6 +395,8 @@ export const BroadcastAnalysis: React.FC<Props> = ({
393395
}
394396
termination={game.termination?.winner}
395397
showArrowLegend={true}
398+
currentFen={analysisController.currentNode?.fen}
399+
orientation={analysisController.orientation}
396400
clock={
397401
analysisController.orientation === 'white'
398402
? broadcastController.currentGame?.whiteClock

src/components/Analysis/StreamAnalysis.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ export const StreamAnalysis: React.FC<Props> = ({
339339
analysisController.orientation === 'white' ? 'black' : 'white'
340340
}
341341
termination={game.termination?.winner}
342+
currentFen={analysisController.currentNode?.fen}
343+
orientation={analysisController.orientation}
342344
clock={
343345
clockState
344346
? analysisController.orientation === 'white'
@@ -397,6 +399,8 @@ export const StreamAnalysis: React.FC<Props> = ({
397399
}
398400
termination={game.termination?.winner}
399401
showArrowLegend={true}
402+
currentFen={analysisController.currentNode?.fen}
403+
orientation={analysisController.orientation}
400404
clock={
401405
clockState
402406
? analysisController.orientation === 'white'

src/components/Common/PlayerInfo.tsx

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,174 @@ interface PlayerInfoProps {
44
rating?: number
55
termination?: string
66
showArrowLegend?: boolean
7+
currentFen?: string
8+
orientation?: 'white' | 'black'
79
clock?: {
810
timeInSeconds: number
911
isActive: boolean
1012
lastUpdateTime: number
1113
}
1214
}
1315

14-
import { useState, useEffect } from 'react'
16+
import { useState, useEffect, useMemo } from 'react'
17+
import { Chess } from 'chess.ts'
18+
19+
type PieceType = 'p' | 'n' | 'b' | 'r' | 'q' | 'k'
20+
type MaterialCount = Record<PieceType, number>
21+
22+
const PIECE_VALUES: Record<string, number> = {
23+
p: 1, // pawn
24+
n: 3, // knight
25+
b: 3, // bishop
26+
r: 5, // rook
27+
q: 9, // queen
28+
k: 0, // king (not counted)
29+
}
30+
31+
const STARTING_MATERIAL: { white: MaterialCount; black: MaterialCount } = {
32+
white: { p: 8, n: 2, b: 2, r: 2, q: 1, k: 1 },
33+
black: { p: 8, n: 2, b: 2, r: 2, q: 1, k: 1 },
34+
}
35+
36+
const calculateCapturedPieces = (fen?: string) => {
37+
if (!fen) return { white: {}, black: {} }
38+
39+
const chess = new Chess(fen)
40+
const board = chess.board()
41+
42+
// Count current pieces on board
43+
const currentMaterial: { white: MaterialCount; black: MaterialCount } = {
44+
white: { p: 0, n: 0, b: 0, r: 0, q: 0, k: 0 },
45+
black: { p: 0, n: 0, b: 0, r: 0, q: 0, k: 0 },
46+
}
47+
48+
for (const row of board) {
49+
for (const square of row) {
50+
if (square) {
51+
const piece = square.type.toLowerCase() as PieceType
52+
const color = square.color === 'w' ? 'white' : 'black'
53+
currentMaterial[color][piece]++
54+
}
55+
}
56+
}
57+
58+
// Calculate captured pieces (starting - current)
59+
const captured = {
60+
white: {} as Record<string, number>,
61+
black: {} as Record<string, number>,
62+
}
63+
64+
for (const piece of Object.keys(STARTING_MATERIAL.white) as PieceType[]) {
65+
const whiteCaptured =
66+
STARTING_MATERIAL.white[piece] - currentMaterial.white[piece]
67+
const blackCaptured =
68+
STARTING_MATERIAL.black[piece] - currentMaterial.black[piece]
69+
70+
if (whiteCaptured > 0) captured.white[piece] = whiteCaptured
71+
if (blackCaptured > 0) captured.black[piece] = blackCaptured
72+
}
73+
74+
return captured
75+
}
76+
77+
const calculateMaterialAdvantage = (
78+
fen?: string,
79+
): { white: number; black: number } => {
80+
if (!fen) return { white: 0, black: 0 }
81+
82+
const chess = new Chess(fen)
83+
const board = chess.board()
84+
85+
let whiteTotal = 0
86+
let blackTotal = 0
87+
88+
for (const row of board) {
89+
for (const square of row) {
90+
if (square) {
91+
const piece = square.type.toLowerCase()
92+
const value = PIECE_VALUES[piece] || 0
93+
if (square.color === 'w') {
94+
whiteTotal += value
95+
} else {
96+
blackTotal += value
97+
}
98+
}
99+
}
100+
}
101+
102+
return { white: whiteTotal, black: blackTotal }
103+
}
15104

16105
export const PlayerInfo: React.FC<PlayerInfoProps> = ({
17106
name,
18107
rating,
19108
color,
20109
termination,
21110
showArrowLegend = false,
111+
currentFen,
112+
orientation = 'white',
22113
clock,
23114
}) => {
24115
const [currentTime, setCurrentTime] = useState<number>(
25116
clock?.timeInSeconds || 0,
26117
)
27118

119+
// Calculate captured pieces and material advantage
120+
const capturedPieces = useMemo(
121+
() => calculateCapturedPieces(currentFen),
122+
[currentFen],
123+
)
124+
const materialAdvantage = useMemo(
125+
() => calculateMaterialAdvantage(currentFen),
126+
[currentFen],
127+
)
128+
129+
// Get pieces captured by this player (pieces of opposite color that were captured)
130+
const myCapturedPieces =
131+
color === 'white' ? capturedPieces.black : capturedPieces.white
132+
133+
// Calculate score advantage for this player
134+
const myAdvantage =
135+
materialAdvantage[color as 'white' | 'black'] -
136+
materialAdvantage[color === 'white' ? 'black' : 'white']
137+
138+
// Map chess pieces to Material UI icons
139+
const getPieceIcon = (piece: string): string => {
140+
const iconMap: Record<string, string> = {
141+
p: 'chess_pawn',
142+
n: 'chess_knight',
143+
b: 'chess_bishop',
144+
r: 'chess_rook',
145+
q: 'chess', // queen uses 'chess' icon
146+
}
147+
return iconMap[piece] || 'chess'
148+
}
149+
150+
// Render captured pieces
151+
const renderCapturedPieces = () => {
152+
const pieces: React.JSX.Element[] = []
153+
154+
// Order pieces by value (lowest to highest)
155+
const orderedPieces = ['p', 'n', 'b', 'r', 'q']
156+
157+
orderedPieces.forEach((piece) => {
158+
const count = myCapturedPieces[piece] || 0
159+
for (let i = 0; i < count; i++) {
160+
pieces.push(
161+
<span
162+
key={`${piece}-${i}`}
163+
className="material-symbols-outlined text-sm text-secondary"
164+
title={`${piece === 'p' ? 'pawn' : piece === 'n' ? 'knight' : piece === 'b' ? 'bishop' : piece === 'r' ? 'rook' : 'queen'}`}
165+
>
166+
{getPieceIcon(piece)}
167+
</span>,
168+
)
169+
}
170+
})
171+
172+
return pieces
173+
}
174+
28175
// Update clock countdown every second if this clock is active
29176
useEffect(() => {
30177
if (!clock || !clock.isActive) return
@@ -83,6 +230,24 @@ export const PlayerInfo: React.FC<PlayerInfoProps> = ({
83230
</div>
84231
</div>
85232
)}
233+
234+
{/* Captured pieces and material advantage */}
235+
{currentFen && (
236+
<div className="flex items-center gap-2">
237+
{/* Captured pieces icons */}
238+
<div className="flex items-center gap-0.5">
239+
{renderCapturedPieces()}
240+
</div>
241+
242+
{/* Material advantage score */}
243+
{myAdvantage !== 0 && (
244+
<span className="text-xs font-medium text-primary">
245+
+{Math.abs(myAdvantage)}
246+
</span>
247+
)}
248+
</div>
249+
)}
250+
86251
{clock && (
87252
<div
88253
className={`flex items-center px-2 py-1 ${

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,8 @@ const Analysis: React.FC<Props> = ({
705705
}
706706
color={controller.orientation === 'white' ? 'black' : 'white'}
707707
termination={analyzedGame.termination?.winner}
708+
currentFen={controller.currentNode?.fen}
709+
orientation={controller.orientation}
708710
/>
709711
<div className="desktop-board-container relative flex aspect-square">
710712
<GameBoard
@@ -776,6 +778,8 @@ const Analysis: React.FC<Props> = ({
776778
color={controller.orientation === 'white' ? 'white' : 'black'}
777779
termination={analyzedGame.termination?.winner}
778780
showArrowLegend={true}
781+
currentFen={controller.currentNode?.fen}
782+
orientation={controller.orientation}
779783
/>
780784
</div>
781785
<ConfigurableScreens

src/pages/openings/index.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,12 @@ const OpeningsPage: NextPage = () => {
548548
{/* Center - Board */}
549549
<div className="desktop-middle-column-container flex flex-col gap-2">
550550
<div className="flex w-full flex-col overflow-hidden rounded">
551-
<PlayerInfo name={topPlayer.name} color={topPlayer.color} />
551+
<PlayerInfo
552+
name={topPlayer.name}
553+
color={topPlayer.color}
554+
currentFen={controller.currentNode?.fen}
555+
orientation={controller.orientation}
556+
/>
552557
<div className="desktop-board-container relative flex aspect-square">
553558
<GameBoard
554559
currentNode={controller.currentNode}
@@ -578,6 +583,8 @@ const OpeningsPage: NextPage = () => {
578583
name={bottomPlayer.name}
579584
color={bottomPlayer.color}
580585
showArrowLegend={controller.analysisEnabled}
586+
currentFen={controller.currentNode?.fen}
587+
orientation={controller.orientation}
581588
/>
582589
</div>
583590

@@ -691,7 +698,12 @@ const OpeningsPage: NextPage = () => {
691698

692699
{/* Board Section */}
693700
<div className="flex w-full flex-col">
694-
<PlayerInfo name={topPlayer.name} color={topPlayer.color} />
701+
<PlayerInfo
702+
name={topPlayer.name}
703+
color={topPlayer.color}
704+
currentFen={controller.currentNode?.fen}
705+
orientation={controller.orientation}
706+
/>
695707
<div className="relative flex aspect-square h-[100vw] w-screen">
696708
<GameBoard
697709
currentNode={controller.currentNode}
@@ -721,6 +733,8 @@ const OpeningsPage: NextPage = () => {
721733
name={bottomPlayer.name}
722734
color={bottomPlayer.color}
723735
showArrowLegend={controller.analysisEnabled}
736+
currentFen={controller.currentNode?.fen}
737+
orientation={controller.orientation}
724738
/>
725739
</div>
726740

0 commit comments

Comments
 (0)