Rafs-an09002 commited on
Commit
8a2f169
·
verified ·
1 Parent(s): b8f4b95

Create engine/search.py

Browse files
Files changed (1) hide show
  1. engine/search.py +469 -0
engine/search.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Synapse-Base Main Search Engine
3
+ State-of-the-art alpha-beta with advanced enhancements
4
+
5
+ Research Implementation:
6
+ - Alpha-Beta with PVS (Principal Variation Search)
7
+ - Aspiration Windows
8
+ - Null Move Pruning
9
+ - Late Move Reductions (LMR)
10
+ - Quiescence Search with SEE
11
+ - Iterative Deepening
12
+ - Transposition Table with Zobrist
13
+ - Advanced Move Ordering
14
+ """
15
+
16
+ import chess
17
+ import time
18
+ import logging
19
+ from typing import Optional, Tuple, List, Dict
20
+
21
+ from .evaluate import NeuralEvaluator
22
+ from .transposition import TranspositionTable, NodeType
23
+ from .move_ordering import MoveOrderer
24
+ from .time_manager import TimeManager
25
+ from .endgame import EndgameDetector
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class SynapseEngine:
31
+ """
32
+ State-of-the-art chess engine with neural evaluation
33
+ """
34
+
35
+ # Search constants (tuned values from research)
36
+ MATE_SCORE = 100000
37
+ MAX_PLY = 100
38
+
39
+ # Null move pruning parameters
40
+ NULL_MOVE_REDUCTION = 2
41
+ NULL_MOVE_MIN_DEPTH = 3
42
+
43
+ # Late move reduction parameters
44
+ LMR_MIN_DEPTH = 3
45
+ LMR_MOVE_THRESHOLD = 4
46
+
47
+ # Aspiration window size
48
+ ASPIRATION_WINDOW = 50
49
+
50
+ def __init__(self, model_path: str, num_threads: int = 2):
51
+ """Initialize engine components"""
52
+
53
+ # Core components
54
+ self.evaluator = NeuralEvaluator(model_path, num_threads)
55
+ self.tt = TranspositionTable(size_mb=256) # 256MB TT
56
+ self.move_orderer = MoveOrderer()
57
+ self.time_manager = TimeManager()
58
+ self.endgame_detector = EndgameDetector()
59
+
60
+ # Search statistics
61
+ self.nodes_evaluated = 0
62
+ self.depth_reached = 0
63
+ self.sel_depth = 0 # Selective depth (quiescence)
64
+ self.principal_variation = []
65
+
66
+ logger.info("🎯 Synapse-Base Engine initialized")
67
+ logger.info(f" Model: {self.evaluator.get_model_size_mb():.2f} MB")
68
+ logger.info(f" TT Size: 256 MB")
69
+
70
+ def get_best_move(
71
+ self,
72
+ fen: str,
73
+ depth: int = 5,
74
+ time_limit: int = 5000
75
+ ) -> Dict:
76
+ """
77
+ Main search entry point
78
+
79
+ Args:
80
+ fen: Position in FEN notation
81
+ depth: Maximum search depth
82
+ time_limit: Time limit in milliseconds
83
+
84
+ Returns:
85
+ Dictionary with best_move, evaluation, stats
86
+ """
87
+ board = chess.Board(fen)
88
+
89
+ # Reset statistics
90
+ self.nodes_evaluated = 0
91
+ self.depth_reached = 0
92
+ self.sel_depth = 0
93
+ self.principal_variation = []
94
+
95
+ # Time management
96
+ time_limit_sec = time_limit / 1000.0
97
+ self.time_manager.start_search(time_limit_sec, time_limit_sec)
98
+
99
+ # Age history for new search
100
+ self.move_orderer.age_history(0.95)
101
+ self.tt.increment_age()
102
+
103
+ # Special cases
104
+ legal_moves = list(board.legal_moves)
105
+ if len(legal_moves) == 0:
106
+ return self._no_legal_moves_result()
107
+
108
+ if len(legal_moves) == 1:
109
+ return self._single_move_result(board, legal_moves[0])
110
+
111
+ # Iterative deepening with aspiration windows
112
+ best_move = legal_moves[0]
113
+ best_score = float('-inf')
114
+ alpha = float('-inf')
115
+ beta = float('inf')
116
+
117
+ for current_depth in range(1, depth + 1):
118
+ # Time check
119
+ if self.time_manager.should_stop(current_depth):
120
+ break
121
+
122
+ # Aspiration window for depth >= 4
123
+ if current_depth >= 4 and abs(best_score) < self.MATE_SCORE - 1000:
124
+ alpha = best_score - self.ASPIRATION_WINDOW
125
+ beta = best_score + self.ASPIRATION_WINDOW
126
+ else:
127
+ alpha = float('-inf')
128
+ beta = float('inf')
129
+
130
+ # Search with aspiration window
131
+ score, move, pv = self._search_root(
132
+ board, current_depth, alpha, beta
133
+ )
134
+
135
+ # Handle aspiration window failures
136
+ if score <= alpha or score >= beta:
137
+ # Research with full window
138
+ score, move, pv = self._search_root(
139
+ board, current_depth, float('-inf'), float('inf')
140
+ )
141
+
142
+ # Update best move
143
+ if move:
144
+ best_move = move
145
+ best_score = score
146
+ self.depth_reached = current_depth
147
+ self.principal_variation = pv
148
+
149
+ logger.info(
150
+ f"Depth {current_depth}: {move.uci()} "
151
+ f"({score:+.2f}) | Nodes: {self.nodes_evaluated} | "
152
+ f"Time: {self.time_manager.elapsed():.2f}s"
153
+ )
154
+
155
+ # Return result
156
+ return {
157
+ 'best_move': best_move.uci(),
158
+ 'evaluation': round(best_score / 100.0, 2), # Convert to pawns
159
+ 'depth_searched': self.depth_reached,
160
+ 'seldepth': self.sel_depth,
161
+ 'nodes_evaluated': self.nodes_evaluated,
162
+ 'time_taken': int(self.time_manager.elapsed() * 1000),
163
+ 'pv': [m.uci() for m in self.principal_variation],
164
+ 'nps': int(self.nodes_evaluated / max(self.time_manager.elapsed(), 0.001)),
165
+ 'tt_stats': self.tt.get_stats(),
166
+ 'move_ordering_stats': self.move_orderer.get_stats()
167
+ }
168
+
169
+ def _search_root(
170
+ self,
171
+ board: chess.Board,
172
+ depth: int,
173
+ alpha: float,
174
+ beta: float
175
+ ) -> Tuple[float, Optional[chess.Move], List[chess.Move]]:
176
+ """Root node search with PVS"""
177
+
178
+ legal_moves = list(board.legal_moves)
179
+
180
+ # TT probe
181
+ zobrist_key = self.tt.compute_zobrist_key(board)
182
+ tt_result = self.tt.probe(zobrist_key, depth, alpha, beta)
183
+ tt_move = tt_result[1] if tt_result else None
184
+
185
+ # Order moves
186
+ ordered_moves = self.move_orderer.order_moves(
187
+ board, legal_moves, depth, tt_move
188
+ )
189
+
190
+ best_move = ordered_moves[0]
191
+ best_score = float('-inf')
192
+ best_pv = []
193
+
194
+ for i, move in enumerate(ordered_moves):
195
+ board.push(move)
196
+
197
+ if i == 0:
198
+ # Full window search for first move (PV node)
199
+ score, pv = self._pvs(
200
+ board, depth - 1, -beta, -alpha, True
201
+ )
202
+ score = -score
203
+ else:
204
+ # Null window search for remaining moves
205
+ score, _ = self._pvs(
206
+ board, depth - 1, -alpha - 1, -alpha, False
207
+ )
208
+ score = -score
209
+
210
+ # Re-search if failed high
211
+ if alpha < score < beta:
212
+ score, pv = self._pvs(
213
+ board, depth - 1, -beta, -alpha, True
214
+ )
215
+ score = -score
216
+ else:
217
+ pv = []
218
+
219
+ board.pop()
220
+
221
+ # Update best
222
+ if score > best_score:
223
+ best_score = score
224
+ best_move = move
225
+ best_pv = [move] + pv
226
+
227
+ # Update alpha
228
+ if score > alpha:
229
+ alpha = score
230
+
231
+ # Time check
232
+ if self.time_manager.should_stop(depth):
233
+ break
234
+
235
+ # Store in TT
236
+ self.tt.store(
237
+ zobrist_key, depth, best_score,
238
+ NodeType.EXACT, best_move
239
+ )
240
+
241
+ return best_score, best_move, best_pv
242
+
243
+ def _pvs(
244
+ self,
245
+ board: chess.Board,
246
+ depth: int,
247
+ alpha: float,
248
+ beta: float,
249
+ do_null: bool
250
+ ) -> Tuple[float, List[chess.Move]]:
251
+ """
252
+ Principal Variation Search (PVS) with alpha-beta
253
+
254
+ Enhanced with:
255
+ - Null move pruning
256
+ - Late move reductions
257
+ - Transposition table
258
+ """
259
+ self.sel_depth = max(self.sel_depth, self.MAX_PLY - depth)
260
+
261
+ # Mate distance pruning
262
+ alpha = max(alpha, -self.MATE_SCORE + (self.MAX_PLY - depth))
263
+ beta = min(beta, self.MATE_SCORE - (self.MAX_PLY - depth) - 1)
264
+ if alpha >= beta:
265
+ return alpha, []
266
+
267
+ # Draw detection
268
+ if board.is_repetition(2) or board.is_fifty_moves():
269
+ return 0, []
270
+
271
+ # TT probe
272
+ zobrist_key = self.tt.compute_zobrist_key(board)
273
+ tt_result = self.tt.probe(zobrist_key, depth, alpha, beta)
274
+
275
+ if tt_result and tt_result[0] is not None:
276
+ return tt_result[0], []
277
+
278
+ tt_move = tt_result[1] if tt_result else None
279
+
280
+ # Quiescence search at leaf nodes
281
+ if depth <= 0:
282
+ return self._quiescence(board, alpha, beta, 0), []
283
+
284
+ # Null move pruning
285
+ if (do_null and
286
+ depth >= self.NULL_MOVE_MIN_DEPTH and
287
+ not board.is_check() and
288
+ self._has_non_pawn_material(board)):
289
+
290
+ board.push(chess.Move.null())
291
+ score, _ = self._pvs(
292
+ board, depth - 1 - self.NULL_MOVE_REDUCTION,
293
+ -beta, -beta + 1, False
294
+ )
295
+ score = -score
296
+ board.pop()
297
+
298
+ if score >= beta:
299
+ return beta, []
300
+
301
+ # Generate and order moves
302
+ legal_moves = list(board.legal_moves)
303
+ if not legal_moves:
304
+ if board.is_check():
305
+ return -self.MATE_SCORE + (self.MAX_PLY - depth), []
306
+ return 0, [] # Stalemate
307
+
308
+ ordered_moves = self.move_orderer.order_moves(
309
+ board, legal_moves, depth, tt_move
310
+ )
311
+
312
+ # Main search loop
313
+ best_score = float('-inf')
314
+ best_pv = []
315
+ node_type = NodeType.UPPER_BOUND
316
+ moves_searched = 0
317
+
318
+ for move in ordered_moves:
319
+ board.push(move)
320
+
321
+ # Late move reductions
322
+ reduction = 0
323
+ if (moves_searched >= self.LMR_MOVE_THRESHOLD and
324
+ depth >= self.LMR_MIN_DEPTH and
325
+ not board.is_check() and
326
+ not board.is_capture(board.peek())):
327
+ reduction = 1
328
+
329
+ # PVS
330
+ if moves_searched == 0:
331
+ score, pv = self._pvs(
332
+ board, depth - 1, -beta, -alpha, True
333
+ )
334
+ score = -score
335
+ else:
336
+ # Reduced search
337
+ score, _ = self._pvs(
338
+ board, depth - 1 - reduction, -alpha - 1, -alpha, True
339
+ )
340
+ score = -score
341
+
342
+ # Re-search if necessary
343
+ if alpha < score < beta:
344
+ score, pv = self._pvs(
345
+ board, depth - 1, -beta, -alpha, True
346
+ )
347
+ score = -score
348
+ else:
349
+ pv = []
350
+
351
+ board.pop()
352
+ moves_searched += 1
353
+
354
+ # Update best
355
+ if score > best_score:
356
+ best_score = score
357
+ best_pv = [move] + pv
358
+
359
+ if score > alpha:
360
+ alpha = score
361
+ node_type = NodeType.EXACT
362
+
363
+ # Update history for good moves
364
+ if not board.is_capture(move):
365
+ self.move_orderer.update_history(move, depth, True)
366
+ self.move_orderer.update_killer_move(move, depth)
367
+
368
+ if score >= beta:
369
+ node_type = NodeType.LOWER_BOUND
370
+ break
371
+
372
+ # Store in TT
373
+ self.tt.store(zobrist_key, depth, best_score, node_type, best_pv[0] if best_pv else None)
374
+
375
+ return best_score, best_pv
376
+
377
+ def _quiescence(
378
+ self,
379
+ board: chess.Board,
380
+ alpha: float,
381
+ beta: float,
382
+ qs_depth: int
383
+ ) -> float:
384
+ """
385
+ Quiescence search to resolve tactical sequences
386
+ Only searches captures and checks
387
+ """
388
+ self.nodes_evaluated += 1
389
+
390
+ # Stand-pat evaluation
391
+ stand_pat = self.evaluator.evaluate_hybrid(board)
392
+ stand_pat = self.endgame_detector.adjust_evaluation(board, stand_pat)
393
+
394
+ if stand_pat >= beta:
395
+ return beta
396
+ if alpha < stand_pat:
397
+ alpha = stand_pat
398
+
399
+ # Depth limit for quiescence
400
+ if qs_depth >= 8:
401
+ return stand_pat
402
+
403
+ # Generate tactical moves
404
+ tactical_moves = [
405
+ move for move in board.legal_moves
406
+ if board.is_capture(move) or board.gives_check(move)
407
+ ]
408
+
409
+ if not tactical_moves:
410
+ return stand_pat
411
+
412
+ # Order tactical moves
413
+ tactical_moves = self.move_orderer.order_moves(
414
+ board, tactical_moves, 0
415
+ )
416
+
417
+ for move in tactical_moves:
418
+ board.push(move)
419
+ score = -self._quiescence(board, -beta, -alpha, qs_depth + 1)
420
+ board.pop()
421
+
422
+ if score >= beta:
423
+ return beta
424
+ if score > alpha:
425
+ alpha = score
426
+
427
+ return alpha
428
+
429
+ def _has_non_pawn_material(self, board: chess.Board) -> bool:
430
+ """Check if side to move has non-pawn material"""
431
+ for piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
432
+ if board.pieces(piece_type, board.turn):
433
+ return True
434
+ return False
435
+
436
+ def _no_legal_moves_result(self) -> Dict:
437
+ """Result when no legal moves"""
438
+ return {
439
+ 'best_move': '0000',
440
+ 'evaluation': 0.0,
441
+ 'depth_searched': 0,
442
+ 'nodes_evaluated': 0,
443
+ 'time_taken': 0
444
+ }
445
+
446
+ def _single_move_result(self, board: chess.Board, move: chess.Move) -> Dict:
447
+ """Result when only one legal move"""
448
+ eval_score = self.evaluator.evaluate_hybrid(board)
449
+
450
+ return {
451
+ 'best_move': move.uci(),
452
+ 'evaluation': round(eval_score / 100.0, 2),
453
+ 'depth_searched': 0,
454
+ 'nodes_evaluated': 1,
455
+ 'time_taken': 0,
456
+ 'pv': [move.uci()]
457
+ }
458
+
459
+ def validate_fen(self, fen: str) -> bool:
460
+ """Validate FEN string"""
461
+ try:
462
+ chess.Board(fen)
463
+ return True
464
+ except:
465
+ return False
466
+
467
+ def get_model_size(self) -> float:
468
+ """Get model size in MB"""
469
+ return self.evaluator.get_model_size_mb()