Skip to content

Commit 20321b6

Browse files
feat: clean up play controller adding new moves
1 parent dae5d07 commit 20321b6

3 files changed

Lines changed: 96 additions & 77 deletions

File tree

src/hooks/usePlayController/usePlayMaiaController.ts

Lines changed: 31 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@ const computeTermination = (chess: Chess): Termination | undefined => {
3838
}
3939

4040
export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
41-
// Core game state
4241
const [gameTree, setGameTree] = useState<GameTree>(
4342
() => new GameTree(config.startFen || nullFen),
4443
)
45-
const [moveTimes, setMoveTimes] = useState<number[]>([])
44+
const [treeVersion, setTreeVersion] = useState<number>(0)
4645
const [resigned, setResigned] = useState<boolean>(false)
4746

48-
// Clock state
4947
const [baseMinutes, incrementSeconds] =
5048
config.timeControl == 'unlimited'
5149
? [0, 0]
@@ -56,16 +54,22 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
5654
const [blackClock, setBlackClock] = useState<number>(initialClockValue)
5755
const [lastMoveTime, setLastMoveTime] = useState<number>(0)
5856

59-
// Navigation state
6057
const [currentNode, setCurrentNode] = useState<GameNode | undefined>(() =>
6158
gameTree.getRoot(),
6259
)
6360
const [orientation, setOrientation] = useState<'white' | 'black'>(
6461
config.player,
6562
)
6663

67-
// Derived game state
68-
const moveList = useMemo(() => gameTree.toMoveArray(), [gameTree])
64+
const moveList = useMemo(
65+
() => gameTree.toMoveArray(),
66+
[gameTree, treeVersion],
67+
)
68+
69+
const moveTimes = useMemo(
70+
() => gameTree.toTimeArray(),
71+
[gameTree, treeVersion],
72+
)
6973

7074
const game: PlayedGame = useMemo(() => {
7175
const mainLine = gameTree.getMainLine()
@@ -80,7 +84,6 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
8084
} as Termination)
8185
: computeTermination(chess)
8286

83-
// Build moves array for compatibility
8487
const moves = []
8588
const rootNode = gameTree.getRoot()
8689
const rootChess = new Chess(rootNode.fen)
@@ -114,12 +117,11 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
114117
termination,
115118
turn: chess.turn() == 'b' ? 'black' : 'white',
116119
}
117-
}, [gameTree, resigned, whiteClock, blackClock, id])
120+
}, [gameTree, treeVersion, resigned, whiteClock, blackClock, id])
118121

119122
const toPlay: Color | null = game.termination ? null : game.turn
120123
const playerActive = toPlay == config.player
121124

122-
// Available moves and pieces
123125
const { availableMoves, pieces } = useMemo(() => {
124126
if (!currentNode) return { availableMoves: [], pieces: {} }
125127

@@ -146,9 +148,8 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
146148
}
147149

148150
return { availableMoves, pieces }
149-
}, [currentNode, playerActive, game.termination])
151+
}, [currentNode, playerActive, game.termination, treeVersion])
150152

151-
// Navigation helpers
152153
const goToNode = useCallback((node: GameNode) => setCurrentNode(node), [])
153154
const goToNextNode = useCallback(() => {
154155
if (currentNode?.mainChild) setCurrentNode(currentNode.mainChild)
@@ -161,13 +162,14 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
161162
[gameTree],
162163
)
163164

164-
const plyCount = useMemo(() => gameTree.getMainLine().length, [gameTree])
165+
const plyCount = useMemo(
166+
() => gameTree.getMainLine().length,
167+
[gameTree, treeVersion],
168+
)
165169

166-
// Clock management
167170
const updateClock = useCallback(
168171
(overrideTime: number | undefined = undefined): number => {
169172
if (moveList.length < 2) {
170-
setMoveTimes([...moveTimes, 0])
171173
return 0 // Clock does not start until first two moves made
172174
}
173175

@@ -187,22 +189,19 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
187189
}
188190
}
189191

190-
setMoveTimes([...moveTimes, elapsed])
191192
setLastMoveTime(now)
192193
return elapsed
193194
},
194195
[
195196
moveList.length,
196197
lastMoveTime,
197-
moveTimes,
198198
toPlay,
199199
whiteClock,
200200
blackClock,
201201
incrementSeconds,
202202
],
203203
)
204204

205-
// Game end by time
206205
useEffect(() => {
207206
if (
208207
playerActive &&
@@ -227,76 +226,39 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
227226
whiteClock,
228227
])
229228

230-
// Move handling
231229
const addMove = useCallback(
232230
(moveUci: string) => {
233-
const mainLine = gameTree.getMainLine()
234-
const lastNode = mainLine[mainLine.length - 1]
235-
const chess = new Chess(lastNode.fen)
236-
const result = chess.move(moveUci, { sloppy: true })
237-
238-
if (result) {
239-
const newTree = new GameTree(gameTree.getRoot().fen)
240-
// Rebuild tree with existing moves plus new move
241-
const existingMoves = gameTree.toMoveArray()
242-
let node = newTree.getRoot()
243-
244-
for (const move of [...existingMoves, moveUci]) {
245-
const tempChess = new Chess(node.fen)
246-
const tempResult = tempChess.move(move, { sloppy: true })
247-
if (tempResult) {
248-
node = newTree.addMainMove(
249-
node,
250-
tempChess.fen(),
251-
move,
252-
tempResult.san,
253-
)
254-
}
255-
}
256-
257-
setGameTree(newTree)
258-
setCurrentNode(node) // Move to the new position
231+
const newNode = gameTree.addMoveToMainLine(moveUci)
232+
if (newNode) {
233+
setCurrentNode(newNode)
234+
// Force re-render by incrementing tree version
235+
setTreeVersion((prev) => prev + 1)
259236
}
260237
},
261238
[gameTree],
262239
)
263240

264241
const addMoveWithTime = useCallback(
265242
(moveUci: string, moveTime: number) => {
266-
addMove(moveUci)
267-
setMoveTimes([...moveTimes, moveTime])
268-
},
269-
[addMove, moveTimes],
270-
)
271-
272-
// Legacy compatibility
273-
const setMoves = useCallback(
274-
(newMoves: string[]) => {
275-
const newTree = new GameTree(config.startFen || nullFen)
276-
let node = newTree.getRoot()
277-
278-
for (const move of newMoves) {
279-
const chess = new Chess(node.fen)
280-
const result = chess.move(move, { sloppy: true })
281-
if (result) {
282-
node = newTree.addMainMove(node, chess.fen(), move, result.san)
283-
}
243+
const newNode = gameTree.addMoveToMainLine(moveUci, moveTime)
244+
if (newNode) {
245+
setCurrentNode(newNode)
246+
// Force re-render by incrementing tree version
247+
setTreeVersion((prev) => prev + 1)
284248
}
285-
286-
setGameTree(newTree)
287-
setCurrentNode(node)
288249
},
289-
[config.startFen],
250+
[gameTree],
290251
)
291252

292253
const reset = () => {
293-
setGameTree(new GameTree(config.startFen || nullFen))
294-
setCurrentNode(gameTree.getRoot())
295-
setMoveTimes([])
254+
const newTree = new GameTree(config.startFen || nullFen)
255+
setGameTree(newTree)
256+
setCurrentNode(newTree.getRoot())
296257
setResigned(false)
297258
setLastMoveTime(0)
298259
setWhiteClock(initialClockValue)
299260
setBlackClock(initialClockValue)
261+
setTreeVersion((prev) => prev + 1)
300262
}
301263

302264
const makeMove = async (moveUci: string): Promise<void> => {
@@ -311,7 +273,6 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
311273
}
312274

313275
return {
314-
// Game state
315276
game,
316277
gameTree,
317278
currentNode,
@@ -342,8 +303,6 @@ export const usePlayMaiaController = (id: string, config: PlayGameConfig) => {
342303

343304
addMove,
344305
addMoveWithTime,
345-
setMoves,
346-
setMoveTimes,
347306
setResigned,
348307
reset,
349308
makeMove,

src/pages/play/maia.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const playStatsLoader = async () => {
2727
}
2828
}
2929

30-
const useVsMaiaPlayTreeController = (
30+
const useVsMaiaPlayController = (
3131
id: string,
3232
playGameConfig: PlayGameConfig,
3333
) => {
@@ -162,7 +162,7 @@ const PlayMaia: React.FC<Props> = ({
162162
playGameConfig,
163163
playAgain,
164164
}: Props) => {
165-
const controller = useVsMaiaPlayTreeController(id, playGameConfig)
165+
const controller = useVsMaiaPlayController(id, playGameConfig)
166166

167167
useEffect(() => {
168168
const handleKeyDown = (event: KeyboardEvent) => {

src/types/base/tree.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ export class GameTree {
8080
move: string,
8181
san: string,
8282
activeModel?: string,
83+
time?: number,
8384
): GameNode {
84-
return node.addChild(fen, move, san, true, activeModel)
85+
return node.addChild(fen, move, san, true, activeModel, time)
8586
}
8687

8788
addVariation(
@@ -90,11 +91,12 @@ export class GameTree {
9091
move: string,
9192
san: string,
9293
activeModel?: string,
94+
time?: number,
9395
): GameNode {
9496
if (node.findVariation(move)) {
9597
return node.findVariation(move) as GameNode
9698
}
97-
return node.addChild(fen, move, san, false, activeModel)
99+
return node.addChild(fen, move, san, false, activeModel, time)
98100
}
99101

100102
toMoveArray(): string[] {
@@ -106,6 +108,52 @@ export class GameTree {
106108
}
107109
return moves
108110
}
111+
112+
toTimeArray(): number[] {
113+
const times: number[] = []
114+
let node = this.root
115+
while (node.mainChild) {
116+
node = node.mainChild
117+
times.push(node.time || 0)
118+
}
119+
return times
120+
}
121+
122+
addMoveToMainLine(moveUci: string, time?: number): GameNode | null {
123+
const mainLine = this.getMainLine()
124+
const lastNode = mainLine[mainLine.length - 1]
125+
126+
const chess = new Chess(lastNode.fen)
127+
const result = chess.move(moveUci, { sloppy: true })
128+
129+
if (result) {
130+
return this.addMainMove(
131+
lastNode,
132+
chess.fen(),
133+
moveUci,
134+
result.san,
135+
undefined,
136+
time,
137+
)
138+
}
139+
140+
return null
141+
}
142+
143+
addMovesToMainLine(moves: string[], times?: number[]): GameNode | null {
144+
let currentNode: GameNode | null = null
145+
146+
for (let i = 0; i < moves.length; i++) {
147+
const move = moves[i]
148+
const time = times?.[i]
149+
currentNode = this.addMoveToMainLine(move, time)
150+
if (!currentNode) {
151+
return null
152+
}
153+
}
154+
155+
return currentNode
156+
}
109157
}
110158

111159
export class GameNode {
@@ -126,6 +174,7 @@ export class GameNode {
126174
private _bestMove: boolean
127175
private _moveNumber: number
128176
private _unlikelyGoodMove: boolean
177+
private _time: number | null
129178

130179
private static readonly BLUNDER_THRESHOLD =
131180
MOVE_CLASSIFICATION.BLUNDER_THRESHOLD
@@ -146,6 +195,7 @@ export class GameNode {
146195
san: string | null = null,
147196
parent: GameNode | null = null,
148197
mainline = true,
198+
time: number | null = null,
149199
) {
150200
this._fen = fen
151201
this._move = move
@@ -164,6 +214,7 @@ export class GameNode {
164214
this._turn = this.parseTurn(fen)
165215
this._check = fen.includes('+')
166216
this._moveNumber = this.parseMoveNumber(fen, this._turn)
217+
this._time = time
167218
}
168219

169220
get fen(): string {
@@ -217,6 +268,9 @@ export class GameNode {
217268
get unlikelyGoodMove(): boolean {
218269
return this._unlikelyGoodMove
219270
}
271+
get time(): number | null {
272+
return this._time
273+
}
220274

221275
private parseTurn(fen: string): Color {
222276
const parts = fen.split(' ')
@@ -314,8 +368,9 @@ export class GameNode {
314368
san: string,
315369
mainline = false,
316370
activeModel?: string,
371+
time?: number,
317372
): GameNode {
318-
const child = new GameNode(fen, move, san, this, mainline)
373+
const child = new GameNode(fen, move, san, this, mainline, time || null)
319374
this._children.push(child)
320375
if (mainline) {
321376
this._mainChild = child
@@ -433,4 +488,9 @@ export class GameNode {
433488
variation._mainline = true
434489
return true
435490
}
491+
492+
// Set the time for this move
493+
setTime(time: number): void {
494+
this._time = time
495+
}
436496
}

0 commit comments

Comments
 (0)