Skip to content

Commit 8dadd0f

Browse files
feat: migrate h&b to use tree structure
1 parent 06d59bd commit 8dadd0f

2 files changed

Lines changed: 313 additions & 309 deletions

File tree

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import { PieceSymbol } from 'chess.ts'
2+
import type { Key } from 'chessground/types'
3+
import { backOff } from 'exponential-backoff'
4+
import type { DrawShape } from 'chessground/draw'
5+
import { useCallback, useEffect, useMemo, useState } from 'react'
6+
7+
import { getGameMove, submitGameMove, getPlayPlayerStats } from 'src/api'
8+
import { PlayGameConfig } from 'src/types'
9+
import { useStats } from 'src/hooks/useStats'
10+
import { useChessSound } from 'src/hooks/useChessSound'
11+
import { usePlayTreeController } from './usePlayTreeController'
12+
13+
const brainStatsLoader = async () => {
14+
const stats = await getPlayPlayerStats()
15+
return {
16+
gamesPlayed: stats.brainGamesPlayed,
17+
gamesWon: stats.brainWon,
18+
rating: stats.brainElo,
19+
}
20+
}
21+
22+
const handStatsLoader = async () => {
23+
const stats = await getPlayPlayerStats()
24+
return {
25+
gamesPlayed: stats.handGamesPlayed,
26+
gamesWon: stats.handWon,
27+
rating: stats.handElo,
28+
}
29+
}
30+
31+
export const useHandBrainTreeController = (
32+
id: string,
33+
playGameConfig: PlayGameConfig,
34+
) => {
35+
const controller = usePlayTreeController(id, playGameConfig)
36+
const { playSound } = useChessSound()
37+
const isBrain = playGameConfig.isBrain
38+
39+
const [selectedPiece, setSelectedPiece] = useState<PieceSymbol | undefined>(
40+
undefined,
41+
)
42+
43+
const [brainMoves, setBrainMoves] = useState<string[]>([])
44+
45+
const [stats, incrementStats, updateRating] = useStats(
46+
isBrain ? brainStatsLoader : handStatsLoader,
47+
)
48+
49+
const movablePieceTypes = useMemo(() => {
50+
return new Set(controller.availableMoves.map((m) => m.piece))
51+
}, [controller.availableMoves])
52+
53+
const availableMoves = useMemo(() => {
54+
if (isBrain) {
55+
return [] // Brain may not make moves directly
56+
} else if (selectedPiece) {
57+
return controller.availableMoves.filter((m) => m.piece == selectedPiece)
58+
} else {
59+
return []
60+
}
61+
}, [isBrain, selectedPiece, controller.availableMoves])
62+
63+
useEffect(() => {
64+
if (
65+
controller.game.id &&
66+
!isBrain &&
67+
!selectedPiece &&
68+
controller.playerActive
69+
) {
70+
// Maia is brain
71+
let canceled = false
72+
73+
const maiaChoosePiece = async () => {
74+
const maiaMoves = await backOff(
75+
() =>
76+
getGameMove(
77+
controller.moveList,
78+
playGameConfig.maiaPartnerVersion,
79+
playGameConfig.startFen,
80+
),
81+
{
82+
jitter: 'full',
83+
},
84+
)
85+
const nextMove = maiaMoves['top_move']
86+
87+
const pieceType = controller.pieces[nextMove.substring(0, 2)].type
88+
89+
if (!canceled) {
90+
setSelectedPiece(pieceType)
91+
setBrainMoves([...brainMoves, pieceType])
92+
}
93+
}
94+
95+
maiaChoosePiece()
96+
return () => {
97+
canceled = true
98+
}
99+
}
100+
}, [
101+
brainMoves,
102+
controller.game.id,
103+
controller.moveList,
104+
controller.pieces,
105+
controller.playerActive,
106+
isBrain,
107+
playGameConfig.maiaPartnerVersion,
108+
playGameConfig.startFen,
109+
selectedPiece,
110+
])
111+
112+
const makeMove = useCallback(
113+
async (moveUci: string) => {
114+
controller.updateClock()
115+
controller.addMove(moveUci)
116+
setSelectedPiece(undefined)
117+
},
118+
[controller],
119+
)
120+
121+
useEffect(() => {
122+
let canceled = false
123+
124+
const makeMaiaMove = async () => {
125+
const maiaClock =
126+
(controller.player == 'white'
127+
? controller.blackClock
128+
: controller.whiteClock) / 1000
129+
const initialClock = controller.timeControl.includes('+')
130+
? parseInt(controller.timeControl.split('+')[0]) * 60
131+
: 0
132+
133+
const maiaMoves = await backOff(
134+
() =>
135+
getGameMove(
136+
controller.moveList,
137+
playGameConfig.maiaVersion,
138+
playGameConfig.startFen,
139+
null,
140+
playGameConfig.simulateMaiaTime ? initialClock : 0,
141+
playGameConfig.simulateMaiaTime ? maiaClock : 0,
142+
),
143+
{
144+
jitter: 'full',
145+
},
146+
)
147+
const nextMove = maiaMoves['top_move']
148+
const moveDelay = maiaMoves['move_delay']
149+
150+
if (canceled) {
151+
return
152+
}
153+
154+
setTimeout(
155+
() => {
156+
const moveTime = controller.updateClock()
157+
controller.addMoveWithTime(nextMove, moveTime)
158+
playSound(false)
159+
},
160+
playGameConfig.simulateMaiaTime ? moveDelay * 1000 : 0,
161+
)
162+
}
163+
164+
if (
165+
controller.game.id &&
166+
!controller.playerActive &&
167+
!controller.game.termination
168+
) {
169+
makeMaiaMove()
170+
return () => {
171+
canceled = true
172+
}
173+
}
174+
}, [
175+
controller.playerActive,
176+
controller.moveList,
177+
controller,
178+
playGameConfig.maiaVersion,
179+
playGameConfig.startFen,
180+
playGameConfig.simulateMaiaTime,
181+
playSound,
182+
])
183+
184+
const selectPiece = useCallback(
185+
async (piece: PieceSymbol) => {
186+
if (movablePieceTypes.has(piece)) {
187+
setSelectedPiece(piece)
188+
setBrainMoves([...brainMoves, piece])
189+
190+
const maiaMoves = await backOff(
191+
() =>
192+
getGameMove(
193+
controller.moveList,
194+
playGameConfig.maiaPartnerVersion,
195+
playGameConfig.startFen,
196+
piece,
197+
),
198+
{
199+
jitter: 'full',
200+
},
201+
)
202+
const nextMove = maiaMoves['top_move']
203+
makeMove(nextMove)
204+
playSound(false)
205+
}
206+
},
207+
[
208+
controller.moveList,
209+
makeMove,
210+
movablePieceTypes,
211+
playGameConfig.maiaPartnerVersion,
212+
playGameConfig.startFen,
213+
brainMoves,
214+
playSound,
215+
],
216+
)
217+
218+
const boardShapes: DrawShape[] = useMemo(() => {
219+
if (!isBrain && selectedPiece) {
220+
return Object.entries(controller.pieces)
221+
.filter(([, piece]) => piece.color == controller.player.substring(0, 1))
222+
.filter(([, piece]) => selectedPiece == piece.type)
223+
.map(([square]) => {
224+
return {
225+
orig: square as Key,
226+
brush: 'green',
227+
}
228+
})
229+
} else {
230+
return []
231+
}
232+
}, [isBrain, selectedPiece, controller.pieces, controller.player])
233+
234+
const reset = () => {
235+
setSelectedPiece(undefined)
236+
setBrainMoves([])
237+
controller.reset()
238+
}
239+
240+
useEffect(() => {
241+
const gameOverState = controller.game.termination?.type || 'not_over'
242+
243+
if (controller.moveList.length == 0 && gameOverState == 'not_over') {
244+
return
245+
}
246+
247+
const winner = controller.game.termination?.winner
248+
249+
const submitFn = async () => {
250+
const response = await backOff(
251+
() =>
252+
submitGameMove(
253+
controller.game.id,
254+
controller.moveList,
255+
controller.moveTimes,
256+
gameOverState,
257+
playGameConfig.isBrain ? 'brain' : 'hand',
258+
playGameConfig.startFen || undefined,
259+
winner,
260+
brainMoves,
261+
),
262+
{
263+
jitter: 'full',
264+
},
265+
)
266+
if (controller.game.termination) {
267+
const winner = controller.game.termination?.winner
268+
269+
updateRating(response.player_elo)
270+
incrementStats(1, winner == playGameConfig.player ? 1 : 0)
271+
}
272+
}
273+
submitFn()
274+
}, [
275+
controller.game.id,
276+
controller.moveList,
277+
controller.game.termination,
278+
controller.moveTimes,
279+
playGameConfig.startFen,
280+
playGameConfig.isBrain,
281+
brainMoves,
282+
playGameConfig.player,
283+
incrementStats,
284+
updateRating,
285+
])
286+
287+
return {
288+
...controller,
289+
availableMoves,
290+
makeMove,
291+
boardShapes,
292+
selectedPiece,
293+
movablePieceTypes,
294+
selectPiece,
295+
reset,
296+
stats,
297+
}
298+
}

0 commit comments

Comments
 (0)