Skip to content

Commit 0a05bd3

Browse files
WIP: add value-head Maia play mode
1 parent 833431f commit 0a05bd3

4 files changed

Lines changed: 209 additions & 19 deletions

File tree

src/components/Common/PlaySetupModal.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useCallback, useContext, useState } from 'react'
77

88
import {
99
Color,
10+
MaiaMoveSelectionMode,
1011
PlayType,
1112
TimeControl,
1213
TimeControlOptionNames,
@@ -80,6 +81,7 @@ interface Props {
8081
maiaVersion?: string
8182
isBrain?: boolean
8283
sampleMoves?: boolean
84+
maiaMoveSelectionMode?: MaiaMoveSelectionMode
8385
simulateMaiaTime?: boolean
8486
startFen?: string
8587
}
@@ -105,6 +107,10 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
105107
const [sampleMoves, setSampleMoves] = useState<boolean>(
106108
props.sampleMoves || true,
107109
)
110+
const [maiaMoveSelectionMode, setMaiaMoveSelectionMode] =
111+
useState<MaiaMoveSelectionMode>(
112+
props.maiaMoveSelectionMode || 'move_matching',
113+
)
108114
const [simulateMaiaTime, setSimulateMaiaTime] = useState<boolean>(
109115
props.simulateMaiaTime !== undefined ? props.simulateMaiaTime : true,
110116
)
@@ -173,6 +179,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
173179
maiaVersion: maiaVersion,
174180
timeControl: timeControl,
175181
sampleMoves: sampleMoves,
182+
maiaMoveSelectionMode: maiaMoveSelectionMode,
176183
simulateMaiaTime: simulateMaiaTime,
177184
startFen: fen,
178185
},
@@ -187,6 +194,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
187194
timeControl: timeControl,
188195
isBrain: isBrain,
189196
sampleMoves: sampleMoves,
197+
maiaMoveSelectionMode: maiaMoveSelectionMode,
190198
simulateMaiaTime: simulateMaiaTime,
191199
startFen: fen,
192200
},
@@ -201,6 +209,7 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
201209
maiaVersion,
202210
timeControl,
203211
sampleMoves,
212+
maiaMoveSelectionMode,
204213
simulateMaiaTime,
205214
fen,
206215
isBrain,
@@ -410,6 +419,35 @@ export const PlaySetupModal: React.FC<Props> = (props: Props) => {
410419
</div>
411420
</div>
412421

422+
{props.playType == 'againstMaia' ? (
423+
<div>
424+
<label
425+
htmlFor="maia-play-style-select"
426+
className="mb-1 block text-sm font-medium text-primary"
427+
>
428+
Maia play style:
429+
</label>
430+
<div id="maia-play-style-select">
431+
<OptionSelect
432+
options={['move_matching', 'value_head']}
433+
labels={['Human move-match', 'Best win rate']}
434+
selected={maiaMoveSelectionMode}
435+
onChange={(selected) =>
436+
setMaiaMoveSelectionMode(
437+
selected as MaiaMoveSelectionMode,
438+
)
439+
}
440+
selectedClassName="border-human-4 bg-human-4 text-white hover:bg-human-4/90"
441+
/>
442+
</div>
443+
<p className="mt-2 text-xs text-secondary">
444+
{maiaMoveSelectionMode === 'move_matching'
445+
? 'Classic Maia: chooses the move it expects a human to play.'
446+
: 'Local Maia 2 mode: evaluates every legal move and picks the one with the best perceived win rate.'}
447+
</p>
448+
</div>
449+
) : null}
450+
413451
<div className="flex items-center gap-2">
414452
<input
415453
type="checkbox"

src/hooks/usePlayController/useVsMaiaController.ts

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react'
1+
import { useCallback, useContext, useEffect } from 'react'
22
import { Chess } from 'chess.ts'
33
import { PlayGameConfig } from 'src/types'
44
import { backOff } from 'exponential-backoff'
@@ -7,6 +7,7 @@ import { usePlayController } from 'src/hooks/usePlayController'
77
import { fetchGameMove, logGameMove, fetchPlayPlayerStats } from 'src/api'
88
import { useSound } from 'src/hooks/useSound'
99
import { safeUpdateRating } from 'src/lib/ratingUtils'
10+
import { MaiaEngineContext } from 'src/contexts'
1011

1112
const playStatsLoader = async () => {
1213
const stats = await fetchPlayPlayerStats()
@@ -25,6 +26,87 @@ export const useVsMaiaPlayController = (
2526
const controller = usePlayController(id, playGameConfig)
2627
const [stats, incrementStats, updateRating] = useStats(playStatsLoader)
2728
const { playMoveSound } = useSound()
29+
const maiaEngine = useContext(MaiaEngineContext)
30+
const valueHeadModel =
31+
playGameConfig.maiaMoveSelectionMode === 'value_head'
32+
? maiaEngine.maia
33+
: null
34+
const valueHeadStatus =
35+
playGameConfig.maiaMoveSelectionMode === 'value_head'
36+
? maiaEngine.status
37+
: null
38+
39+
const selectValueHeadMove = useCallback(async () => {
40+
if (
41+
!controller.currentNode ||
42+
valueHeadStatus !== 'ready' ||
43+
!valueHeadModel
44+
) {
45+
return null
46+
}
47+
48+
const currentFen = controller.currentNode.fen
49+
const chess = new Chess(currentFen)
50+
const legalMoves = chess.moves({ verbose: true }) as Array<{
51+
from: string
52+
to: string
53+
promotion?: string
54+
}>
55+
56+
if (legalMoves.length === 0) {
57+
return null
58+
}
59+
60+
const candidateMoves = legalMoves.map(
61+
(move) => `${move.from}${move.to}${move.promotion ?? ''}`,
62+
)
63+
const candidateBoards = candidateMoves.map((moveUci) => {
64+
const board = new Chess(currentFen)
65+
board.move(moveUci, { sloppy: true })
66+
return board.fen()
67+
})
68+
69+
const maiaRating = parseInt(
70+
playGameConfig.maiaVersion.replace('maia_kdd_', ''),
71+
10,
72+
)
73+
const modelElo = Number.isNaN(maiaRating) ? 1500 : maiaRating
74+
const { result } = await valueHeadModel.batchEvaluate(
75+
candidateBoards,
76+
Array(candidateBoards.length).fill(modelElo),
77+
Array(candidateBoards.length).fill(modelElo),
78+
)
79+
80+
const maiaIsWhite = controller.player === 'black'
81+
let bestMove = candidateMoves[0]
82+
let bestScore = maiaIsWhite ? result[0].value : 1 - result[0].value
83+
84+
for (let index = 1; index < candidateMoves.length; index++) {
85+
const whiteWinProb = result[index].value
86+
const maiaWinProb = maiaIsWhite ? whiteWinProb : 1 - whiteWinProb
87+
88+
if (maiaWinProb > bestScore) {
89+
bestMove = candidateMoves[index]
90+
bestScore = maiaWinProb
91+
}
92+
}
93+
94+
const estimatedDelaySeconds = Math.min(
95+
3,
96+
0.35 + legalMoves.length * 0.04 + Math.random() * 0.25,
97+
)
98+
99+
return {
100+
top_move: bestMove,
101+
move_delay: estimatedDelaySeconds,
102+
}
103+
}, [
104+
controller.currentNode,
105+
controller.player,
106+
playGameConfig.maiaVersion,
107+
valueHeadModel,
108+
valueHeadStatus,
109+
])
28110

29111
const makePlayerMove = async (moveUci: string) => {
30112
const moveTime = controller.updateClock()
@@ -48,20 +130,28 @@ export const useVsMaiaPlayController = (
48130
? parseInt(controller.timeControl.split('+')[0]) * 60
49131
: 0
50132

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-
)
133+
const maiaMoves =
134+
playGameConfig.maiaMoveSelectionMode === 'value_head'
135+
? await selectValueHeadMove()
136+
: await backOff(
137+
() =>
138+
fetchGameMove(
139+
controller.moveList,
140+
playGameConfig.maiaVersion,
141+
playGameConfig.startFen,
142+
null,
143+
simulateMaiaTime ? initialClock : 0,
144+
simulateMaiaTime ? maiaClock : 0,
145+
),
146+
{
147+
jitter: 'full',
148+
},
149+
)
150+
151+
if (!maiaMoves?.top_move) {
152+
return
153+
}
154+
65155
const nextMove = maiaMoves['top_move']
66156
const moveDelay = maiaMoves['move_delay']
67157

@@ -107,8 +197,11 @@ export const useVsMaiaPlayController = (
107197
controller.game.termination,
108198
controller.moveList.length,
109199
playGameConfig.maiaVersion,
200+
playGameConfig.maiaMoveSelectionMode,
110201
playGameConfig.startFen,
111202
simulateMaiaTime,
203+
selectValueHeadMove,
204+
valueHeadStatus,
112205
])
113206

114207
useEffect(() => {

src/pages/play/maia.tsx

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ import { startGame } from 'src/api'
33
import { NextPage } from 'next/types'
44
import { useRouter } from 'next/router'
55
import { tourConfigs } from 'src/constants/tours'
6-
import { ModalContext, useTour } from 'src/contexts'
7-
import { Loading, PlayControls } from 'src/components'
8-
import { Color, TimeControl, PlayGameConfig } from 'src/types'
6+
import {
7+
MaiaEngineContext,
8+
MaiaEngineContextProvider,
9+
ModalContext,
10+
useTour,
11+
} from 'src/contexts'
12+
import { DownloadModelModal, Loading, PlayControls } from 'src/components'
13+
import {
14+
Color,
15+
MaiaEngine,
16+
MaiaMoveSelectionMode,
17+
TimeControl,
18+
PlayGameConfig,
19+
} from 'src/types'
920
import { useContext, useEffect, useMemo, useState } from 'react'
1021
import { GameplayInterface } from 'src/components/Board/GameplayInterface'
1122
import { useVsMaiaPlayController } from 'src/hooks/usePlayController/useVsMaiaController'
@@ -74,7 +85,13 @@ const PlayMaia: React.FC<Props> = ({
7485
)
7586
}
7687

77-
const PlayMaiaPage: NextPage = () => {
88+
interface PageContentProps {
89+
maiaEngine?: MaiaEngine
90+
}
91+
92+
const PlayMaiaPageContent: React.FC<PageContentProps> = ({
93+
maiaEngine,
94+
}: PageContentProps) => {
7895
const { startTour, tourState } = useTour()
7996
const [initialTourCheck, setInitialTourCheck] = useState(false)
8097

@@ -96,6 +113,7 @@ const PlayMaiaPage: NextPage = () => {
96113
timeControl,
97114
isBrain,
98115
sampleMoves,
116+
maiaMoveSelectionMode,
99117
simulateMaiaTime: simulateMaiaTimeQuery,
100118
startFen,
101119
} = router.query
@@ -114,13 +132,16 @@ const PlayMaiaPage: NextPage = () => {
114132
timeControl: (timeControl || 'unlimited') as TimeControl,
115133
isBrain: isBrain == 'true',
116134
sampleMoves: sampleMoves == 'true',
135+
maiaMoveSelectionMode: (maiaMoveSelectionMode ||
136+
'move_matching') as MaiaMoveSelectionMode,
117137
simulateMaiaTime: simulateMaiaTime,
118138
startFen: typeof startFen == 'string' ? startFen : undefined,
119139
}),
120140
[
121141
startFen,
122142
isBrain,
123143
maiaVersion,
144+
maiaMoveSelectionMode,
124145
player,
125146
sampleMoves,
126147
timeControl,
@@ -177,6 +198,10 @@ const PlayMaiaPage: NextPage = () => {
177198
}
178199
}
179200

201+
if (!router.isReady) {
202+
return
203+
}
204+
180205
if (!id) {
181206
fetchGameId()
182207

@@ -195,6 +220,15 @@ const PlayMaiaPage: NextPage = () => {
195220
content="Challenge the most human-like chess AI. Unlike traditional engines that play robotically, Maia naturally plays moves a person would make, trained on millions of human games with real chess intuition."
196221
/>
197222
</Head>
223+
{playGameConfig.maiaMoveSelectionMode === 'value_head' &&
224+
maiaEngine &&
225+
(maiaEngine.status === 'no-cache' ||
226+
maiaEngine.status === 'downloading') ? (
227+
<DownloadModelModal
228+
progress={maiaEngine.progress}
229+
download={maiaEngine.downloadModel}
230+
/>
231+
) : null}
198232
<Loading isLoading={!router.isReady || !id}>
199233
{router.isReady && id && (
200234
<PlayMaia
@@ -210,4 +244,25 @@ const PlayMaiaPage: NextPage = () => {
210244
)
211245
}
212246

247+
const PlayMaiaPageWithLocalMaia: React.FC = () => {
248+
const maiaEngine = useContext(MaiaEngineContext)
249+
250+
return <PlayMaiaPageContent maiaEngine={maiaEngine} />
251+
}
252+
253+
const PlayMaiaPage: NextPage = () => {
254+
const router = useRouter()
255+
const requiresLocalMaia = router.query.maiaMoveSelectionMode === 'value_head'
256+
257+
if (!requiresLocalMaia) {
258+
return <PlayMaiaPageContent />
259+
}
260+
261+
return (
262+
<MaiaEngineContextProvider>
263+
<PlayMaiaPageWithLocalMaia />
264+
</MaiaEngineContextProvider>
265+
)
266+
}
267+
213268
export default PlayMaiaPage

src/types/play.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ export const TimeControlOptionNames = [
1010
'Unlimited',
1111
]
1212

13+
export const MaiaMoveSelectionModes = ['move_matching', 'value_head'] as const
14+
1315
// To fix some weird inconsistency between vercel and local:
1416
// eslint-disable-next-line prettier/prettier
1517
export type TimeControl = (typeof TimeControlOptions)[number]
18+
export type MaiaMoveSelectionMode = (typeof MaiaMoveSelectionModes)[number]
1619

1720
export type PlayType = 'againstMaia' | 'handAndBrain'
1821

@@ -23,6 +26,7 @@ export interface PlayGameConfig {
2326
playType: PlayType
2427
isBrain: boolean
2528
sampleMoves: boolean
29+
maiaMoveSelectionMode?: MaiaMoveSelectionMode
2630
simulateMaiaTime?: boolean
2731
startFen?: string
2832
maiaPartnerVersion?: string

0 commit comments

Comments
 (0)