File size: 6,186 Bytes
4b6bfbd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
from typing import List, Dict, Tuple, Set
# --- Constants --- #
MAX_HALFMOVES = 128 # cap for embedding table size
MAX_FULLMOVES = 256 # cap for embedding table size
# --- Helper Mappings --- #
PIECE_TO_IDX: Dict[str, int] = {
'P': 0, 'N': 1, 'B': 2, 'R': 3, 'Q': 4, 'K': 5,
'p': 6, 'n': 7, 'b': 8, 'r': 9, 'q': 10, 'k': 11,
'.': 12
}
IDX_TO_PIECE: Dict[int, str] = {v: k for k, v in PIECE_TO_IDX.items()}
EMPTY_SQ_IDX = PIECE_TO_IDX['.']
# Map algebraic square notation (e.g., 'a1', 'h8') to 0-63 index
# a1=0, b1=1, ..., h1=7, a2=8, ..., h8=63
SQUARE_TO_IDX: Dict[str, int] = {
f"{file}{rank}": (rank - 1) * 8 + (ord(file) - ord('a'))
for rank in range(1, 9)
for file in 'abcdefgh'
}
IDX_TO_SQUARE: Dict[int, str] = {v: k for k, v in SQUARE_TO_IDX.items()}
# --- Coordinate and Notation Helpers ---
# Precompute maps for efficiency
_IDX_TO_COORDS: Dict[int, Tuple[int, int]] = {i: (i // 8, i % 8) for i in range(64)} # (rank, file) 0-7
_COORDS_TO_IDX: Dict[Tuple[int, int], int] = {v: k for k, v in _IDX_TO_COORDS.items()}
_IDX_TO_ALG: Dict[int, str] = {
i: f"{chr(ord('a') + file)}{rank + 1}"
for i, (rank, file) in _IDX_TO_COORDS.items()
}
_ALG_TO_IDX: Dict[str, int] = {v: k for k, v in _IDX_TO_ALG.items()}
def _coords_to_alg(r: int, f: int) -> str:
"""Converts 0-indexed (rank, file) to algebraic notation."""
if 0 <= r < 8 and 0 <= f < 8:
return f"{chr(ord('a') + f)}{r + 1}"
# This should not happen with valid indices, but good for safety
raise ValueError(f"Invalid coordinates: ({r}, {f})")
def generate_structurally_valid_move_map() -> Dict[str, int]:
"""
Generates a dictionary mapping chess moves that are geometrically possible
by *some* standard piece (K, Q, R, B, N, or P) to unique integer indices.
It excludes moves that are structurally impossible for any piece to make
in one turn (e.g., a1->h5 for non-knight).
Includes standard UCI promotions (e.g., "e7e8q"), replacing the
corresponding simple pawn move to the final rank (e.g., "e7e8").
This is based purely on piece movement geometry, not the current board state.
Returns:
Dict[str, int]: A map from the valid UCI move string to a unique
integer index (0 to N-1). The size N is expected
to be around 1800-1900.
"""
valid_moves: Set[str] = set()
# Keep track of base moves (like 'e7e8') that are replaced by promotions
# according to UCI standard.
promo_base_moves_to_exclude: Set[str] = set()
# 1. Generate all geometrically possible non-promotion moves
for from_idx in range(64):
from_r, from_f = _IDX_TO_COORDS[from_idx]
from_alg = _IDX_TO_ALG[from_idx]
for to_idx in range(64):
if from_idx == to_idx:
continue
to_r, to_f = _IDX_TO_COORDS[to_idx]
to_alg = _IDX_TO_ALG[to_idx]
dr, df = to_r - from_r, to_f - from_f
abs_dr, abs_df = abs(dr), abs(df)
# Check if the geometry matches any standard piece movement
# Note: Queen moves are covered by Rook + Bishop checks.
# Note: Pawn single pushes/captures are covered by King/Rook/Bishop geometry.
# Note: Pawn double pushes are covered by Rook geometry.
is_king_move = max(abs_dr, abs_df) == 1
is_knight_move = (abs_dr == 2 and abs_df == 1) or (abs_dr == 1 and abs_df == 2)
is_rook_move = dr == 0 or df == 0 # Includes King horiz/vert & pawn double push
is_bishop_move = abs_dr == abs_df # Includes King diagonal & pawn capture/push
if is_king_move or is_knight_move or is_rook_move or is_bishop_move:
uci_move = f"{from_alg}{to_alg}"
valid_moves.add(uci_move)
# 2. Generate promotion moves explicitly and mark base moves for exclusion
promo_pieces = ['q', 'r', 'b', 'n']
for from_f in range(8):
# White promotions (from rank 7 (idx 6) to rank 8 (idx 7))
from_r_w, to_r_w = 6, 7
if from_r_w != 7: # Ensure we are on the correct rank before promotion
from_alg_w = _coords_to_alg(from_r_w, from_f)
# Possible destinations: push (df=0), capture left (df=-1), capture right (df=1)
for df in [-1, 0, 1]:
to_f_w = from_f + df
if 0 <= to_f_w < 8:
to_alg_w = _coords_to_alg(to_r_w, to_f_w)
base_move = f"{from_alg_w}{to_alg_w}"
#promo_base_moves_to_exclude.add(base_move) # Mark e.g. "e7e8" for exclusion
for p in promo_pieces:
valid_moves.add(f"{base_move}{p}") # Add e.g. "e7e8q"
# Black promotions (from rank 2 (idx 1) to rank 1 (idx 0))
from_r_b, to_r_b = 1, 0
if from_r_b != 0: # Ensure we are on the correct rank before promotion
from_alg_b = _coords_to_alg(from_r_b, from_f)
# Possible destinations: push (df=0), capture left (df=-1), capture right (df=1)
for df in [-1, 0, 1]:
to_f_b = from_f + df
if 0 <= to_f_b < 8:
to_alg_b = _coords_to_alg(to_r_b, to_f_b)
base_move = f"{from_alg_b}{to_alg_b}"
#promo_base_moves_to_exclude.add(base_move) # Mark e.g. "e2e1" for exclusion
for p in promo_pieces:
valid_moves.add(f"{base_move}{p}") # Add e.g. "e2e1q"
# 3. Remove the base moves that were replaced by promotions
final_valid_moves = valid_moves - promo_base_moves_to_exclude
# 4. Add draw claim
final_valid_moves.add("<claim_draw>")
# 5. Create the final map with sorted keys for deterministic indices
sorted_moves = sorted(list(final_valid_moves))
move_map = {move: i for i, move in enumerate(sorted_moves)}
# Optional: Print the number of moves found for verification
# print(f"Generated {len(move_map)} structurally valid unique UCI moves.")
return move_map
UCI_MOVE_TO_IDX = generate_structurally_valid_move_map()
IDX_TO_UCI_MOVE = {v:k for k,v in UCI_MOVE_TO_IDX.items()} |