ChessTransformer / chesstransformer /models /tokenizer /alphazero_move_encoder.py
system's picture
system HF Staff
Deploy from 1040f46
a5f2e46 verified
Raw
History Blame Contribute Delete
4.61 kB
"""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