-
-
Notifications
You must be signed in to change notification settings - Fork 366
More practice fixes #2680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More practice fixes #2680
Changes from 3 commits
170fe8a
e6b6205
180e026
ca84d11
d3fce7c
15ddb9b
be370d4
72fb244
567f992
ea8b09d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -47,15 +47,32 @@ final _logger = Logger('OfflineComputerGameController'); | |||||||
| /// The number of CPU cores to use for engine evaluation. | ||||||||
| final numberOfCoresForEvaluation = max(1, maxEngineCores - 1); | ||||||||
|
|
||||||||
| /// Max search time for hints evaluation. | ||||||||
| const _kHintsMaxSearchTime = Duration(milliseconds: 3000); | ||||||||
|
|
||||||||
| /// Ply threshold for opening phase. Below this, we check the master database | ||||||||
| /// to consider book moves as good regardless of engine evaluation. | ||||||||
| const _kOpeningPlyThreshold = 30; | ||||||||
|
|
||||||||
| /// Minimum depth for which we consider the practice mode evaluation usable. | ||||||||
| const _kEvalMinDepth = kDebugMode ? 12 : 16; | ||||||||
| /// Max search time for hints evaluation. | ||||||||
| const _kHintsMaxSearchTime = Duration(milliseconds: 5000); | ||||||||
|
|
||||||||
| /// Depth threshold for using an engine evaluation for hints. | ||||||||
| /// | ||||||||
| /// Lower end devices will probably not reach it so the evaluation will run for the full search time | ||||||||
| /// but it help to get faster feedback on move quality and hints on higher end devices. | ||||||||
| // TODO: consider using searched nodes instead of depth | ||||||||
| const _kHintsEvalMinDepth = kDebugMode ? 14 : 18; | ||||||||
|
|
||||||||
| /// Min search time for a move evaluation in practice mode when the move is not in the pre-move PVs. | ||||||||
| const _kMoveEvalMinSearchTime = Duration(milliseconds: 1000); | ||||||||
|
|
||||||||
| /// Max search time for a move evaluation in practice mode when the move is not in the pre-move PVs. | ||||||||
| /// | ||||||||
| /// We want a fast feedback here, and since multipv=1 the search should be fast. | ||||||||
| const _kMoveEvalMaxSearchTime = Duration(milliseconds: 2000); | ||||||||
|
|
||||||||
| /// Depth threshold for using an engine evaluation for move evaluation in practice mode. | ||||||||
| /// | ||||||||
| /// The search is done with multipv=1 here, so we can reach higher depths. | ||||||||
| const _kMoveEvalMinDepth = kDebugMode ? 18 : 20; | ||||||||
|
|
||||||||
| /// Stockfish flavor to use for the engine opponent and hint generation. | ||||||||
| /// | ||||||||
|
|
@@ -212,8 +229,13 @@ class OfflineComputerGameController extends Notifier<OfflineComputerGameState> { | |||||||
| if (!state.game.practiceMode || !state.game.playable) return; | ||||||||
|
|
||||||||
| var preMoveAnalysis = state.currentAnalysis; | ||||||||
| final cursorBeforeMove = state.stepCursor; | ||||||||
| final positionBefore = state.currentPosition; | ||||||||
| final plyBeforeMove = state.currentPosition.ply; | ||||||||
| final stepsBeforeMove = state.game.steps | ||||||||
| .skip(1) | ||||||||
| .map((s) => Step(position: s.position, sanMove: s.sanMove!)) | ||||||||
| .toIList(); | ||||||||
|
|
||||||||
| state = state.copyWith(isEvaluatingMove: true); | ||||||||
|
|
||||||||
|
|
@@ -229,16 +251,31 @@ class OfflineComputerGameController extends Notifier<OfflineComputerGameState> { | |||||||
| // If hints were still loading when we made the move, wait for them to complete | ||||||||
| // so we can get the "before" evaluation for comparison. | ||||||||
| // Wait time must be longer than _kHintsMaxSearchTime to account for engine startup overhead. | ||||||||
| if (preMoveAnalysis?.eval == null) { | ||||||||
| if (state.isLoadingHint && preMoveAnalysis?.eval == null) { | ||||||||
| final maxWaitTime = _kHintsMaxSearchTime + const Duration(milliseconds: 1000); | ||||||||
| final deadline = DateTime.now().add(maxWaitTime); | ||||||||
| while (state.isLoadingHint && ref.mounted && DateTime.now().isBefore(deadline)) { | ||||||||
| while (state.game.steps[cursorBeforeMove].computerAnalysis?.eval == null && | ||||||||
veloce marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||
| ref.mounted && | ||||||||
| DateTime.now().isBefore(deadline)) { | ||||||||
| await Future<void>.delayed(const Duration(milliseconds: 50)); | ||||||||
| } | ||||||||
|
|
||||||||
| if (!ref.mounted) return; | ||||||||
|
|
||||||||
| preMoveAnalysis = state.game.steps[stepCursorAfterMove - 1].computerAnalysis; | ||||||||
| preMoveAnalysis = state.game.steps[cursorBeforeMove].computerAnalysis; | ||||||||
| } else if (preMoveAnalysis?.eval == null) { | ||||||||
| // Not loading hints and hints are null? Let's run a quick evaluation | ||||||||
| final evalAfter = await _getEval( | ||||||||
| _makeMoveEvalWork(stepsBeforeMove), | ||||||||
| minSearchTime: _kMoveEvalMinSearchTime, | ||||||||
| depthThreshold: _kMoveEvalMinDepth, | ||||||||
| tablebaseLookupPosition: positionBefore, | ||||||||
| ); | ||||||||
| if (!ref.mounted) return; | ||||||||
| if (evalAfter != null) { | ||||||||
| preMoveAnalysis = ComputerAnalysis(eval: evalAfter); | ||||||||
| _setStepAnalysis(cursorBeforeMove, preMoveAnalysis); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| final preMoveEval = preMoveAnalysis?.eval; | ||||||||
|
|
@@ -291,57 +328,31 @@ class OfflineComputerGameController extends Notifier<OfflineComputerGameState> { | |||||||
| _logger.info('Move not in computed hints PVs, evaluating: ${move.uci}'); | ||||||||
|
|
||||||||
| try { | ||||||||
| final evaluationService = ref.read(evaluationServiceProvider); | ||||||||
|
|
||||||||
| final stepsAfter = state.game.steps | ||||||||
| .skip(1) | ||||||||
| .map((s) => Step(position: s.position, sanMove: s.sanMove!)) | ||||||||
| .toIList(); | ||||||||
|
|
||||||||
| final workAfter = EvalWork( | ||||||||
| id: state.game.id, | ||||||||
| stockfishFlavor: _kComputerStockfishFlavor, | ||||||||
| variant: Variant.standard, | ||||||||
| threads: numberOfCoresForEvaluation, | ||||||||
| hashSize: evaluationService.maxMemory, | ||||||||
| searchTime: const Duration(milliseconds: 2000), | ||||||||
| // We want the fastest search here and we only need the eval | ||||||||
| multiPv: 1, | ||||||||
| threatMode: false, | ||||||||
| initialPosition: state.game.initialPosition, | ||||||||
| steps: stepsAfter, | ||||||||
| ); | ||||||||
| final workAfter = _makeMoveEvalWork(stepsAfter); | ||||||||
|
|
||||||||
| final positionAfterMove = state.currentPosition; | ||||||||
| final Completer<ClientEval?> evalCompleter = Completer(); | ||||||||
|
|
||||||||
| // Launch engine eval and tablebase lookup in parallel. | ||||||||
| // Fallback: if neither engine nor tablebase provide an eval in time, complete with null so | ||||||||
| // the await below always finishes. | ||||||||
| Timer(const Duration(seconds: 3), () { | ||||||||
| if (!evalCompleter.isCompleted) { | ||||||||
| evalCompleter.complete(null); | ||||||||
| } | ||||||||
| }); | ||||||||
| _getEval(workAfter, minSearchTime: const Duration(milliseconds: 500)).then((eval) { | ||||||||
| if (!evalCompleter.isCompleted && eval != null) { | ||||||||
| evalCompleter.complete(eval); | ||||||||
| } | ||||||||
| }); | ||||||||
| if (isTablebaseRelevant(positionAfterMove)) { | ||||||||
| _fetchTablebaseEval(positionAfterMove).then((tablebaseEval) { | ||||||||
| if (!evalCompleter.isCompleted && tablebaseEval != null) { | ||||||||
| evalCompleter.complete(tablebaseEval); | ||||||||
| } | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| final evalAfter = await evalCompleter.future; | ||||||||
| final evalAfter = await _getEval( | ||||||||
| workAfter, | ||||||||
| minSearchTime: _kMoveEvalMinSearchTime, | ||||||||
| depthThreshold: _kMoveEvalMinDepth, | ||||||||
| tablebaseLookupPosition: positionAfterMove, | ||||||||
| ); | ||||||||
|
|
||||||||
| if (!ref.mounted) return; | ||||||||
|
|
||||||||
| if (evalAfter != null) { | ||||||||
| _logger.info('Move eval: depth=${evalAfter.depth}, score=${evalAfter.evalString}'); | ||||||||
| _logger.info( | ||||||||
| 'Move eval computed: depth=${evalAfter.depth}, searchTime=${evalAfter is LocalEval ? evalAfter.searchTime : null} nodes=${evalAfter.nodes} score=${evalAfter.evalString}', | ||||||||
| ); | ||||||||
| // print( | ||||||||
| // 'Move eval computed: depth=${evalAfter.depth}, searchTime=${evalAfter is LocalEval ? evalAfter.searchTime : null} nodes=${evalAfter.nodes}', | ||||||||
| // ); | ||||||||
|
||||||||
| // print( | |
| // 'Move eval computed: depth=${evalAfter.depth}, searchTime=${evalAfter is LocalEval ? evalAfter.searchTime : null} nodes=${evalAfter.nodes}', | |
| // ); |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in dartdoc: "optionnally" should be "optionally".
| /// Gets an evaluation by requesting at the same time the local engine and a cloud eval and optionnally doing a tablebase lookup. | |
| /// Gets an evaluation by requesting at the same time the local engine and a cloud eval and optionally doing a tablebase lookup. |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_getEvalTimer is a single shared field and is cancelled/reassigned on every _getEval call. If multiple _getEval calls overlap (e.g., hints evaluation while a practice move evaluation starts), a later call can cancel the earlier call’s fallback timer, potentially leaving that earlier completer without its safety timeout if an exception occurs. Consider making the fallback timer local to _getEval (and cancelling it when the completer completes) so concurrent calls don’t interfere with each other.
Outdated
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These commented-out print(...) debug statements add noise to the production code path. Consider removing them (or gating them behind kDebugMode logging) to keep the file clean.
| // print( | |
| // 'Hints computed: depth = ${finalEval?.depth}, searchTime = ${finalEval is LocalEval ? finalEval.searchTime : null} nodes=${finalEval?.nodes}', | |
| // ); |
Uh oh!
There was an error while loading. Please reload this page.