Skip to content

Commit 611c2cc

Browse files
Merge pull request #260 from CSSLab/codex/drill-background-analysis
Background Maia + Stockfish analysis during drill play
2 parents 65eecb6 + 53c8236 commit 611c2cc

4 files changed

Lines changed: 99 additions & 50 deletions

File tree

src/hooks/useAnalysisController/useAnalysisController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const useAnalysisController = (
2626
game: AnalyzedGame,
2727
initialOrientation?: 'white' | 'black',
2828
enableAutoSave = true,
29+
enableEngineAnalysis = true,
2930
) => {
3031
const defaultOrientation = initialOrientation
3132
? initialOrientation
@@ -86,6 +87,7 @@ export const useAnalysisController = (
8687
deepAnalysisController.progress.isAnalyzing
8788
? deepAnalysisController.config.targetDepth
8889
: 18,
90+
enableEngineAnalysis,
8991
)
9092

9193
const availableMoves = useMemo(() => {

src/hooks/useAnalysisController/useEngineAnalysis.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const useEngineAnalysis = (
1515
currentMaiaModel: string,
1616
setAnalysisState: React.Dispatch<React.SetStateAction<number>>,
1717
targetDepth = 18,
18+
enabled = true,
1819
) => {
1920
const maia = useContext(MaiaEngineContext)
2021
const stockfish = useContext(StockfishEngineContext)
@@ -78,7 +79,7 @@ export const useEngineAnalysis = (
7879
}
7980

8081
useEffect(() => {
81-
if (!currentNode) return
82+
if (!currentNode || !enabled) return
8283

8384
const board = new Chess(currentNode.fen)
8485
const nodeFen = currentNode.fen
@@ -163,10 +164,11 @@ export const useEngineAnalysis = (
163164
inProgressAnalyses,
164165
maia,
165166
setAnalysisState,
167+
enabled,
166168
])
167169

168170
useEffect(() => {
169-
if (!currentNode) return
171+
if (!currentNode || !enabled) return
170172

171173
const shouldForceStockfishRerun =
172174
stockfishDebugRerunToken > lastConsumedStockfishRerunTokenRef.current
@@ -281,5 +283,6 @@ export const useEngineAnalysis = (
281283
stockfishDebugRerunToken,
282284
currentNode?.analysis.maia?.[currentMaiaModel]?.policy,
283285
currentNode?.mainChild?.move,
286+
enabled,
284287
])
285288
}

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -903,13 +903,40 @@ export const useOpeningDrillController = (
903903
[currentMaiaModel, stockfish],
904904
)
905905

906-
// Background analysis: run Maia + Stockfish on positions as they are played
907-
// so that post-drill analysis is already complete (or nearly so) when the
908-
// drill ends.
909-
const backgroundAnalysisQueueRef = useRef<Set<string>>(new Set())
906+
// Background analysis: run Maia + Stockfish on drill positions as they are
907+
// played so post-drill analysis has less (or no) work to do.
908+
//
909+
// Uses a simple promise-chain pattern: each node's analysis is chained onto
910+
// a single promise ref. No queue management, no running flags — just append
911+
// work to the chain. Nodes are tracked by FEN in a Set to avoid duplicates.
912+
const bgChainRef = useRef<Promise<void>>(Promise.resolve())
913+
const bgAnalyzedFensRef = useRef<Set<string>>(new Set())
914+
const bgCancelledRef = useRef(false)
915+
const bgDrillIdRef = useRef<string | null>(null)
916+
const ensureMaiaRef = useRef(ensureMaiaForNode)
917+
const ensureStockfishRef = useRef(ensureStockfishForNode)
918+
useEffect(() => {
919+
ensureMaiaRef.current = ensureMaiaForNode
920+
}, [ensureMaiaForNode])
921+
useEffect(() => {
922+
ensureStockfishRef.current = ensureStockfishForNode
923+
}, [ensureStockfishForNode])
910924

911925
useEffect(() => {
912-
if (!currentDrillGame || isAnalyzingDrill) return
926+
if (!currentDrillGame || isAnalyzingDrill) {
927+
return
928+
}
929+
930+
// If drill changed, reset for the new drill
931+
if (currentDrillGame.id !== bgDrillIdRef.current) {
932+
bgCancelledRef.current = true
933+
// Let any in-flight work finish with the cancelled flag, then reset
934+
bgChainRef.current = bgChainRef.current.then(() => {
935+
bgCancelledRef.current = false
936+
})
937+
bgAnalyzedFensRef.current = new Set()
938+
bgDrillIdRef.current = currentDrillGame.id
939+
}
913940

914941
const mainLine = gameTree.getMainLine()
915942
const openingEndNode = currentDrillGame.openingEndNode
@@ -918,50 +945,50 @@ export const useOpeningDrillController = (
918945
: 0
919946
const drillNodes = mainLine.slice(startIndex)
920947

921-
// Find nodes that still need analysis and haven't been queued yet
922-
const nodesNeedingWork = drillNodes.filter((node) => {
923-
if (backgroundAnalysisQueueRef.current.has(node.fen)) return false
924-
const hasMaia =
925-
node.analysis.maia &&
926-
MAIA_MODELS.every((model) => node.analysis.maia?.[model])
927-
const hasStockfish =
928-
node.analysis.stockfish &&
929-
node.analysis.stockfish.depth >= DRILL_STOCKFISH_TARGET_DEPTH
930-
return !hasMaia || !hasStockfish
931-
})
932-
933-
if (nodesNeedingWork.length === 0) return
934-
935-
let cancelled = false
936-
937-
const runBackgroundAnalysis = async () => {
938-
for (const node of nodesNeedingWork) {
939-
if (cancelled) break
940-
backgroundAnalysisQueueRef.current.add(node.fen)
941-
await ensureMaiaForNode(node)
942-
if (cancelled) break
943-
// Let Stockfish finish its current evaluation even if new moves come in
944-
await ensureStockfishForNode(node)
945-
}
946-
}
947-
948-
runBackgroundAnalysis()
948+
for (const node of drillNodes) {
949+
if (bgAnalyzedFensRef.current.has(node.fen)) continue
950+
bgAnalyzedFensRef.current.add(node.fen)
949951

950-
return () => {
951-
cancelled = true
952+
// Chain this node's analysis onto the promise chain.
953+
// Wrapped in try/catch so one failure doesn't break the whole chain.
954+
bgChainRef.current = bgChainRef.current.then(async () => {
955+
try {
956+
if (bgCancelledRef.current) return
957+
console.log('[bg] maia start:', node.san || node.move || '?')
958+
await ensureMaiaRef.current(node)
959+
const hasMaia = !!(
960+
node.analysis.maia &&
961+
MAIA_MODELS.every((m) => node.analysis.maia?.[m])
962+
)
963+
console.log('[bg] maia done:', hasMaia, '| sf start')
964+
if (bgCancelledRef.current) return
965+
await ensureStockfishRef.current(node)
966+
console.log(
967+
'[bg] sf done, depth:',
968+
node.analysis.stockfish?.depth ?? 0,
969+
)
970+
} catch (error) {
971+
console.error('[bg] error analyzing node:', error)
972+
}
973+
})
952974
}
953-
}, [
954-
currentDrillGame,
955-
gameTree,
956-
isAnalyzingDrill,
957-
ensureMaiaForNode,
958-
ensureStockfishForNode,
959-
// Re-run when tree grows (new moves played)
960-
treeController.currentNode,
961-
])
975+
}, [currentDrillGame, gameTree, isAnalyzingDrill, treeController.currentNode])
976+
977+
// Stop background analysis. Signals cancellation and stops stockfish so
978+
// ensureDrillAnalysis can use stockfish immediately. The chain's remaining
979+
// .then() callbacks will see the cancelled flag and return quickly.
980+
// Does NOT await the chain — avoids hanging if a step is stuck.
981+
const stopBackgroundAnalysis = useCallback(() => {
982+
bgCancelledRef.current = true
983+
stockfish.stopEvaluation()
984+
}, [stockfish])
962985

963986
const ensureDrillAnalysis = useCallback(
964987
async (drillGame: OpeningDrillGame): Promise<boolean> => {
988+
// Signal background to stop and give the generator a tick to clean up
989+
stopBackgroundAnalysis()
990+
await delay(50)
991+
965992
const mainLine = drillGame.tree.getMainLine()
966993
const startingNode = drillGame.openingEndNode || mainLine[0]
967994
const startIndex = startingNode
@@ -1036,7 +1063,12 @@ export const useOpeningDrillController = (
10361063

10371064
return !wasCancelled
10381065
},
1039-
[ensureMaiaForNode, ensureStockfishForNode, setDrillAnalysisProgress],
1066+
[
1067+
ensureMaiaForNode,
1068+
ensureStockfishForNode,
1069+
setDrillAnalysisProgress,
1070+
stopBackgroundAnalysis,
1071+
],
10401072
)
10411073

10421074
const cancelDrillAnalysis = useCallback(() => {

src/pages/openings/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,26 @@ const OpeningsPage: NextPage = () => {
180180
},
181181
controller.currentDrill?.playerColor || 'white',
182182
false, // Disable auto-saving on openings page
183+
controller.analysisEnabled || controller.continueAnalyzingMode, // Disable engine analysis during drill play
183184
)
184185

185-
// Sync analysis controller with current node
186+
// Sync analysis controller with current node — only when analysis is active
187+
// (post-drill continue-analyzing mode). During drill play, the analysis
188+
// controller's auto-stockfish would conflict with background analysis.
186189
useEffect(() => {
187-
if (controller.currentNode && analysisController.setCurrentNode) {
190+
if (
191+
controller.currentNode &&
192+
analysisController.setCurrentNode &&
193+
(controller.analysisEnabled || controller.continueAnalyzingMode)
194+
) {
188195
analysisController.setCurrentNode(controller.currentNode)
189196
}
190-
}, [controller.currentNode, analysisController.setCurrentNode])
197+
}, [
198+
controller.currentNode,
199+
controller.analysisEnabled,
200+
controller.continueAnalyzingMode,
201+
analysisController.setCurrentNode,
202+
])
191203

192204
// Create game object for MovesContainer
193205
const gameForContainer = useMemo(() => {

0 commit comments

Comments
 (0)