1- import { useEffect } from 'react'
1+ import { useCallback , useContext , useEffect } from 'react'
22import { Chess } from 'chess.ts'
33import { PlayGameConfig } from 'src/types'
44import { backOff } from 'exponential-backoff'
@@ -7,6 +7,26 @@ import { usePlayController } from 'src/hooks/usePlayController'
77import { fetchGameMove , logGameMove , fetchPlayPlayerStats } from 'src/api'
88import { useSound } from 'src/hooks/useSound'
99import { safeUpdateRating } from 'src/lib/ratingUtils'
10+ import { MaiaEngineContext } from 'src/contexts'
11+
12+ const MAIA_VALUE_HEAD_DEBUG_KEY = 'maia.valueHeadDebug'
13+
14+ const isTruthy = ( value : string | null | undefined ) : boolean => {
15+ if ( ! value ) return false
16+ return [ '1' , 'true' , 'yes' , 'on' ] . includes ( value . toLowerCase ( ) )
17+ }
18+
19+ const isMaiaValueHeadDebugEnabled = ( ) : boolean => {
20+ if ( typeof window === 'undefined' ) {
21+ return false
22+ }
23+
24+ try {
25+ return isTruthy ( window . localStorage . getItem ( MAIA_VALUE_HEAD_DEBUG_KEY ) )
26+ } catch {
27+ return false
28+ }
29+ }
1030
1131const playStatsLoader = async ( ) => {
1232 const stats = await fetchPlayPlayerStats ( )
@@ -25,6 +45,126 @@ export const useVsMaiaPlayController = (
2545 const controller = usePlayController ( id , playGameConfig )
2646 const [ stats , incrementStats , updateRating ] = useStats ( playStatsLoader )
2747 const { playMoveSound } = useSound ( )
48+ const maiaEngine = useContext ( MaiaEngineContext )
49+ const valueHeadModel =
50+ playGameConfig . maiaMoveSelectionMode === 'value_head'
51+ ? maiaEngine . maia
52+ : null
53+ const valueHeadStatus =
54+ playGameConfig . maiaMoveSelectionMode === 'value_head'
55+ ? maiaEngine . status
56+ : null
57+ const playerRatingForValueHead = stats . rating ?? 1500
58+ const valueHeadDebugEnabled = isMaiaValueHeadDebugEnabled ( )
59+
60+ const selectValueHeadMove = useCallback ( async ( ) => {
61+ if (
62+ ! controller . currentNode ||
63+ valueHeadStatus !== 'ready' ||
64+ ! valueHeadModel
65+ ) {
66+ if ( valueHeadDebugEnabled ) {
67+ console . log ( '[Maia value-head debug] selector unavailable' , {
68+ hasCurrentNode : ! ! controller . currentNode ,
69+ valueHeadStatus,
70+ hasModel : ! ! valueHeadModel ,
71+ } )
72+ }
73+ return null
74+ }
75+
76+ const currentFen = controller . currentNode . fen
77+ const chess = new Chess ( currentFen )
78+ const legalMoves = chess . moves ( { verbose : true } ) as Array < {
79+ from : string
80+ to : string
81+ promotion ?: string
82+ } >
83+
84+ if ( legalMoves . length === 0 ) {
85+ return null
86+ }
87+
88+ const candidateMoves = legalMoves . map (
89+ ( move ) => `${ move . from } ${ move . to } ${ move . promotion ?? '' } ` ,
90+ )
91+ const candidateBoards = candidateMoves . map ( ( moveUci ) => {
92+ const board = new Chess ( currentFen )
93+ board . move ( moveUci , { sloppy : true } )
94+ return board . fen ( )
95+ } )
96+
97+ const maiaRating = parseInt (
98+ playGameConfig . maiaVersion . replace ( 'maia_kdd_' , '' ) ,
99+ 10 ,
100+ )
101+ const modelElo = Number . isNaN ( maiaRating ) ? 1500 : maiaRating
102+
103+ // After Maia makes a candidate move, it is the human's turn.
104+ // Maia's value head conditions on the side to move as elo_self,
105+ // so the resulting boards must use the human as elo_self and Maia as elo_oppo.
106+ const { result } = await valueHeadModel . batchEvaluate (
107+ candidateBoards ,
108+ Array ( candidateBoards . length ) . fill ( playerRatingForValueHead ) ,
109+ Array ( candidateBoards . length ) . fill ( modelElo ) ,
110+ )
111+
112+ const maiaIsWhite = controller . player === 'black'
113+ let bestMove = candidateMoves [ 0 ]
114+ let bestScore = maiaIsWhite ? result [ 0 ] . value : 1 - result [ 0 ] . value
115+
116+ for ( let index = 1 ; index < candidateMoves . length ; index ++ ) {
117+ const whiteWinProb = result [ index ] . value
118+ const maiaWinProb = maiaIsWhite ? whiteWinProb : 1 - whiteWinProb
119+
120+ if ( maiaWinProb > bestScore ) {
121+ bestMove = candidateMoves [ index ]
122+ bestScore = maiaWinProb
123+ }
124+ }
125+
126+ if ( valueHeadDebugEnabled ) {
127+ const candidateSummaries = candidateMoves
128+ . map ( ( moveUci , index ) => {
129+ const moveResult = result [ index ]
130+ const whiteWinProb = moveResult . value
131+ const maiaWinProb = maiaIsWhite ? whiteWinProb : 1 - whiteWinProb
132+ const board = new Chess ( currentFen )
133+ const moveObj = board . move ( moveUci , { sloppy : true } )
134+
135+ return {
136+ move : moveUci ,
137+ san : moveObj ?. san ?? moveUci ,
138+ maiaWinProb : Number ( maiaWinProb . toFixed ( 4 ) ) ,
139+ }
140+ } )
141+ . sort ( ( a , b ) => b . maiaWinProb - a . maiaWinProb )
142+
143+ console . groupCollapsed (
144+ `[Maia value-head debug] ${ playGameConfig . maiaVersion } selected ${ bestMove } ` ,
145+ )
146+ console . table ( candidateSummaries )
147+ console . groupEnd ( )
148+ }
149+
150+ const estimatedDelaySeconds = Math . min (
151+ 3 ,
152+ 0.35 + legalMoves . length * 0.04 + Math . random ( ) * 0.25 ,
153+ )
154+
155+ return {
156+ top_move : bestMove ,
157+ move_delay : estimatedDelaySeconds ,
158+ }
159+ } , [
160+ controller . currentNode ,
161+ controller . player ,
162+ playGameConfig . maiaVersion ,
163+ playerRatingForValueHead ,
164+ valueHeadModel ,
165+ valueHeadStatus ,
166+ valueHeadDebugEnabled ,
167+ ] )
28168
29169 const makePlayerMove = async ( moveUci : string ) => {
30170 const moveTime = controller . updateClock ( )
@@ -48,20 +188,28 @@ export const useVsMaiaPlayController = (
48188 ? parseInt ( controller . timeControl . split ( '+' ) [ 0 ] ) * 60
49189 : 0
50190
51- const maiaMoves = await backOff (
52- ( ) =>
53- fetchGameMove (
54- controller . moveList ,
55- playGameConfig . maiaVersion ,
56- playGameConfig . startFen ,
57- null ,
58- simulateMaiaTime ? initialClock : 0 ,
59- simulateMaiaTime ? maiaClock : 0 ,
60- ) ,
61- {
62- jitter : 'full' ,
63- } ,
64- )
191+ const maiaMoves =
192+ playGameConfig . maiaMoveSelectionMode === 'value_head'
193+ ? await selectValueHeadMove ( )
194+ : await backOff (
195+ ( ) =>
196+ fetchGameMove (
197+ controller . moveList ,
198+ playGameConfig . maiaVersion ,
199+ playGameConfig . startFen ,
200+ null ,
201+ simulateMaiaTime ? initialClock : 0 ,
202+ simulateMaiaTime ? maiaClock : 0 ,
203+ ) ,
204+ {
205+ jitter : 'full' ,
206+ } ,
207+ )
208+
209+ if ( ! maiaMoves ?. top_move ) {
210+ return
211+ }
212+
65213 const nextMove = maiaMoves [ 'top_move' ]
66214 const moveDelay = maiaMoves [ 'move_delay' ]
67215
@@ -107,8 +255,11 @@ export const useVsMaiaPlayController = (
107255 controller . game . termination ,
108256 controller . moveList . length ,
109257 playGameConfig . maiaVersion ,
258+ playGameConfig . maiaMoveSelectionMode ,
110259 playGameConfig . startFen ,
111260 simulateMaiaTime ,
261+ selectValueHeadMove ,
262+ valueHeadStatus ,
112263 ] )
113264
114265 useEffect ( ( ) => {
0 commit comments