Skip to content

Commit 162e056

Browse files
authored
Simplify evaluation service (#2554)
1 parent 7a3d91e commit 162e056

29 files changed

+2724
-934
lines changed

lib/src/binding.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
44
import 'package:flutter/foundation.dart';
55
import 'package:flutter/widgets.dart';
66
import 'package:lichess_mobile/firebase_options.dart';
7-
import 'package:lichess_mobile/src/model/engine/engine.dart';
7+
import 'package:multistockfish/multistockfish.dart';
88
import 'package:shared_preferences/shared_preferences.dart';
99

1010
/// A singleton class that provides access to plugins and external APIs.
@@ -82,8 +82,8 @@ abstract class LichessBinding {
8282
/// Wraps [FirebaseMessaging.onBackgroundMessage].
8383
void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler);
8484

85-
/// The factory to create Stockfish
86-
StockfishFactory get stockfishFactory;
85+
/// The Stockfish singleton instance.
86+
Stockfish get stockfish;
8787
}
8888

8989
/// A concrete implementation of [LichessBinding] for the app.
@@ -169,5 +169,5 @@ class AppLichessBinding extends LichessBinding {
169169
FirebaseMessaging.onMessageOpenedApp;
170170

171171
@override
172-
StockfishFactory get stockfishFactory => const StockfishFactory();
172+
Stockfish get stockfish => Stockfish.instance;
173173
}

lib/src/log.dart

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:developer' as developer;
2+
import 'dart:io' show Platform;
23

34
import 'package:flutter/foundation.dart';
45
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -28,11 +29,15 @@ class AppLogService {
2829
Iterable<LogRecord> get logs => _logs.values;
2930

3031
void start() {
31-
ref.listen(logPreferencesProvider.select((prefs) => prefs.level), (prev, next) {
32-
if (next != prev) {
33-
Logger.root.level = next;
34-
}
35-
}, fireImmediately: true);
32+
if (kDebugMode) {
33+
Logger.root.level = Level.ALL;
34+
} else {
35+
ref.listen(logPreferencesProvider.select((prefs) => prefs.level), (prev, next) {
36+
if (next != prev) {
37+
Logger.root.level = next;
38+
}
39+
}, fireImmediately: true);
40+
}
3641

3742
Logger.root.onRecord.listen((record) {
3843
if (kDebugMode) {
@@ -45,7 +50,9 @@ class AppLogService {
4550
stackTrace: record.stackTrace,
4651
);
4752

48-
if (_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.FINE) {
53+
if (_loggersToShowInTerminal.contains(record.loggerName) &&
54+
record.level >= Level.FINE &&
55+
!Platform.environment.containsKey('FLUTTER_TEST')) {
4956
debugPrint('[${record.loggerName}] ${record.message}');
5057
}
5158
} else {

lib/src/model/analysis/analysis_controller.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ class AnalysisController extends AsyncNotifier<AnalysisState>
341341
contextOpening: opening,
342342
isComputerAnalysisAllowed: isComputerAnalysisAllowed,
343343
isServerAnalysisEnabled: prefs.enableServerAnalysis,
344-
evaluationContext: EvaluationContext(variant: _variant, initialPosition: _root.position),
344+
evaluationContext: EvaluationContext(
345+
id: options.gameId,
346+
variant: _variant,
347+
initialPosition: _root.position,
348+
),
345349
playersAnalysis: serverAnalysis,
346350
acplChartData: serverAnalysis != null ? _makeAcplChartData() : null,
347351
division: division,

lib/src/model/analysis/retro_controller.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
144144
variant: _game.meta.variant,
145145
currentPath: UciPath.empty,
146146
evaluationContext: EvaluationContext(
147+
id: options.id,
147148
variant: _game.meta.variant,
148149
initialPosition: _root.position,
149150
),
@@ -234,6 +235,7 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
234235
lastMove: mistakes.firstOrNull?.branch.sanMove.move,
235236
variant: _game.meta.variant,
236237
evaluationContext: EvaluationContext(
238+
id: options.id,
237239
variant: _game.meta.variant,
238240
initialPosition: _root.position,
239241
),

lib/src/model/broadcast/broadcast_analysis_controller.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class BroadcastAnalysisController extends AsyncNotifier<BroadcastAnalysisState>
134134
lastMove: lastMove,
135135
pov: Side.white,
136136
evaluationContext: EvaluationContext(
137+
id: params.gameId,
137138
variant: Variant.standard,
138139
initialPosition: _root.position,
139140
),

lib/src/model/engine/engine.dart

Lines changed: 8 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -1,254 +1,16 @@
1-
import 'dart:async';
2-
3-
import 'package:flutter/foundation.dart';
4-
import 'package:lichess_mobile/src/binding.dart';
51
import 'package:lichess_mobile/src/constants.dart';
6-
import 'package:lichess_mobile/src/model/engine/uci_protocol.dart';
7-
import 'package:lichess_mobile/src/model/engine/work.dart';
8-
import 'package:logging/logging.dart';
92
import 'package:multistockfish/multistockfish.dart';
103

11-
enum EngineState { initial, loading, idle, computing, error, disposed }
12-
13-
/// An engine that can compute chess positions.
14-
///
15-
/// This is a high-level abstraction over a chess engine process.
16-
///
17-
/// See [StockfishEngine] for a concrete implementation.
18-
abstract class Engine {
19-
/// The current state of the engine.
20-
ValueListenable<EngineState> get state;
21-
22-
/// The name of the engine.
23-
String get name;
24-
25-
/// Start the engine with the given [work].
26-
Stream<EvalResult> start(Work work);
27-
28-
/// Stop the engine current computation.
29-
void stop();
30-
31-
/// A future that completes once the underlying engine process is exited.
32-
Future<void> get exited;
33-
34-
/// Whether the engine is disposed.
35-
///
36-
/// This will be `true` once [dispose] is called. Once the engine is disposed, it cannot be
37-
/// used anymore, and [start] and [stop] methods will throw a [StateError].
38-
bool get isDisposed;
39-
40-
/// Dispose the engine. It cannot be used after this method is called.
41-
///
42-
/// Returns the same future as [exited], that completes once the underlying engine process is exited.
43-
///
44-
/// It is safe to call this method multiple times.
45-
Future<void> dispose();
46-
}
47-
484
const _nnueDownloadUrl = '$kLichessCDNHost/assets/lifat/nnue/';
495

50-
/// A concrete implementation of [Engine] that uses Stockfish as the underlying engine.
51-
class StockfishEngine implements Engine {
52-
StockfishEngine(this.flavor, {String? smallNetPath, String? bigNetPath})
53-
: _protocol = UCIProtocol(),
54-
_smallNetPath = smallNetPath,
55-
_bigNetPath = bigNetPath,
56-
assert(
57-
flavor != StockfishFlavor.latestNoNNUE || smallNetPath != null && bigNetPath != null,
58-
'NNUE paths must be provided for chess flavor',
59-
);
60-
61-
/// URL to download the latest big NNUE network.
62-
static final bigNetUrl = Uri.parse('$_nnueDownloadUrl${Stockfish.latestBigNNUE}');
63-
64-
/// SHA256 hash (first 12 digits) of the latest big NNUE network.
65-
static final bigNetHash = Stockfish.latestBigNNUE.substring(3, 15);
66-
67-
/// URL to download the latest small NNUE network.
68-
static final smallNetUrl = Uri.parse('$_nnueDownloadUrl${Stockfish.latestSmallNNUE}');
69-
70-
/// SHA256 hash (first 12 digits) of the latest small NNUE network.
71-
static final smallNetHash = Stockfish.latestSmallNNUE.substring(3, 15);
72-
73-
final StockfishFlavor flavor;
74-
final UCIProtocol _protocol;
75-
final String? _smallNetPath;
76-
final String? _bigNetPath;
77-
78-
Stockfish? _stockfish;
79-
String _name = 'Stockfish';
80-
StreamSubscription<String>? _stdoutSubscription;
81-
82-
bool _isDisposed = false;
83-
84-
final _state = ValueNotifier(EngineState.initial);
85-
86-
final _log = Logger('StockfishEngine');
87-
88-
/// A completer that completes once the underlying engine has exited.
89-
final _exitCompleter = Completer<void>();
90-
91-
@override
92-
ValueListenable<EngineState> get state => _state;
93-
94-
@override
95-
String get name => _name;
96-
97-
@override
98-
Future<void> get exited => _exitCompleter.future;
99-
100-
@override
101-
bool get isDisposed => _isDisposed;
102-
103-
@override
104-
Stream<EvalResult> start(Work work) {
105-
if (isDisposed) {
106-
throw StateError('Engine is disposed');
107-
}
108-
109-
_log.info('engine start at ply ${work.position.ply} and path ${work.path}');
110-
_protocol.compute(work);
111-
112-
if (_stockfish == null) {
113-
try {
114-
final stockfish = LichessBinding.instance.stockfishFactory(
115-
flavor,
116-
smallNetPath: _smallNetPath,
117-
bigNetPath: _bigNetPath,
118-
);
119-
_stockfish = stockfish;
120-
121-
_state.value = EngineState.loading;
122-
_stdoutSubscription = stockfish.stdout.listen((line) {
123-
_protocol.received(line);
124-
});
125-
126-
stockfish.state.addListener(_stockfishStateListener);
127-
128-
// Ensure the engine is ready before sending commands
129-
void onReadyOnce() {
130-
if (stockfish.state.value == StockfishState.ready) {
131-
_protocol.connected((String cmd) {
132-
stockfish.stdin = cmd;
133-
});
134-
stockfish.state.removeListener(onReadyOnce);
135-
}
136-
}
137-
138-
stockfish.state.addListener(onReadyOnce);
139-
140-
// Check immediately in case the engine is already ready
141-
// This prevents a race where the engine becomes ready between
142-
// adding _stockfishStateListener and adding onReadyOnce
143-
if (stockfish.state.value == StockfishState.ready) {
144-
onReadyOnce();
145-
}
146-
147-
_protocol.isComputing.addListener(() {
148-
if (_protocol.isComputing.value) {
149-
_state.value = EngineState.computing;
150-
} else {
151-
_state.value = EngineState.idle;
152-
}
153-
});
154-
_protocol.engineName.then((name) {
155-
_name = name;
156-
});
157-
} catch (e, s) {
158-
_stockfish = null;
159-
_log.severe('error loading stockfish', e, s);
160-
_state.value = EngineState.error;
161-
}
162-
}
163-
164-
return _protocol.evalStream.where((e) => e.$1 == work);
165-
}
166-
167-
void _stockfishStateListener() {
168-
switch (_stockfish?.state.value) {
169-
case StockfishState.ready:
170-
_state.value = EngineState.idle;
171-
case StockfishState.error:
172-
_state.value = EngineState.error;
173-
case StockfishState.disposed:
174-
_log.info('engine disposed');
175-
_state.value = EngineState.disposed;
176-
_exitCompleter.complete();
177-
_stockfish?.state.removeListener(_stockfishStateListener);
178-
_state.dispose();
179-
default:
180-
// do nothing
181-
}
182-
}
183-
184-
@override
185-
void stop() {
186-
if (isDisposed) {
187-
throw StateError('Engine is disposed');
188-
}
189-
_protocol.compute(null);
190-
}
191-
192-
@override
193-
Future<void> dispose() {
194-
if (isDisposed) {
195-
return exited;
196-
}
197-
_log.fine('disposing engine');
198-
_isDisposed = true;
199-
_stdoutSubscription?.cancel();
200-
_protocol.dispose();
201-
if (_stockfish != null) {
202-
switch (_stockfish!.state.value) {
203-
case StockfishState.disposed:
204-
case StockfishState.error:
205-
if (_exitCompleter.isCompleted == false) {
206-
_exitCompleter.complete();
207-
}
208-
case StockfishState.ready:
209-
_stockfish!.dispose();
210-
case StockfishState.initial:
211-
case StockfishState.starting:
212-
// wait to be ready or error, then dispose
213-
void onReadyOrErrorOnce() {
214-
final currentState = _stockfish!.state.value;
215-
if (currentState == StockfishState.ready) {
216-
_stockfish!.dispose();
217-
_stockfish!.state.removeListener(onReadyOrErrorOnce);
218-
} else if (currentState == StockfishState.error ||
219-
currentState == StockfishState.disposed) {
220-
if (_exitCompleter.isCompleted == false) {
221-
_exitCompleter.complete();
222-
}
223-
_stockfish!.state.removeListener(onReadyOrErrorOnce);
224-
}
225-
}
226-
_stockfish!.state.addListener(onReadyOrErrorOnce);
227-
// Check immediately in case state already transitioned
228-
onReadyOrErrorOnce();
229-
}
230-
} else {
231-
if (_exitCompleter.isCompleted == false) {
232-
_exitCompleter.complete();
233-
}
234-
}
235-
return exited;
236-
}
237-
}
238-
239-
/// A factory to create a [Stockfish].
240-
///
241-
/// This is useful to be able to mock [Stockfish] in tests.
242-
class StockfishFactory {
243-
const StockfishFactory();
6+
/// URL to download the latest big NNUE network.
7+
final bigNetUrl = Uri.parse('$_nnueDownloadUrl${Stockfish.latestBigNNUE}');
2448

245-
Stockfish call(
246-
StockfishFlavor flavor, {
9+
/// SHA256 hash (first 12 digits) of the latest big NNUE network.
10+
final bigNetHash = Stockfish.latestBigNNUE.substring(3, 15);
24711

248-
/// Full path to the small net file for NNUE evaluation.
249-
String? smallNetPath,
12+
/// URL to download the latest small NNUE network.
13+
final smallNetUrl = Uri.parse('$_nnueDownloadUrl${Stockfish.latestSmallNNUE}');
25014

251-
/// Full path to the big net file for NNUE evaluation.
252-
String? bigNetPath,
253-
}) => Stockfish(flavor: flavor, smallNetPath: smallNetPath, bigNetPath: bigNetPath);
254-
}
15+
/// SHA256 hash (first 12 digits) of the latest small NNUE network.
16+
final smallNetHash = Stockfish.latestSmallNNUE.substring(3, 15);

0 commit comments

Comments
 (0)