|
1 | | -import 'dart:async'; |
2 | | - |
3 | | -import 'package:flutter/foundation.dart'; |
4 | | -import 'package:lichess_mobile/src/binding.dart'; |
5 | 1 | 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'; |
9 | 2 | import 'package:multistockfish/multistockfish.dart'; |
10 | 3 |
|
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 | | - |
48 | 4 | const _nnueDownloadUrl = '$kLichessCDNHost/assets/lifat/nnue/'; |
49 | 5 |
|
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}'); |
244 | 8 |
|
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); |
247 | 11 |
|
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}'); |
250 | 14 |
|
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