Spaces:
Sleeping
Sleeping
| """AlphaZero-style action encoding for chess moves. | |
| Encodes moves as (from_square, action_plane) pairs using the 8×8×73 | |
| representation from the AlphaZero paper (Silver et al., 2018). | |
| The 73 action planes decompose as: | |
| 0–55: Queen-type moves — 8 directions × 7 distances | |
| 56–63: Knight moves — 8 L-shaped jumps | |
| 64–72: Underpromotions — 3 directions × 3 piece types (knight/bishop/rook) | |
| Queen promotions are encoded as regular queen-type moves (distance-1 to the | |
| last rank). Underpromotions get their own dedicated planes. | |
| """ | |
| import chess | |
| import torch | |
| NUM_ACTION_PLANES = 73 | |
| # Direction vectors (file_delta, rank_delta) for queen-type moves | |
| QUEEN_DIRECTIONS = [ | |
| (0, 1), # N | |
| (1, 1), # NE | |
| (1, 0), # E | |
| (1, -1), # SE | |
| (0, -1), # S | |
| (-1, -1), # SW | |
| (-1, 0), # W | |
| (-1, 1), # NW | |
| ] | |
| # Knight jump offsets (file_delta, rank_delta) | |
| KNIGHT_JUMPS = [ | |
| (1, 2), (2, 1), (2, -1), (1, -2), | |
| (-1, -2), (-2, -1), (-2, 1), (-1, 2), | |
| ] | |
| # Underpromotion piece types (queen promotion is a regular queen-type move) | |
| UNDERPROMO_PIECES = [chess.KNIGHT, chess.BISHOP, chess.ROOK] | |
| # Pre-compute knight jump lookup for O(1) detection | |
| _KNIGHT_JUMP_INDEX = {(df, dr): i for i, (df, dr) in enumerate(KNIGHT_JUMPS)} | |
| def move_to_action_plane(from_sq: int, to_sq: int, promotion: int | None = None) -> int: | |
| """Convert a chess move to its action plane index (0–72). | |
| Args: | |
| from_sq: Source square (0–63, python-chess ordering). | |
| to_sq: Destination square (0–63). | |
| promotion: Promotion piece type (chess.QUEEN, chess.KNIGHT, etc.) or None. | |
| Returns: | |
| Action plane index in [0, 72]. | |
| """ | |
| df = (to_sq % 8) - (from_sq % 8) | |
| dr = (to_sq // 8) - (from_sq // 8) | |
| # ── Underpromotion ─────────────────────────────────────────────────── | |
| if promotion is not None and promotion != chess.QUEEN: | |
| # Direction: file delta ∈ {-1, 0, +1} → index 0, 1, 2 | |
| dir_idx = df + 1 | |
| piece_idx = UNDERPROMO_PIECES.index(promotion) | |
| return 64 + dir_idx * 3 + piece_idx | |
| # ── Knight move ────────────────────────────────────────────────────── | |
| knight_idx = _KNIGHT_JUMP_INDEX.get((df, dr)) | |
| if knight_idx is not None: | |
| return 56 + knight_idx | |
| # ── Queen-type move (includes queen promotions) ────────────────────── | |
| distance = max(abs(df), abs(dr)) | |
| dir_f = (df // distance) if df != 0 else 0 | |
| dir_r = (dr // distance) if dr != 0 else 0 | |
| dir_idx = QUEEN_DIRECTIONS.index((dir_f, dir_r)) | |
| return dir_idx * 7 + (distance - 1) | |
| def action_to_move(from_sq: int, plane: int) -> tuple[int, int | None]: | |
| """Decode an action plane back to (to_square, promotion_piece | None). | |
| For queen-type moves landing on promotion ranks, the caller must check | |
| whether the source piece is a pawn and add ``chess.QUEEN`` promotion. | |
| Args: | |
| from_sq: Source square (0–63). | |
| plane: Action plane index (0–72). | |
| Returns: | |
| (to_sq, promotion_piece) — promotion_piece is set only for underpromotions. | |
| """ | |
| from_file = from_sq % 8 | |
| from_rank = from_sq // 8 | |
| if plane < 56: | |
| # Queen-type move | |
| dir_idx = plane // 7 | |
| distance = plane % 7 + 1 | |
| df, dr = QUEEN_DIRECTIONS[dir_idx] | |
| to_file = from_file + df * distance | |
| to_rank = from_rank + dr * distance | |
| return to_rank * 8 + to_file, None | |
| if plane < 64: | |
| # Knight move | |
| df, dr = KNIGHT_JUMPS[plane - 56] | |
| to_file = from_file + df | |
| to_rank = from_rank + dr | |
| return to_rank * 8 + to_file, None | |
| # Underpromotion | |
| promo_idx = plane - 64 | |
| dir_idx = promo_idx // 3 | |
| piece_idx = promo_idx % 3 | |
| df = dir_idx - 1 # 0→-1, 1→0, 2→+1 | |
| to_file = from_file + df | |
| to_rank = 7 if from_rank == 6 else 0 # white promotes to rank 8, black to rank 1 | |
| return to_rank * 8 + to_file, UNDERPROMO_PIECES[piece_idx] | |
| def build_legal_moves_planes(board: chess.Board) -> torch.Tensor: | |
| """Build a (64, 73) boolean mask of legal moves in action-plane format.""" | |
| mask = torch.zeros(64, NUM_ACTION_PLANES, dtype=torch.bool) | |
| for move in board.legal_moves: | |
| plane = move_to_action_plane(move.from_square, move.to_square, move.promotion) | |
| mask[move.from_square, plane] = True | |
| return mask | |