| """ |
| Transposition Table for Nexus-Nano |
| 64MB compact cache for speed |
| """ |
|
|
| import chess |
| import numpy as np |
| from typing import Optional, Dict, Tuple |
| from enum import Enum |
|
|
|
|
| class NodeType(Enum): |
| EXACT = 0 |
| LOWER_BOUND = 1 |
| UPPER_BOUND = 2 |
|
|
|
|
| class TTEntry: |
| __slots__ = ['zobrist_key', 'depth', 'score', 'node_type', 'best_move', 'age'] |
| |
| def __init__(self, zobrist_key, depth, score, node_type, best_move, age): |
| self.zobrist_key = zobrist_key |
| self.depth = depth |
| self.score = score |
| self.node_type = node_type |
| self.best_move = best_move |
| self.age = age |
|
|
|
|
| class TranspositionTable: |
| """Compact 64MB transposition table""" |
| |
| def __init__(self, size_mb: int = 64): |
| """Initialize with 64MB size""" |
| |
| bytes_per_entry = 64 |
| self.max_entries = (size_mb * 1024 * 1024) // bytes_per_entry |
| |
| self.table: Dict[int, TTEntry] = {} |
| |
| self.hits = 0 |
| self.misses = 0 |
| self.collisions = 0 |
| self.current_age = 0 |
| |
| self._init_zobrist_keys() |
| |
| def _init_zobrist_keys(self): |
| """Initialize Zobrist keys""" |
| np.random.seed(42) |
| |
| self.zobrist_pieces = np.random.randint( |
| 0, 2**63, size=(12, 64), dtype=np.int64 |
| ) |
| self.zobrist_turn = np.random.randint(0, 2**63, dtype=np.int64) |
| self.zobrist_castling = np.random.randint(0, 2**63, size=4, dtype=np.int64) |
| self.zobrist_ep = np.random.randint(0, 2**63, size=8, dtype=np.int64) |
| |
| def compute_zobrist_key(self, board: chess.Board) -> int: |
| """Fast Zobrist hash computation""" |
| |
| key = 0 |
| |
| piece_to_idx = { |
| (chess.PAWN, chess.WHITE): 0, (chess.KNIGHT, chess.WHITE): 1, |
| (chess.BISHOP, chess.WHITE): 2, (chess.ROOK, chess.WHITE): 3, |
| (chess.QUEEN, chess.WHITE): 4, (chess.KING, chess.WHITE): 5, |
| (chess.PAWN, chess.BLACK): 6, (chess.KNIGHT, chess.BLACK): 7, |
| (chess.BISHOP, chess.BLACK): 8, (chess.ROOK, chess.BLACK): 9, |
| (chess.QUEEN, chess.BLACK): 10, (chess.KING, chess.BLACK): 11, |
| } |
| |
| for square, piece in board.piece_map().items(): |
| idx = piece_to_idx[(piece.piece_type, piece.color)] |
| key ^= self.zobrist_pieces[idx, square] |
| |
| if board.turn == chess.BLACK: |
| key ^= self.zobrist_turn |
| |
| if board.has_kingside_castling_rights(chess.WHITE): |
| key ^= self.zobrist_castling[0] |
| if board.has_queenside_castling_rights(chess.WHITE): |
| key ^= self.zobrist_castling[1] |
| if board.has_kingside_castling_rights(chess.BLACK): |
| key ^= self.zobrist_castling[2] |
| if board.has_queenside_castling_rights(chess.BLACK): |
| key ^= self.zobrist_castling[3] |
| |
| if board.ep_square is not None: |
| key ^= self.zobrist_ep[board.ep_square % 8] |
| |
| return key |
| |
| def probe(self, zobrist_key, depth, alpha, beta) -> Optional[Tuple]: |
| """Fast TT probe""" |
| |
| entry = self.table.get(zobrist_key) |
| |
| if entry is None: |
| self.misses += 1 |
| return None |
| |
| if entry.zobrist_key != zobrist_key: |
| self.collisions += 1 |
| return None |
| |
| if entry.depth < depth: |
| self.misses += 1 |
| return None |
| |
| self.hits += 1 |
| |
| score = entry.score |
| |
| if entry.node_type == NodeType.EXACT: |
| return (score, entry.best_move) |
| elif entry.node_type == NodeType.LOWER_BOUND and score >= beta: |
| return (score, entry.best_move) |
| elif entry.node_type == NodeType.UPPER_BOUND and score <= alpha: |
| return (score, entry.best_move) |
| |
| return (None, entry.best_move) |
| |
| def store(self, zobrist_key, depth, score, node_type, best_move): |
| """Store entry""" |
| |
| existing = self.table.get(zobrist_key) |
| |
| if existing and depth < existing.depth and existing.age == self.current_age: |
| return |
| |
| self.table[zobrist_key] = TTEntry( |
| zobrist_key, depth, score, node_type, best_move, self.current_age |
| ) |
| |
| if len(self.table) > self.max_entries: |
| self._cleanup() |
| |
| def _cleanup(self): |
| """Quick cleanup (10%)""" |
| remove_count = self.max_entries // 10 |
| old_keys = sorted(self.table.keys(), key=lambda k: self.table[k].age)[:remove_count] |
| for key in old_keys: |
| del self.table[key] |
| |
| def increment_age(self): |
| self.current_age += 1 |
| |
| def clear(self): |
| self.table.clear() |
| self.hits = self.misses = self.collisions = 0 |
| |
| def get_stats(self) -> Dict: |
| total = self.hits + self.misses |
| hit_rate = (self.hits / total * 100) if total > 0 else 0 |
| |
| return { |
| 'entries': len(self.table), |
| 'max_entries': self.max_entries, |
| 'usage_percent': len(self.table) / self.max_entries * 100, |
| 'hits': self.hits, |
| 'misses': self.misses, |
| 'hit_rate': hit_rate, |
| 'collisions': self.collisions |
| } |