| """Chess-board preprocessing for the Hugging Face model. |
| |
| This is deliberately a board encoder rather than a text tokenizer. Each |
| board becomes 64 mover-relative piece IDs, and each legal move is represented |
| by the tokenized board after that move. Successors remain in the original |
| position's perspective. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from collections.abc import Sequence |
| from typing import Any |
|
|
| import chess |
| import torch |
|
|
|
|
| EMPTY = 0 |
| MY_PAWN = 1 |
| MY_KNIGHT = 2 |
| MY_BISHOP = 3 |
| MY_ROOK = 4 |
| MY_QUEEN = 5 |
| MY_KING = 6 |
| OPPONENT_OFFSET = 6 |
| NUM_PIECE_STATES = 13 |
|
|
|
|
| def canonical_square(square: chess.Square, perspective: chess.Color) -> int: |
| """Map a square into mover-relative canonical order.""" |
|
|
| return square if perspective == chess.WHITE else square ^ 56 |
|
|
|
|
| def tokenize_board( |
| board: chess.Board, perspective: chess.Color | None = None |
| ) -> torch.Tensor: |
| """Return a board as a ``[64]`` uint8 tensor of piece-role IDs.""" |
|
|
| if perspective is None: |
| perspective = board.turn |
| tokens = torch.zeros(64, dtype=torch.uint8) |
| for square, piece in board.piece_map().items(): |
| role = piece.piece_type |
| if piece.color != perspective: |
| role += OPPONENT_OFFSET |
| tokens[canonical_square(square, perspective)] = role |
| return tokens |
|
|
|
|
| def build_model_inputs( |
| boards: Sequence[chess.Board], |
| *, |
| include_candidate_uci: bool = False, |
| ) -> dict[str, Any]: |
| """Build the tensors expected by ``ChessTransitionPolicy``. |
| |
| Candidate order is the order returned by ``python-chess``. If |
| ``include_candidate_uci`` is true, the returned dictionary also contains |
| ``candidate_uci`` for mapping output scores back to moves. |
| """ |
|
|
| if not boards: |
| raise ValueError("At least one board is required") |
|
|
| current_boards: list[torch.Tensor] = [] |
| successor_boards: list[torch.Tensor] = [] |
| candidate_uci: list[tuple[str, ...]] = [] |
| candidate_counts: list[int] = [] |
|
|
| for board in boards: |
| perspective = board.turn |
| legal_moves = list(board.legal_moves) |
| if not legal_moves: |
| raise ValueError("Every board must have at least one legal move") |
| current_boards.append(tokenize_board(board, perspective)) |
| candidate_counts.append(len(legal_moves)) |
| if include_candidate_uci: |
| candidate_uci.append(tuple(move.uci() for move in legal_moves)) |
| for move in legal_moves: |
| board.push(move) |
| successor_boards.append(tokenize_board(board, perspective)) |
| board.pop() |
|
|
| counts = torch.tensor(candidate_counts, dtype=torch.int32) |
| offsets = torch.zeros(len(boards) + 1, dtype=torch.int64) |
| offsets[1:] = torch.cumsum(counts.to(torch.int64), dim=0) |
| owners = torch.repeat_interleave( |
| torch.arange(len(boards), dtype=torch.int64), counts.to(torch.int64) |
| ) |
| max_candidates = max(candidate_counts) |
| candidate_mask = ( |
| torch.arange(max_candidates).unsqueeze(0) |
| < counts.unsqueeze(1) |
| ) |
|
|
| result: dict[str, Any] = { |
| "current_boards": torch.stack(current_boards), |
| "successor_boards": torch.stack(successor_boards), |
| "candidate_owner": owners, |
| "candidate_offsets": offsets, |
| "candidate_mask": candidate_mask, |
| } |
| if include_candidate_uci: |
| result["candidate_uci"] = tuple(candidate_uci) |
| return result |
|
|