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

Create engine/transposition.py

Browse files
Files changed (1) hide show
  1. engine/transposition.py +278 -0
engine/transposition.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Transposition Table with Zobrist Hashing
3
+ Research: Stockfish uses 2GB TT, we use 400MB for Colab constraints
4
+
5
+ References:
6
+ - Zobrist (1970) - Hash functions for chess positions
7
+ - Stockfish TT - Replacement strategies
8
+ - AlphaBeta enhancements - Exact/Lower/Upper bounds
9
+ """
10
+
11
+ import chess
12
+ import numpy as np
13
+ from typing import Optional, Dict, Tuple
14
+ from enum import Enum
15
+
16
+
17
+ class NodeType(Enum):
18
+ """Type of transposition table entry"""
19
+ EXACT = 0 # PV-node (exact score)
20
+ LOWER_BOUND = 1 # Cut-node (beta cutoff)
21
+ UPPER_BOUND = 2 # All-node (failed low)
22
+
23
+
24
+ class TTEntry:
25
+ """Single transposition table entry"""
26
+
27
+ __slots__ = ['zobrist_key', 'depth', 'score', 'node_type', 'best_move', 'age']
28
+
29
+ def __init__(
30
+ self,
31
+ zobrist_key: int,
32
+ depth: int,
33
+ score: float,
34
+ node_type: NodeType,
35
+ best_move: Optional[chess.Move],
36
+ age: int
37
+ ):
38
+ self.zobrist_key = zobrist_key
39
+ self.depth = depth
40
+ self.score = score
41
+ self.node_type = node_type
42
+ self.best_move = best_move
43
+ self.age = age
44
+
45
+
46
+ class TranspositionTable:
47
+ """
48
+ Zobrist-hashed transposition table
49
+ Replacement strategy: Always replace if deeper or newer
50
+ """
51
+
52
+ def __init__(self, size_mb: int = 256):
53
+ """
54
+ Initialize transposition table
55
+
56
+ Args:
57
+ size_mb: Table size in megabytes (default 256MB)
58
+ """
59
+ # Calculate number of entries (each entry ~64 bytes)
60
+ bytes_per_entry = 64
61
+ self.max_entries = (size_mb * 1024 * 1024) // bytes_per_entry
62
+
63
+ # Hash table (dict for simplicity, could use array for speed)
64
+ self.table: Dict[int, TTEntry] = {}
65
+
66
+ # Statistics
67
+ self.hits = 0
68
+ self.misses = 0
69
+ self.collisions = 0
70
+ self.current_age = 0
71
+
72
+ # Zobrist keys for hashing (initialized once)
73
+ self._init_zobrist_keys()
74
+
75
+ def _init_zobrist_keys(self):
76
+ """
77
+ Initialize Zobrist random keys
78
+ One key per (piece_type, color, square) combination
79
+ """
80
+ np.random.seed(42) # Reproducible keys
81
+
82
+ self.zobrist_pieces = np.random.randint(
83
+ 0, 2**63, size=(12, 64), dtype=np.int64
84
+ )
85
+
86
+ # Additional keys for game state
87
+ self.zobrist_turn = np.random.randint(0, 2**63, dtype=np.int64)
88
+ self.zobrist_castling = np.random.randint(0, 2**63, size=4, dtype=np.int64)
89
+ self.zobrist_ep = np.random.randint(0, 2**63, size=8, dtype=np.int64)
90
+
91
+ def compute_zobrist_key(self, board: chess.Board) -> int:
92
+ """
93
+ Compute Zobrist hash for position
94
+
95
+ Args:
96
+ board: chess.Board
97
+
98
+ Returns:
99
+ 64-bit Zobrist key
100
+ """
101
+ key = 0
102
+
103
+ # Piece positions
104
+ piece_to_index = {
105
+ (chess.PAWN, chess.WHITE): 0,
106
+ (chess.KNIGHT, chess.WHITE): 1,
107
+ (chess.BISHOP, chess.WHITE): 2,
108
+ (chess.ROOK, chess.WHITE): 3,
109
+ (chess.QUEEN, chess.WHITE): 4,
110
+ (chess.KING, chess.WHITE): 5,
111
+ (chess.PAWN, chess.BLACK): 6,
112
+ (chess.KNIGHT, chess.BLACK): 7,
113
+ (chess.BISHOP, chess.BLACK): 8,
114
+ (chess.ROOK, chess.BLACK): 9,
115
+ (chess.QUEEN, chess.BLACK): 10,
116
+ (chess.KING, chess.BLACK): 11,
117
+ }
118
+
119
+ for square, piece in board.piece_map().items():
120
+ piece_idx = piece_to_index[(piece.piece_type, piece.color)]
121
+ key ^= self.zobrist_pieces[piece_idx, square]
122
+
123
+ # Turn
124
+ if board.turn == chess.BLACK:
125
+ key ^= self.zobrist_turn
126
+
127
+ # Castling rights
128
+ if board.has_kingside_castling_rights(chess.WHITE):
129
+ key ^= self.zobrist_castling[0]
130
+ if board.has_queenside_castling_rights(chess.WHITE):
131
+ key ^= self.zobrist_castling[1]
132
+ if board.has_kingside_castling_rights(chess.BLACK):
133
+ key ^= self.zobrist_castling[2]
134
+ if board.has_queenside_castling_rights(chess.BLACK):
135
+ key ^= self.zobrist_castling[3]
136
+
137
+ # En passant
138
+ if board.ep_square is not None:
139
+ ep_file = board.ep_square % 8
140
+ key ^= self.zobrist_ep[ep_file]
141
+
142
+ return key
143
+
144
+ def probe(
145
+ self,
146
+ zobrist_key: int,
147
+ depth: int,
148
+ alpha: float,
149
+ beta: float
150
+ ) -> Optional[Tuple[float, Optional[chess.Move]]]:
151
+ """
152
+ Probe transposition table
153
+
154
+ Args:
155
+ zobrist_key: Zobrist hash of position
156
+ depth: Current search depth
157
+ alpha: Alpha value
158
+ beta: Beta value
159
+
160
+ Returns:
161
+ (score, best_move) if usable entry found, else None
162
+ """
163
+ entry = self.table.get(zobrist_key)
164
+
165
+ if entry is None:
166
+ self.misses += 1
167
+ return None
168
+
169
+ # Zobrist collision check
170
+ if entry.zobrist_key != zobrist_key:
171
+ self.collisions += 1
172
+ return None
173
+
174
+ # Depth check: only use if searched deeper
175
+ if entry.depth < depth:
176
+ self.misses += 1
177
+ return None
178
+
179
+ self.hits += 1
180
+
181
+ # Check if score is usable based on node type
182
+ score = entry.score
183
+
184
+ if entry.node_type == NodeType.EXACT:
185
+ return (score, entry.best_move)
186
+
187
+ elif entry.node_type == NodeType.LOWER_BOUND:
188
+ if score >= beta:
189
+ return (score, entry.best_move)
190
+
191
+ elif entry.node_type == NodeType.UPPER_BOUND:
192
+ if score <= alpha:
193
+ return (score, entry.best_move)
194
+
195
+ # Entry exists but not usable for cutoff
196
+ # Still return best_move for move ordering
197
+ return (None, entry.best_move)
198
+
199
+ def store(
200
+ self,
201
+ zobrist_key: int,
202
+ depth: int,
203
+ score: float,
204
+ node_type: NodeType,
205
+ best_move: Optional[chess.Move]
206
+ ):
207
+ """
208
+ Store entry in transposition table
209
+
210
+ Args:
211
+ zobrist_key: Zobrist hash
212
+ depth: Search depth
213
+ score: Position score
214
+ node_type: Type of node (exact/lower/upper)
215
+ best_move: Best move found
216
+ """
217
+ # Check if we should replace existing entry
218
+ existing = self.table.get(zobrist_key)
219
+
220
+ if existing is not None:
221
+ # Always replace if:
222
+ # 1. New search is deeper
223
+ # 2. Same depth but newer (generational replacement)
224
+ if depth < existing.depth and existing.age == self.current_age:
225
+ return # Keep existing deeper entry
226
+
227
+ # Store new entry
228
+ self.table[zobrist_key] = TTEntry(
229
+ zobrist_key=zobrist_key,
230
+ depth=depth,
231
+ score=score,
232
+ node_type=node_type,
233
+ best_move=best_move,
234
+ age=self.current_age
235
+ )
236
+
237
+ # Cleanup if table too large (simple strategy)
238
+ if len(self.table) > self.max_entries:
239
+ self._cleanup_old_entries()
240
+
241
+ def _cleanup_old_entries(self):
242
+ """Remove oldest 10% of entries"""
243
+ entries_to_remove = self.max_entries // 10
244
+
245
+ # Remove oldest entries (by age)
246
+ old_keys = sorted(
247
+ self.table.keys(),
248
+ key=lambda k: self.table[k].age
249
+ )[:entries_to_remove]
250
+
251
+ for key in old_keys:
252
+ del self.table[key]
253
+
254
+ def increment_age(self):
255
+ """Increment generation counter (call at search start)"""
256
+ self.current_age += 1
257
+
258
+ def clear(self):
259
+ """Clear all entries"""
260
+ self.table.clear()
261
+ self.hits = 0
262
+ self.misses = 0
263
+ self.collisions = 0
264
+
265
+ def get_stats(self) -> Dict:
266
+ """Get table statistics"""
267
+ total_probes = self.hits + self.misses
268
+ hit_rate = (self.hits / total_probes * 100) if total_probes > 0 else 0
269
+
270
+ return {
271
+ 'entries': len(self.table),
272
+ 'max_entries': self.max_entries,
273
+ 'usage_percent': len(self.table) / self.max_entries * 100,
274
+ 'hits': self.hits,
275
+ 'misses': self.misses,
276
+ 'hit_rate': hit_rate,
277
+ 'collisions': self.collisions
278
+ }