Rafs-an09002 commited on
Commit
4357f6e
·
verified ·
1 Parent(s): f47c631

Create engine/search.py

Browse files
Files changed (1) hide show
  1. engine/search.py +338 -202
engine/search.py CHANGED
@@ -1,289 +1,424 @@
1
  """
2
  Nexus-Core Search Engine
3
- Efficient alpha-beta with essential optimizations
4
- - Basic transposition table
5
- - Simple move ordering (MVV-LVA)
6
- - Quiescence search
 
 
 
7
  """
8
 
9
- import onnxruntime as ort
10
- import numpy as np
11
  import chess
12
  import time
13
  import logging
14
- from pathlib import Path
15
- from typing import Optional, Dict, Tuple, List
 
 
 
 
 
16
 
17
  logger = logging.getLogger(__name__)
18
 
19
 
20
  class NexusCoreEngine:
21
- """
22
- Lightweight chess engine for Nexus-Core
23
- Optimized for speed over strength
24
- """
25
 
26
- PIECE_VALUES = {
27
- chess.PAWN: 100,
28
- chess.KNIGHT: 320,
29
- chess.BISHOP: 330,
30
- chess.ROOK: 500,
31
- chess.QUEEN: 900,
32
- chess.KING: 0
33
- }
34
 
35
  def __init__(self, model_path: str, num_threads: int = 2):
36
  """Initialize engine"""
37
 
38
- self.model_path = Path(model_path)
39
- if not self.model_path.exists():
40
- raise FileNotFoundError(f"Model not found: {model_path}")
41
-
42
- # Load ONNX model
43
- sess_options = ort.SessionOptions()
44
- sess_options.intra_op_num_threads = num_threads
45
- sess_options.inter_op_num_threads = num_threads
46
- sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
47
-
48
- logger.info(f"Loading Nexus-Core from {model_path}...")
49
- self.session = ort.InferenceSession(
50
- str(self.model_path),
51
- sess_options=sess_options,
52
- providers=['CPUExecutionProvider']
53
- )
54
-
55
- self.input_name = self.session.get_inputs()[0].name
56
- self.output_name = self.session.get_outputs()[0].name
57
-
58
- # Simple transposition table (dict-based, 100K entries)
59
- self.tt_cache = {}
60
- self.max_tt_size = 100000
61
 
62
  # Statistics
63
  self.nodes_evaluated = 0
 
 
 
64
 
65
- logger.info(" Nexus-Core engine ready")
 
 
66
 
67
- def fen_to_tensor(self, fen: str) -> np.ndarray:
68
- """Convert FEN to 12-channel tensor"""
69
- board = chess.Board(fen)
70
- tensor = np.zeros((1, 12, 8, 8), dtype=np.float32)
 
 
 
 
71
 
72
- piece_to_channel = {
73
- chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2,
74
- chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5
75
- }
76
 
77
- for square, piece in board.piece_map().items():
78
- rank, file = divmod(square, 8)
79
- channel = piece_to_channel[piece.piece_type]
80
- if piece.color == chess.BLACK:
81
- channel += 6
82
- tensor[0, channel, rank, file] = 1.0
83
 
84
- return tensor
85
-
86
- def evaluate(self, board: chess.Board) -> float:
87
- """Neural network evaluation"""
88
- self.nodes_evaluated += 1
89
 
90
- # Check cache (simple FEN-based)
91
- fen_key = board.fen().split(' ')[0]
92
- if fen_key in self.tt_cache:
93
- return self.tt_cache[fen_key]
 
 
 
 
 
94
 
95
- # Run inference
96
- input_tensor = self.fen_to_tensor(board.fen())
97
- output = self.session.run([self.output_name], {self.input_name: input_tensor})
98
 
99
- # Value output (tanh normalized)
100
- eval_score = float(output[0][0][0]) * 400.0 # Scale to centipawns
101
 
102
- # Flip for black
103
- if board.turn == chess.BLACK:
104
- eval_score = -eval_score
105
 
106
- # Cache result
107
- if len(self.tt_cache) < self.max_tt_size:
108
- self.tt_cache[fen_key] = eval_score
109
 
110
- return eval_score
111
-
112
- def order_moves(self, board: chess.Board, moves: List[chess.Move]) -> List[chess.Move]:
113
- """
114
- Simple move ordering
115
- 1. Captures (MVV-LVA)
116
- 2. Checks
117
- 3. Other moves
118
- """
119
- scored_moves = []
120
 
121
- for move in moves:
122
- score = 0
 
123
 
124
- # Captures
125
- if board.is_capture(move):
126
- victim = board.piece_at(move.to_square)
127
- attacker = board.piece_at(move.from_square)
128
- if victim and attacker:
129
- victim_val = self.PIECE_VALUES.get(victim.piece_type, 0)
130
- attacker_val = self.PIECE_VALUES.get(attacker.piece_type, 1)
131
- score = (victim_val * 10 - attacker_val) * 100
132
 
133
- # Promotions
134
- if move.promotion == chess.QUEEN:
135
- score += 9000
 
136
 
137
- # Checks
138
- board.push(move)
139
- if board.is_check():
140
- score += 5000
141
- board.pop()
142
 
143
- scored_moves.append((score, move))
 
 
 
 
 
 
 
 
 
 
144
 
145
- scored_moves.sort(key=lambda x: x[0], reverse=True)
146
- return [move for _, move in scored_moves]
 
 
 
 
 
 
 
 
 
 
147
 
148
- def quiescence(self, board: chess.Board, alpha: float, beta: float, depth: int = 2) -> float:
149
- """Quiescence search (captures only)"""
150
-
151
- stand_pat = self.evaluate(board)
 
 
 
 
152
 
153
- if stand_pat >= beta:
154
- return beta
155
- if alpha < stand_pat:
156
- alpha = stand_pat
157
 
158
- if depth == 0:
159
- return stand_pat
 
 
160
 
161
- # Only captures
162
- captures = [m for m in board.legal_moves if board.is_capture(m)]
163
- if not captures:
164
- return stand_pat
165
 
166
- captures = self.order_moves(board, captures)
 
 
167
 
168
- for move in captures:
169
  board.push(move)
170
- score = -self.quiescence(board, -beta, -alpha, depth - 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  board.pop()
172
 
173
- if score >= beta:
174
- return beta
 
 
 
175
  if score > alpha:
176
  alpha = score
 
 
 
177
 
178
- return alpha
 
 
 
179
 
180
- def alpha_beta(
181
  self,
182
  board: chess.Board,
183
  depth: int,
184
  alpha: float,
185
  beta: float,
186
- start_time: float,
187
- time_limit: float
188
- ) -> Tuple[float, Optional[chess.Move]]:
189
- """Alpha-beta search"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- # Time check
192
- if time.time() - start_time > time_limit:
193
- return self.evaluate(board), None
194
 
195
- # Terminal nodes
196
- if board.is_game_over():
197
- if board.is_checkmate():
198
- return -10000, None
199
- return 0, None
200
 
201
- # Leaf nodes
202
- if depth == 0:
203
- return self.quiescence(board, alpha, beta), None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
 
205
  legal_moves = list(board.legal_moves)
206
  if not legal_moves:
207
- return 0, None
 
 
208
 
209
- ordered_moves = self.order_moves(board, legal_moves)
 
 
210
 
211
- best_move = ordered_moves[0]
212
  best_score = float('-inf')
 
 
 
213
 
214
  for move in ordered_moves:
215
  board.push(move)
216
- score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha, start_time, time_limit)
217
- score = -score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  board.pop()
 
219
 
220
  if score > best_score:
221
  best_score = score
222
- best_move = move
223
-
224
- alpha = max(alpha, score)
225
- if alpha >= beta:
226
- break
227
-
228
- return best_score, best_move
 
 
 
 
 
 
 
 
 
 
229
 
230
- def get_best_move(self, fen: str, depth: int = 4, time_limit: int = 3000) -> Dict:
231
- """Main search entry"""
 
 
 
 
 
 
232
 
233
- board = chess.Board(fen)
234
- self.nodes_evaluated = 0
235
 
236
- time_limit_sec = time_limit / 1000.0
237
- start_time = time.time()
 
238
 
239
- # Special cases
240
- legal_moves = list(board.legal_moves)
241
- if len(legal_moves) == 0:
242
- return {'best_move': '0000', 'evaluation': 0.0, 'depth_searched': 0, 'nodes_evaluated': 0}
243
 
244
- if len(legal_moves) == 1:
245
- return {
246
- 'best_move': legal_moves[0].uci(),
247
- 'evaluation': round(self.evaluate(board) / 100.0, 2),
248
- 'depth_searched': 0,
249
- 'nodes_evaluated': 1,
250
- 'time_taken': 0
251
- }
252
 
253
- # Iterative deepening
254
- best_move = legal_moves[0]
255
- best_score = float('-inf')
 
 
256
 
257
- for current_depth in range(1, depth + 1):
258
- if time.time() - start_time > time_limit_sec * 0.9:
259
- break
 
 
 
 
 
 
260
 
261
- try:
262
- score, move = self.alpha_beta(
263
- board, current_depth,
264
- float('-inf'), float('inf'),
265
- start_time, time_limit_sec
266
- )
267
-
268
- if move:
269
- best_move = move
270
- best_score = score
271
-
272
- except Exception as e:
273
- logger.warning(f"Search error: {e}")
274
- break
275
 
276
- time_taken = int((time.time() - start_time) * 1000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
  return {
279
- 'best_move': best_move.uci(),
280
- 'evaluation': round(best_score / 100.0, 2),
281
- 'depth_searched': current_depth,
282
- 'nodes_evaluated': self.nodes_evaluated,
283
- 'time_taken': time_taken
 
284
  }
285
 
286
  def validate_fen(self, fen: str) -> bool:
 
287
  try:
288
  chess.Board(fen)
289
  return True
@@ -291,4 +426,5 @@ class NexusCoreEngine:
291
  return False
292
 
293
  def get_model_size(self) -> float:
294
- return self.model_path.stat().st_size / (1024 * 1024)
 
 
1
  """
2
  Nexus-Core Search Engine
3
+ PVS with advanced pruning techniques
4
+
5
+ Research Implementation:
6
+ - Principal Variation Search (Marsland, 1986)
7
+ - Null Move Pruning (Donninger, 1993)
8
+ - Late Move Reductions (Heinz, 2000)
9
+ - Quiescence Search (Harris, 1975)
10
  """
11
 
 
 
12
  import chess
13
  import time
14
  import logging
15
+ from typing import Optional, Tuple, List, Dict
16
+
17
+ from .evaluate import NexusCoreEvaluator
18
+ from .transposition import TranspositionTable, NodeType
19
+ from .move_ordering import MoveOrderer
20
+ from .time_manager import TimeManager
21
+ from .endgame import EndgameDetector
22
 
23
  logger = logging.getLogger(__name__)
24
 
25
 
26
  class NexusCoreEngine:
27
+ """Nexus-Core chess engine with 13.2M parameter neural network"""
28
+
29
+ MATE_SCORE = 100000
30
+ MAX_PLY = 100
31
 
32
+ # Pruning parameters
33
+ NULL_MOVE_REDUCTION = 2
34
+ NULL_MOVE_MIN_DEPTH = 3
35
+ LMR_MIN_DEPTH = 3
36
+ LMR_MOVE_THRESHOLD = 4
37
+ ASPIRATION_WINDOW = 50
 
 
38
 
39
  def __init__(self, model_path: str, num_threads: int = 2):
40
  """Initialize engine"""
41
 
42
+ self.evaluator = NexusCoreEvaluator(model_path, num_threads)
43
+ self.tt = TranspositionTable(size_mb=128) # 128MB TT
44
+ self.move_orderer = MoveOrderer()
45
+ self.time_manager = TimeManager()
46
+ self.endgame_detector = EndgameDetector()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  # Statistics
49
  self.nodes_evaluated = 0
50
+ self.depth_reached = 0
51
+ self.sel_depth = 0
52
+ self.principal_variation = []
53
 
54
+ logger.info("🎯 Nexus-Core Engine initialized")
55
+ logger.info(f" Model: {self.evaluator.get_model_size_mb():.2f} MB")
56
+ logger.info(f" TT Size: 128 MB")
57
 
58
+ def get_best_move(
59
+ self,
60
+ fen: str,
61
+ depth: int = 5,
62
+ time_limit: int = 3000
63
+ ) -> Dict:
64
+ """
65
+ Main search entry point
66
 
67
+ Args:
68
+ fen: Position in FEN
69
+ depth: Max search depth
70
+ time_limit: Time limit in ms
71
 
72
+ Returns:
73
+ Dictionary with best_move and stats
74
+ """
 
 
 
75
 
76
+ board = chess.Board(fen)
 
 
 
 
77
 
78
+ # Reset stats
79
+ self.nodes_evaluated = 0
80
+ self.depth_reached = 0
81
+ self.sel_depth = 0
82
+ self.principal_variation = []
83
+
84
+ # Time management
85
+ time_limit_sec = time_limit / 1000.0
86
+ self.time_manager.start_search(time_limit_sec, time_limit_sec)
87
 
88
+ # Age history
89
+ self.move_orderer.age_history(0.95)
90
+ self.tt.increment_age()
91
 
92
+ # Special cases
93
+ legal_moves = list(board.legal_moves)
94
 
95
+ if len(legal_moves) == 0:
96
+ return self._no_legal_moves_result()
 
97
 
98
+ if len(legal_moves) == 1:
99
+ return self._single_move_result(board, legal_moves[0])
 
100
 
101
+ # Iterative deepening
102
+ best_move = legal_moves[0]
103
+ best_score = float('-inf')
104
+ alpha = float('-inf')
105
+ beta = float('inf')
 
 
 
 
 
106
 
107
+ for current_depth in range(1, depth + 1):
108
+ if self.time_manager.should_stop(current_depth):
109
+ break
110
 
111
+ # Aspiration windows for depth >= 4
112
+ if current_depth >= 4 and abs(best_score) < self.MATE_SCORE - 1000:
113
+ alpha = best_score - self.ASPIRATION_WINDOW
114
+ beta = best_score + self.ASPIRATION_WINDOW
115
+ else:
116
+ alpha = float('-inf')
117
+ beta = float('inf')
 
118
 
119
+ # Search
120
+ score, move, pv = self._search_root(
121
+ board, current_depth, alpha, beta
122
+ )
123
 
124
+ # Handle aspiration failures
125
+ if score <= alpha or score >= beta:
126
+ score, move, pv = self._search_root(
127
+ board, current_depth, float('-inf'), float('inf')
128
+ )
129
 
130
+ # Update best
131
+ if move:
132
+ best_move = move
133
+ best_score = score
134
+ self.depth_reached = current_depth
135
+ self.principal_variation = pv
136
+
137
+ logger.info(
138
+ f"Depth {current_depth}: {move.uci()} "
139
+ f"({score:+.2f}) | Nodes: {self.nodes_evaluated}"
140
+ )
141
 
142
+ return {
143
+ 'best_move': best_move.uci(),
144
+ 'evaluation': round(best_score / 100.0, 2),
145
+ 'depth_searched': self.depth_reached,
146
+ 'seldepth': self.sel_depth,
147
+ 'nodes_evaluated': self.nodes_evaluated,
148
+ 'time_taken': int(self.time_manager.elapsed() * 1000),
149
+ 'pv': [m.uci() for m in self.principal_variation],
150
+ 'nps': int(self.nodes_evaluated / max(self.time_manager.elapsed(), 0.001)),
151
+ 'tt_stats': self.tt.get_stats(),
152
+ 'move_ordering_stats': self.move_orderer.get_stats()
153
+ }
154
 
155
+ def _search_root(
156
+ self,
157
+ board: chess.Board,
158
+ depth: int,
159
+ alpha: float,
160
+ beta: float
161
+ ) -> Tuple[float, Optional[chess.Move], List[chess.Move]]:
162
+ """Root node search"""
163
 
164
+ legal_moves = list(board.legal_moves)
 
 
 
165
 
166
+ # TT probe
167
+ zobrist_key = self.tt.compute_zobrist_key(board)
168
+ tt_result = self.tt.probe(zobrist_key, depth, alpha, beta)
169
+ tt_move = tt_result[1] if tt_result else None
170
 
171
+ # Order moves
172
+ ordered_moves = self.move_orderer.order_moves(
173
+ board, legal_moves, depth, tt_move
174
+ )
175
 
176
+ best_move = ordered_moves[0]
177
+ best_score = float('-inf')
178
+ best_pv = []
179
 
180
+ for i, move in enumerate(ordered_moves):
181
  board.push(move)
182
+
183
+ if i == 0:
184
+ score, pv = self._pvs(
185
+ board, depth - 1, -beta, -alpha, True
186
+ )
187
+ score = -score
188
+ else:
189
+ score, _ = self._pvs(
190
+ board, depth - 1, -alpha - 1, -alpha, False
191
+ )
192
+ score = -score
193
+
194
+ if alpha < score < beta:
195
+ score, pv = self._pvs(
196
+ board, depth - 1, -beta, -alpha, True
197
+ )
198
+ score = -score
199
+ else:
200
+ pv = []
201
+
202
  board.pop()
203
 
204
+ if score > best_score:
205
+ best_score = score
206
+ best_move = move
207
+ best_pv = [move] + pv
208
+
209
  if score > alpha:
210
  alpha = score
211
+
212
+ if self.time_manager.should_stop(depth):
213
+ break
214
 
215
+ # Store in TT
216
+ self.tt.store(zobrist_key, depth, best_score, NodeType.EXACT, best_move)
217
+
218
+ return best_score, best_move, best_pv
219
 
220
+ def _pvs(
221
  self,
222
  board: chess.Board,
223
  depth: int,
224
  alpha: float,
225
  beta: float,
226
+ do_null: bool
227
+ ) -> Tuple[float, List[chess.Move]]:
228
+ """Principal Variation Search"""
229
+
230
+ self.sel_depth = max(self.sel_depth, self.MAX_PLY - depth)
231
+
232
+ # Mate distance pruning
233
+ alpha = max(alpha, -self.MATE_SCORE + (self.MAX_PLY - depth))
234
+ beta = min(beta, self.MATE_SCORE - (self.MAX_PLY - depth) - 1)
235
+ if alpha >= beta:
236
+ return alpha, []
237
+
238
+ # Draw detection
239
+ if board.is_repetition(2) or board.is_fifty_moves():
240
+ return 0, []
241
+
242
+ # TT probe
243
+ zobrist_key = self.tt.compute_zobrist_key(board)
244
+ tt_result = self.tt.probe(zobrist_key, depth, alpha, beta)
245
 
246
+ if tt_result and tt_result[0] is not None:
247
+ return tt_result[0], []
 
248
 
249
+ tt_move = tt_result[1] if tt_result else None
 
 
 
 
250
 
251
+ # Quiescence search
252
+ if depth <= 0:
253
+ return self._quiescence(board, alpha, beta, 0), []
254
+
255
+ # Null move pruning
256
+ if (do_null and
257
+ depth >= self.NULL_MOVE_MIN_DEPTH and
258
+ not board.is_check() and
259
+ self._has_non_pawn_material(board)):
260
+
261
+ board.push(chess.Move.null())
262
+ score, _ = self._pvs(
263
+ board, depth - 1 - self.NULL_MOVE_REDUCTION,
264
+ -beta, -beta + 1, False
265
+ )
266
+ score = -score
267
+ board.pop()
268
+
269
+ if score >= beta:
270
+ return beta, []
271
 
272
+ # Generate moves
273
  legal_moves = list(board.legal_moves)
274
  if not legal_moves:
275
+ if board.is_check():
276
+ return -self.MATE_SCORE + (self.MAX_PLY - depth), []
277
+ return 0, []
278
 
279
+ ordered_moves = self.move_orderer.order_moves(
280
+ board, legal_moves, depth, tt_move
281
+ )
282
 
283
+ # Main search
284
  best_score = float('-inf')
285
+ best_pv = []
286
+ node_type = NodeType.UPPER_BOUND
287
+ moves_searched = 0
288
 
289
  for move in ordered_moves:
290
  board.push(move)
291
+
292
+ # Late move reductions
293
+ reduction = 0
294
+ if (moves_searched >= self.LMR_MOVE_THRESHOLD and
295
+ depth >= self.LMR_MIN_DEPTH and
296
+ not board.is_check() and
297
+ not board.is_capture(board.peek())):
298
+ reduction = 1
299
+
300
+ # PVS
301
+ if moves_searched == 0:
302
+ score, pv = self._pvs(
303
+ board, depth - 1, -beta, -alpha, True
304
+ )
305
+ score = -score
306
+ else:
307
+ score, _ = self._pvs(
308
+ board, depth - 1 - reduction, -alpha - 1, -alpha, True
309
+ )
310
+ score = -score
311
+
312
+ if alpha < score < beta:
313
+ score, pv = self._pvs(
314
+ board, depth - 1, -beta, -alpha, True
315
+ )
316
+ score = -score
317
+ else:
318
+ pv = []
319
+
320
  board.pop()
321
+ moves_searched += 1
322
 
323
  if score > best_score:
324
  best_score = score
325
+ best_pv = [move] + pv
326
+
327
+ if score > alpha:
328
+ alpha = score
329
+ node_type = NodeType.EXACT
330
+
331
+ if not board.is_capture(move):
332
+ self.move_orderer.update_history(move, depth, True)
333
+ self.move_orderer.update_killer_move(move, depth)
334
+
335
+ if score >= beta:
336
+ node_type = NodeType.LOWER_BOUND
337
+ break
338
+
339
+ self.tt.store(zobrist_key, depth, best_score, node_type, best_pv[0] if best_pv else None)
340
+
341
+ return best_score, best_pv
342
 
343
+ def _quiescence(
344
+ self,
345
+ board: chess.Board,
346
+ alpha: float,
347
+ beta: float,
348
+ qs_depth: int
349
+ ) -> float:
350
+ """Quiescence search"""
351
 
352
+ self.nodes_evaluated += 1
 
353
 
354
+ # Stand-pat
355
+ stand_pat = self.evaluator.evaluate_hybrid(board)
356
+ stand_pat = self.endgame_detector.adjust_evaluation(board, stand_pat)
357
 
358
+ if stand_pat >= beta:
359
+ return beta
360
+ if alpha < stand_pat:
361
+ alpha = stand_pat
362
 
363
+ # Depth limit
364
+ if qs_depth >= 8:
365
+ return stand_pat
 
 
 
 
 
366
 
367
+ # Tactical moves
368
+ tactical_moves = [
369
+ move for move in board.legal_moves
370
+ if board.is_capture(move) or board.gives_check(move)
371
+ ]
372
 
373
+ if not tactical_moves:
374
+ return stand_pat
375
+
376
+ tactical_moves = self.move_orderer.order_moves(board, tactical_moves, 0)
377
+
378
+ for move in tactical_moves:
379
+ board.push(move)
380
+ score = -self._quiescence(board, -beta, -alpha, qs_depth + 1)
381
+ board.pop()
382
 
383
+ if score >= beta:
384
+ return beta
385
+ if score > alpha:
386
+ alpha = score
 
 
 
 
 
 
 
 
 
 
387
 
388
+ return alpha
389
+
390
+ def _has_non_pawn_material(self, board: chess.Board) -> bool:
391
+ """Check for non-pawn material"""
392
+ for piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
393
+ if board.pieces(piece_type, board.turn):
394
+ return True
395
+ return False
396
+
397
+ def _no_legal_moves_result(self) -> Dict:
398
+ """No legal moves result"""
399
+ return {
400
+ 'best_move': '0000',
401
+ 'evaluation': 0.0,
402
+ 'depth_searched': 0,
403
+ 'nodes_evaluated': 0,
404
+ 'time_taken': 0
405
+ }
406
+
407
+ def _single_move_result(self, board: chess.Board, move: chess.Move) -> Dict:
408
+ """Single move result"""
409
+ eval_score = self.evaluator.evaluate_hybrid(board)
410
 
411
  return {
412
+ 'best_move': move.uci(),
413
+ 'evaluation': round(eval_score / 100.0, 2),
414
+ 'depth_searched': 0,
415
+ 'nodes_evaluated': 1,
416
+ 'time_taken': 0,
417
+ 'pv': [move.uci()]
418
  }
419
 
420
  def validate_fen(self, fen: str) -> bool:
421
+ """Validate FEN"""
422
  try:
423
  chess.Board(fen)
424
  return True
 
426
  return False
427
 
428
  def get_model_size(self) -> float:
429
+ """Get model size"""
430
+ return self.evaluator.get_model_size_mb()