Rafs-an09002 commited on
Commit
2d2e215
·
verified ·
1 Parent(s): 330cbbe

Create engine/transposition.py

Browse files
Files changed (1) hide show
  1. engine/transposition.py +234 -0
engine/transposition.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Transposition Table for Nexus-Core
3
+ 128MB cache (smaller than Synapse-Base for efficiency)
4
+
5
+ Research: Zobrist (1970) - Hashing for chess positions
6
+ """
7
+
8
+ import chess
9
+ import numpy as np
10
+ from typing import Optional, Dict, Tuple
11
+ from enum import Enum
12
+
13
+
14
+ class NodeType(Enum):
15
+ """Transposition table node types"""
16
+ EXACT = 0
17
+ LOWER_BOUND = 1
18
+ UPPER_BOUND = 2
19
+
20
+
21
+ class TTEntry:
22
+ """Single transposition table entry"""
23
+
24
+ __slots__ = ['zobrist_key', 'depth', 'score', 'node_type', 'best_move', 'age']
25
+
26
+ def __init__(
27
+ self,
28
+ zobrist_key: int,
29
+ depth: int,
30
+ score: float,
31
+ node_type: NodeType,
32
+ best_move: Optional[chess.Move],
33
+ age: int
34
+ ):
35
+ self.zobrist_key = zobrist_key
36
+ self.depth = depth
37
+ self.score = score
38
+ self.node_type = node_type
39
+ self.best_move = best_move
40
+ self.age = age
41
+
42
+
43
+ class TranspositionTable:
44
+ """
45
+ Zobrist-hashed transposition table
46
+ 128MB size for Nexus-Core
47
+ """
48
+
49
+ def __init__(self, size_mb: int = 128):
50
+ """Initialize TT with specified size"""
51
+
52
+ bytes_per_entry = 64
53
+ self.max_entries = (size_mb * 1024 * 1024) // bytes_per_entry
54
+
55
+ self.table: Dict[int, TTEntry] = {}
56
+
57
+ # Statistics
58
+ self.hits = 0
59
+ self.misses = 0
60
+ self.collisions = 0
61
+ self.current_age = 0
62
+
63
+ # Zobrist keys
64
+ self._init_zobrist_keys()
65
+
66
+ def _init_zobrist_keys(self):
67
+ """Initialize Zobrist random numbers"""
68
+ np.random.seed(42)
69
+
70
+ # Piece keys [12 pieces × 64 squares]
71
+ self.zobrist_pieces = np.random.randint(
72
+ 0, 2**63, size=(12, 64), dtype=np.int64
73
+ )
74
+
75
+ # State keys
76
+ self.zobrist_turn = np.random.randint(0, 2**63, dtype=np.int64)
77
+ self.zobrist_castling = np.random.randint(0, 2**63, size=4, dtype=np.int64)
78
+ self.zobrist_ep = np.random.randint(0, 2**63, size=8, dtype=np.int64)
79
+
80
+ def compute_zobrist_key(self, board: chess.Board) -> int:
81
+ """Compute Zobrist hash for position"""
82
+
83
+ key = 0
84
+
85
+ # Pieces
86
+ piece_to_index = {
87
+ (chess.PAWN, chess.WHITE): 0,
88
+ (chess.KNIGHT, chess.WHITE): 1,
89
+ (chess.BISHOP, chess.WHITE): 2,
90
+ (chess.ROOK, chess.WHITE): 3,
91
+ (chess.QUEEN, chess.WHITE): 4,
92
+ (chess.KING, chess.WHITE): 5,
93
+ (chess.PAWN, chess.BLACK): 6,
94
+ (chess.KNIGHT, chess.BLACK): 7,
95
+ (chess.BISHOP, chess.BLACK): 8,
96
+ (chess.ROOK, chess.BLACK): 9,
97
+ (chess.QUEEN, chess.BLACK): 10,
98
+ (chess.KING, chess.BLACK): 11,
99
+ }
100
+
101
+ for square, piece in board.piece_map().items():
102
+ piece_idx = piece_to_index[(piece.piece_type, piece.color)]
103
+ key ^= self.zobrist_pieces[piece_idx, square]
104
+
105
+ # Turn
106
+ if board.turn == chess.BLACK:
107
+ key ^= self.zobrist_turn
108
+
109
+ # Castling
110
+ if board.has_kingside_castling_rights(chess.WHITE):
111
+ key ^= self.zobrist_castling[0]
112
+ if board.has_queenside_castling_rights(chess.WHITE):
113
+ key ^= self.zobrist_castling[1]
114
+ if board.has_kingside_castling_rights(chess.BLACK):
115
+ key ^= self.zobrist_castling[2]
116
+ if board.has_queenside_castling_rights(chess.BLACK):
117
+ key ^= self.zobrist_castling[3]
118
+
119
+ # En passant
120
+ if board.ep_square is not None:
121
+ ep_file = board.ep_square % 8
122
+ key ^= self.zobrist_ep[ep_file]
123
+
124
+ return key
125
+
126
+ def probe(
127
+ self,
128
+ zobrist_key: int,
129
+ depth: int,
130
+ alpha: float,
131
+ beta: float
132
+ ) -> Optional[Tuple[float, Optional[chess.Move]]]:
133
+ """Probe transposition table"""
134
+
135
+ entry = self.table.get(zobrist_key)
136
+
137
+ if entry is None:
138
+ self.misses += 1
139
+ return None
140
+
141
+ # Collision check
142
+ if entry.zobrist_key != zobrist_key:
143
+ self.collisions += 1
144
+ return None
145
+
146
+ # Depth check
147
+ if entry.depth < depth:
148
+ self.misses += 1
149
+ return None
150
+
151
+ self.hits += 1
152
+
153
+ # Check node type
154
+ score = entry.score
155
+
156
+ if entry.node_type == NodeType.EXACT:
157
+ return (score, entry.best_move)
158
+ elif entry.node_type == NodeType.LOWER_BOUND:
159
+ if score >= beta:
160
+ return (score, entry.best_move)
161
+ elif entry.node_type == NodeType.UPPER_BOUND:
162
+ if score <= alpha:
163
+ return (score, entry.best_move)
164
+
165
+ return (None, entry.best_move)
166
+
167
+ def store(
168
+ self,
169
+ zobrist_key: int,
170
+ depth: int,
171
+ score: float,
172
+ node_type: NodeType,
173
+ best_move: Optional[chess.Move]
174
+ ):
175
+ """Store entry in TT"""
176
+
177
+ existing = self.table.get(zobrist_key)
178
+
179
+ # Replacement strategy: always replace if deeper or newer
180
+ if existing is not None:
181
+ if depth < existing.depth and existing.age == self.current_age:
182
+ return
183
+
184
+ # Store
185
+ self.table[zobrist_key] = TTEntry(
186
+ zobrist_key=zobrist_key,
187
+ depth=depth,
188
+ score=score,
189
+ node_type=node_type,
190
+ best_move=best_move,
191
+ age=self.current_age
192
+ )
193
+
194
+ # Cleanup if too large
195
+ if len(self.table) > self.max_entries:
196
+ self._cleanup_old_entries()
197
+
198
+ def _cleanup_old_entries(self):
199
+ """Remove oldest 10% of entries"""
200
+ entries_to_remove = self.max_entries // 10
201
+
202
+ old_keys = sorted(
203
+ self.table.keys(),
204
+ key=lambda k: self.table[k].age
205
+ )[:entries_to_remove]
206
+
207
+ for key in old_keys:
208
+ del self.table[key]
209
+
210
+ def increment_age(self):
211
+ """Increment generation counter"""
212
+ self.current_age += 1
213
+
214
+ def clear(self):
215
+ """Clear all entries"""
216
+ self.table.clear()
217
+ self.hits = 0
218
+ self.misses = 0
219
+ self.collisions = 0
220
+
221
+ def get_stats(self) -> Dict:
222
+ """Get statistics"""
223
+ total_probes = self.hits + self.misses
224
+ hit_rate = (self.hits / total_probes * 100) if total_probes > 0 else 0
225
+
226
+ return {
227
+ 'entries': len(self.table),
228
+ 'max_entries': self.max_entries,
229
+ 'usage_percent': len(self.table) / self.max_entries * 100,
230
+ 'hits': self.hits,
231
+ 'misses': self.misses,
232
+ 'hit_rate': hit_rate,
233
+ 'collisions': self.collisions
234
+ }