Skip to content

Commit f8be3e5

Browse files
feat: add clocks to livestreamed game
1 parent ea0f470 commit f8be3e5

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

src/components/Analysis/StreamAnalysis.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
StockfishEvaluation,
1919
GameNode,
2020
} from 'src/types'
21-
import { StreamState } from 'src/hooks/useLichessStreamController'
21+
import { StreamState, ClockState } from 'src/hooks/useLichessStreamController'
2222
import { WindowSizeContext } from 'src/contexts'
2323
import { PlayerInfo } from 'src/components/Common/PlayerInfo'
2424
import { GameBoard } from 'src/components/Board/GameBoard'
@@ -33,6 +33,7 @@ import { MAIA_MODELS } from 'src/constants/common'
3333
interface Props {
3434
game: AnalyzedGame
3535
streamState: StreamState
36+
clockState: ClockState
3637
onReconnect: () => void
3738
onStopStream: () => void
3839
analysisController: any // This should be typed properly based on your analysis controller
@@ -41,6 +42,7 @@ interface Props {
4142
export const StreamAnalysis: React.FC<Props> = ({
4243
game,
4344
streamState,
45+
clockState,
4446
onReconnect,
4547
onStopStream,
4648
analysisController,
@@ -353,6 +355,21 @@ export const StreamAnalysis: React.FC<Props> = ({
353355
analysisController.orientation === 'white' ? 'black' : 'white'
354356
}
355357
termination={game.termination.winner}
358+
clock={
359+
clockState
360+
? analysisController.orientation === 'white'
361+
? {
362+
timeInSeconds: clockState.blackTime,
363+
isActive: clockState.activeColor === 'black',
364+
lastUpdateTime: clockState.lastUpdateTime,
365+
}
366+
: {
367+
timeInSeconds: clockState.whiteTime,
368+
isActive: clockState.activeColor === 'white',
369+
lastUpdateTime: clockState.lastUpdateTime,
370+
}
371+
: undefined
372+
}
356373
/>
357374
<div className="desktop-board-container relative flex aspect-square">
358375
<GameBoard
@@ -396,6 +413,21 @@ export const StreamAnalysis: React.FC<Props> = ({
396413
}
397414
termination={game.termination.winner}
398415
showArrowLegend={true}
416+
clock={
417+
clockState
418+
? analysisController.orientation === 'white'
419+
? {
420+
timeInSeconds: clockState.whiteTime,
421+
isActive: clockState.activeColor === 'white',
422+
lastUpdateTime: clockState.lastUpdateTime,
423+
}
424+
: {
425+
timeInSeconds: clockState.blackTime,
426+
isActive: clockState.activeColor === 'black',
427+
lastUpdateTime: clockState.lastUpdateTime,
428+
}
429+
: undefined
430+
}
399431
/>
400432
</div>
401433
<ConfigurableScreens

src/components/Common/PlayerInfo.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,55 @@ interface PlayerInfoProps {
44
rating?: number
55
termination?: string
66
showArrowLegend?: boolean
7+
clock?: {
8+
timeInSeconds: number
9+
isActive: boolean
10+
lastUpdateTime: number
11+
}
712
}
813

14+
import { useState, useEffect } from 'react'
15+
916
export const PlayerInfo: React.FC<PlayerInfoProps> = ({
1017
name,
1118
rating,
1219
color,
1320
termination,
1421
showArrowLegend = false,
22+
clock,
1523
}) => {
24+
const [currentTime, setCurrentTime] = useState<number>(
25+
clock?.timeInSeconds || 0,
26+
)
27+
28+
// Update clock countdown every second if this clock is active
29+
useEffect(() => {
30+
if (!clock || !clock.isActive) return
31+
32+
const interval = setInterval(() => {
33+
const now = Date.now()
34+
const elapsedSinceUpdate = (now - clock.lastUpdateTime) / 1000
35+
const newTime = Math.max(0, clock.timeInSeconds - elapsedSinceUpdate)
36+
setCurrentTime(newTime)
37+
}, 100) // Update every 100ms for smooth countdown
38+
39+
return () => clearInterval(interval)
40+
}, [clock])
41+
42+
// Update current time when clock prop changes (new move received)
43+
useEffect(() => {
44+
if (clock) {
45+
setCurrentTime(clock.timeInSeconds)
46+
}
47+
}, [clock?.timeInSeconds, clock?.lastUpdateTime])
48+
49+
// Format time as MM:SS
50+
const formatTime = (seconds: number): string => {
51+
const mins = Math.floor(seconds / 60)
52+
const secs = Math.floor(seconds % 60)
53+
return `${mins}:${secs.toString().padStart(2, '0')}`
54+
}
55+
1656
return (
1757
<div className="flex h-10 w-full items-center justify-between bg-background-1 px-4">
1858
<div className="flex items-center gap-1.5">
@@ -23,7 +63,16 @@ export const PlayerInfo: React.FC<PlayerInfoProps> = ({
2363
{name ?? 'Unknown'} {rating ? `(${rating})` : null}
2464
</p>
2565
</div>
26-
<div className="flex items-center gap-10">
66+
<div className="flex items-center gap-4">
67+
{clock && (
68+
<div
69+
className={`font-mono text-sm font-medium ${
70+
clock.isActive ? 'text-primary' : 'text-secondary'
71+
} ${currentTime < 60 ? 'text-red-400' : ''}`}
72+
>
73+
{formatTime(currentTime)}
74+
</div>
75+
)}
2776
{showArrowLegend && (
2877
<div className="flex flex-col items-start">
2978
<div className="flex items-center gap-0.5">

src/hooks/useLichessStreamController.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ export interface StreamState {
1414
gameStarted: boolean
1515
}
1616

17+
export interface ClockState {
18+
whiteTime: number // seconds
19+
blackTime: number // seconds
20+
activeColor: 'white' | 'black' | null
21+
lastUpdateTime: number // timestamp when clocks were last updated
22+
}
23+
1724
export interface LichessStreamController {
1825
game: AnalyzedGame | null
1926
streamState: StreamState
27+
clockState: ClockState
2028
startStream: (gameId: string) => void
2129
stopStream: () => void
2230
reconnect: () => void
@@ -31,6 +39,12 @@ export const useLichessStreamController = (): LichessStreamController => {
3139
error: null,
3240
gameStarted: false,
3341
})
42+
const [clockState, setClockState] = useState<ClockState>({
43+
whiteTime: 0,
44+
blackTime: 0,
45+
activeColor: null,
46+
lastUpdateTime: Date.now(),
47+
})
3448

3549
const abortController = useRef<AbortController | null>(null)
3650
const currentGameId = useRef<string | null>(null)
@@ -68,6 +82,14 @@ export const useLichessStreamController = (): LichessStreamController => {
6882
currentGameId.current = null
6983
streamMoves.current = []
7084
reconnectAttempts.current = 0
85+
86+
// Reset clock state
87+
setClockState({
88+
whiteTime: 0,
89+
blackTime: 0,
90+
activeColor: null,
91+
lastUpdateTime: Date.now(),
92+
})
7193
}, [clearReconnectTimeout])
7294

7395
const handleGameStart = useCallback((gameData: any) => {
@@ -96,6 +118,20 @@ export const useLichessStreamController = (): LichessStreamController => {
96118
// Add move to our tracking array
97119
streamMoves.current.push(moveData)
98120

121+
// Update clock state if clock data is available
122+
if (moveData.wc !== undefined && moveData.bc !== undefined) {
123+
const currentFen = moveData.fen
124+
// Determine whose turn it is from the FEN (w = white, b = black)
125+
const activeColor = currentFen?.includes(' w ') ? 'white' : 'black'
126+
127+
setClockState({
128+
whiteTime: moveData.wc,
129+
blackTime: moveData.bc,
130+
activeColor,
131+
lastUpdateTime: Date.now(),
132+
})
133+
}
134+
99135
// Update the game state with the new move
100136
setGame((currentGame) => {
101137
// If we don't have a game yet, we need to wait for the initial game state
@@ -265,10 +301,11 @@ export const useLichessStreamController = (): LichessStreamController => {
265301
() => ({
266302
game,
267303
streamState,
304+
clockState,
268305
startStream,
269306
stopStream,
270307
reconnect,
271308
}),
272-
[game, streamState], // Only depend on actual state, not functions
309+
[game, streamState, clockState], // Only depend on actual state, not functions
273310
)
274311
}

src/pages/analysis/stream/[gameId].tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ const StreamAnalysisPage: NextPage = () => {
175175
<StreamAnalysis
176176
game={streamController.game || dummyGame}
177177
streamState={streamController.streamState}
178+
clockState={streamController.clockState}
178179
onReconnect={streamController.reconnect}
179180
onStopStream={streamController.stopStream}
180181
analysisController={analysisController}

0 commit comments

Comments
 (0)