Rafs-an09002 commited on
Commit
e3e5e3d
·
verified ·
1 Parent(s): 94d271b

Create engine/move_ordering.py

Browse files
Files changed (1) hide show
  1. engine/move_ordering.py +284 -0
engine/move_ordering.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Advanced Move Ordering Techniques
3
+ Critical for alpha-beta pruning efficiency
4
+
5
+ Research References:
6
+ - Stockfish move ordering (2023) - History heuristic + killer moves
7
+ - Fruit 2.1 - MVV-LVA and late move reductions
8
+ - Crafty - History tables and counter moves
9
+
10
+ Good move ordering can reduce search tree by 10-100x!
11
+ """
12
+
13
+ import chess
14
+ from typing import List, Optional, Dict, Tuple
15
+ import numpy as np
16
+
17
+
18
+ class MoveOrderer:
19
+ """
20
+ Advanced move ordering system
21
+ Combines multiple heuristics for optimal move ordering
22
+ """
23
+
24
+ # Piece values for MVV-LVA (victim values)
25
+ PIECE_VALUES = {
26
+ chess.PAWN: 100,
27
+ chess.KNIGHT: 320,
28
+ chess.BISHOP: 330,
29
+ chess.ROOK: 500,
30
+ chess.QUEEN: 900,
31
+ chess.KING: 20000
32
+ }
33
+
34
+ def __init__(self):
35
+ """Initialize move ordering data structures"""
36
+
37
+ # Killer moves (quiet moves that caused beta cutoff)
38
+ # killer_moves[depth] = [move1, move2]
39
+ self.killer_moves: Dict[int, List[Optional[chess.Move]]] = {}
40
+ self.max_killers = 2 # Store top 2 killer moves per depth
41
+
42
+ # History heuristic (move success rates)
43
+ # history[from_square][to_square] = score
44
+ self.history = np.zeros((64, 64), dtype=np.int32)
45
+
46
+ # Counter moves (refutation table)
47
+ # counter_moves[prev_move] = best_response
48
+ self.counter_moves: Dict[chess.Move, Optional[chess.Move]] = {}
49
+
50
+ # Statistics
51
+ self.history_hits = 0
52
+ self.killer_hits = 0
53
+
54
+ def order_moves(
55
+ self,
56
+ board: chess.Board,
57
+ moves: List[chess.Move],
58
+ depth: int,
59
+ tt_move: Optional[chess.Move] = None,
60
+ previous_move: Optional[chess.Move] = None
61
+ ) -> List[chess.Move]:
62
+ """
63
+ Order moves for optimal alpha-beta pruning
64
+
65
+ Priority order (research-based):
66
+ 1. TT move (from transposition table)
67
+ 2. Winning captures (MVV-LVA)
68
+ 3. Killer moves
69
+ 4. Counter moves
70
+ 5. History heuristic
71
+ 6. Losing captures
72
+ 7. Quiet moves
73
+
74
+ Args:
75
+ board: Current position
76
+ moves: Legal moves to order
77
+ depth: Current search depth
78
+ tt_move: Best move from transposition table
79
+ previous_move: Opponent's last move
80
+
81
+ Returns:
82
+ Ordered list of moves
83
+ """
84
+ scored_moves = []
85
+
86
+ for move in moves:
87
+ score = 0
88
+
89
+ # ========== PRIORITY 1: TT Move ==========
90
+ if tt_move and move == tt_move:
91
+ score += 1000000
92
+
93
+ # ========== PRIORITY 2: Captures (MVV-LVA) ==========
94
+ elif board.is_capture(move):
95
+ score += self._score_capture(board, move)
96
+
97
+ # ========== PRIORITY 3-7: Quiet Moves ==========
98
+ else:
99
+ # Check if it's a killer move
100
+ if self._is_killer_move(move, depth):
101
+ score += 9000
102
+ self.killer_hits += 1
103
+
104
+ # Counter move bonus
105
+ if previous_move and move == self.counter_moves.get(previous_move):
106
+ score += 8000
107
+
108
+ # History heuristic
109
+ history_score = self.history[move.from_square, move.to_square]
110
+ score += min(history_score, 7000)
111
+
112
+ # Promotions
113
+ if move.promotion == chess.QUEEN:
114
+ score += 10000
115
+ elif move.promotion in [chess.KNIGHT, chess.ROOK, chess.BISHOP]:
116
+ score += 5000
117
+
118
+ # Checks
119
+ board.push(move)
120
+ if board.is_check():
121
+ score += 6000
122
+ board.pop()
123
+
124
+ # Castling
125
+ if board.is_castling(move):
126
+ score += 3000
127
+
128
+ # Positional bonuses
129
+ score += self._score_positional(board, move)
130
+
131
+ scored_moves.append((score, move))
132
+
133
+ # Sort by score (descending)
134
+ scored_moves.sort(key=lambda x: x[0], reverse=True)
135
+
136
+ return [move for _, move in scored_moves]
137
+
138
+ def _score_capture(self, board: chess.Board, move: chess.Move) -> int:
139
+ """
140
+ Score capture using MVV-LVA (Most Valuable Victim - Least Valuable Attacker)
141
+ Enhanced with SEE (Static Exchange Evaluation)
142
+ """
143
+ captured_piece = board.piece_at(move.to_square)
144
+ moving_piece = board.piece_at(move.from_square)
145
+
146
+ if not captured_piece or not moving_piece:
147
+ return 0
148
+
149
+ victim_value = self.PIECE_VALUES.get(captured_piece.piece_type, 0)
150
+ attacker_value = self.PIECE_VALUES.get(moving_piece.piece_type, 1)
151
+
152
+ # MVV-LVA base score
153
+ mvv_lva_score = (victim_value * 10 - attacker_value) * 100
154
+
155
+ # Bonus for en passant
156
+ if board.is_en_passant(move):
157
+ mvv_lva_score += 10500
158
+
159
+ # Penalty for hanging pieces (simplified SEE)
160
+ # Check if capture square is defended
161
+ if board.is_attacked_by(not board.turn, move.to_square):
162
+ # Losing capture if victim < attacker
163
+ if victim_value < attacker_value:
164
+ mvv_lva_score -= 5000 # Deprioritize losing captures
165
+
166
+ return mvv_lva_score
167
+
168
+ def _score_positional(self, board: chess.Board, move: chess.Move) -> int:
169
+ """
170
+ Positional scoring for quiet moves
171
+ Based on Stockfish evaluation terms
172
+ """
173
+ score = 0
174
+
175
+ piece = board.piece_at(move.from_square)
176
+ if not piece:
177
+ return 0
178
+
179
+ # Center control (e4, d4, e5, d5)
180
+ center_squares = [chess.E4, chess.D4, chess.E5, chess.D5]
181
+ if move.to_square in center_squares:
182
+ score += 50
183
+
184
+ # Extended center
185
+ extended_center = [
186
+ chess.C3, chess.D3, chess.E3, chess.F3,
187
+ chess.C4, chess.F4,
188
+ chess.C5, chess.F5,
189
+ chess.C6, chess.D6, chess.E6, chess.F6
190
+ ]
191
+ if move.to_square in extended_center:
192
+ score += 20
193
+
194
+ # Piece development (from back rank)
195
+ if piece.piece_type in [chess.KNIGHT, chess.BISHOP]:
196
+ from_rank = move.from_square // 8
197
+ if from_rank in [0, 7]: # Back rank
198
+ score += 30
199
+
200
+ # Knight outposts (protected squares in enemy territory)
201
+ if piece.piece_type == chess.KNIGHT:
202
+ to_rank = move.to_square // 8
203
+ if (board.turn == chess.WHITE and to_rank >= 4) or \
204
+ (board.turn == chess.BLACK and to_rank <= 3):
205
+ # Check if protected by pawn
206
+ board.push(move)
207
+ if board.is_attacked_by(board.turn, move.to_square):
208
+ score += 40
209
+ board.pop()
210
+
211
+ return score
212
+
213
+ def _is_killer_move(self, move: chess.Move, depth: int) -> bool:
214
+ """Check if move is a killer move at this depth"""
215
+ killers = self.killer_moves.get(depth, [])
216
+ return move in killers
217
+
218
+ def update_killer_move(self, move: chess.Move, depth: int):
219
+ """
220
+ Update killer moves for this depth
221
+ Killer moves are quiet moves that caused beta cutoff
222
+ """
223
+ if depth not in self.killer_moves:
224
+ self.killer_moves[depth] = []
225
+
226
+ killers = self.killer_moves[depth]
227
+
228
+ # Add move if not already in list
229
+ if move not in killers:
230
+ killers.insert(0, move)
231
+ # Keep only top N killers
232
+ self.killer_moves[depth] = killers[:self.max_killers]
233
+
234
+ def update_history(self, move: chess.Move, depth: int, success: bool):
235
+ """
236
+ Update history heuristic
237
+
238
+ Args:
239
+ move: Move that was tried
240
+ depth: Search depth
241
+ success: True if move caused beta cutoff
242
+ """
243
+ if success:
244
+ # Increase score (depth squared bonus for deeper searches)
245
+ bonus = depth * depth
246
+ self.history[move.from_square, move.to_square] += bonus
247
+ self.history_hits += 1
248
+ else:
249
+ # Slight penalty for moves that failed low
250
+ self.history[move.from_square, move.to_square] -= 1
251
+
252
+ # Cap values to prevent overflow
253
+ self.history = np.clip(self.history, -10000, 10000)
254
+
255
+ def update_counter_move(self, previous_move: chess.Move, refutation: chess.Move):
256
+ """
257
+ Update counter move table
258
+ Counter move = best response to opponent's last move
259
+ """
260
+ self.counter_moves[previous_move] = refutation
261
+
262
+ def clear_history(self):
263
+ """Clear history table (called at new search)"""
264
+ self.history.fill(0)
265
+ self.killer_moves.clear()
266
+ self.history_hits = 0
267
+ self.killer_hits = 0
268
+
269
+ def age_history(self, factor: float = 0.9):
270
+ """
271
+ Age history table (reduce all values)
272
+ Prevents old data from dominating
273
+ """
274
+ self.history = (self.history * factor).astype(np.int32)
275
+
276
+ def get_stats(self) -> Dict:
277
+ """Get move ordering statistics"""
278
+ return {
279
+ 'killer_hits': self.killer_hits,
280
+ 'history_hits': self.history_hits,
281
+ 'history_max': int(np.max(self.history)),
282
+ 'killer_depths': len(self.killer_moves),
283
+ 'counter_moves': len(self.counter_moves)
284
+ }