Download tinychess/encoding.py from cazyundee/training: direct link, hf CLI and curl.
- Browser
- Download file 7.32 kB
-
https://huggingface.co/spaces/cazyundee/training/resolve/main/tinychess/encoding.py
- Command line
-
hf download hf://spaces/cazyundee/training/tinychess/encoding.py
-
curl -L -o encoding.py https://huggingface.co/spaces/cazyundee/training/resolve/main/tinychess/encoding.py
7.32 kB
| """ | |
| Board and move encoding for TinyChess. | |
| Design notes | |
| ------------ | |
| * Positions are *canonicalised* to side-to-move = White by vertically mirroring | |
| the board and swapping piece colours when Black is to move. This is a | |
| lossless symmetry of chess (ignoring nothing: castling rights and en-passant | |
| are mirrored too), and it is a standard, non-cheating canonicalisation. | |
| * A move is packed into a single int32 with structural fields only. We | |
| deliberately store *rules-derived* structure (from/to/promo/piece/capture/ | |
| castle/ep) and no heuristic evaluation. | |
| * Per-square features are kept separate (64 vectors); nothing is collapsed to a | |
| single vector at encoding time. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import chess | |
| # --------------------------------------------------------------------------- | |
| # Move packing | |
| # --------------------------------------------------------------------------- | |
| # bits: from(6) | to(6) | promo(3) | piece(3) | capture(1) | castle(1) | ep(1) | |
| _F_FROM = 0 | |
| _F_TO = 6 | |
| _F_PROMO = 12 | |
| _F_PIECE = 15 | |
| _F_CAP = 18 | |
| _F_CASTLE = 19 | |
| _F_EP = 20 | |
| PROMO_CODES = {None: 0, chess.KNIGHT: 1, chess.BISHOP: 2, chess.ROOK: 3, chess.QUEEN: 4} | |
| PROMO_INV = {v: k for k, v in PROMO_CODES.items()} | |
| N_PROMO = 5 | |
| N_PIECE_TYPES = 6 # pawn..king -> 0..5 | |
| MOVE_FIELD_SIZES = dict(from_sq=64, to_sq=64, promo=N_PROMO, piece=N_PIECE_TYPES, | |
| capture=2, castle=2, ep=2) | |
| def pack_move(board: chess.Board, move: chess.Move) -> int: | |
| """Pack a legal move on `board` into an int32 of structural fields.""" | |
| piece = board.piece_at(move.from_square) | |
| ptype = (piece.piece_type - 1) if piece is not None else 0 | |
| is_ep = board.is_en_passant(move) | |
| is_cap = board.is_capture(move) | |
| is_castle = board.is_castling(move) | |
| v = (move.from_square << _F_FROM) | (move.to_square << _F_TO) | |
| v |= PROMO_CODES.get(move.promotion, 0) << _F_PROMO | |
| v |= ptype << _F_PIECE | |
| v |= int(is_cap) << _F_CAP | |
| v |= int(is_castle) << _F_CASTLE | |
| v |= int(is_ep) << _F_EP | |
| return v | |
| def unpack_move(v: int) -> dict: | |
| return dict( | |
| from_sq=(v >> _F_FROM) & 63, | |
| to_sq=(v >> _F_TO) & 63, | |
| promo=(v >> _F_PROMO) & 7, | |
| piece=(v >> _F_PIECE) & 7, | |
| capture=(v >> _F_CAP) & 1, | |
| castle=(v >> _F_CASTLE) & 1, | |
| ep=(v >> _F_EP) & 1, | |
| ) | |
| def unpack_fields(v: np.ndarray) -> np.ndarray: | |
| """Vectorised unpack -> int64 array [..., 7] in MOVE_FIELD order.""" | |
| v = np.asarray(v, dtype=np.int64) | |
| return np.stack([ | |
| (v >> _F_FROM) & 63, | |
| (v >> _F_TO) & 63, | |
| (v >> _F_PROMO) & 7, | |
| (v >> _F_PIECE) & 7, | |
| (v >> _F_CAP) & 1, | |
| (v >> _F_CASTLE) & 1, | |
| (v >> _F_EP) & 1, | |
| ], axis=-1) | |
| MOVE_FIELD_ORDER = ["from_sq", "to_sq", "promo", "piece", "capture", "castle", "ep"] | |
| def packed_to_uci(v: int) -> str: | |
| d = unpack_move(v) | |
| m = chess.Move(d["from_sq"], d["to_sq"], promotion=PROMO_INV.get(d["promo"])) | |
| return m.uci() | |
| def packed_to_move(v: int) -> chess.Move: | |
| d = unpack_move(v) | |
| return chess.Move(d["from_sq"], d["to_sq"], promotion=PROMO_INV.get(d["promo"])) | |
| # --------------------------------------------------------------------------- | |
| # Canonicalisation | |
| # --------------------------------------------------------------------------- | |
| def canonical_board(board: chess.Board) -> tuple[chess.Board, bool]: | |
| """Return (board with White to move, flipped?). | |
| Uses python-chess `mirror()` which flips vertically and swaps colours, | |
| correctly transforming castling rights and the en-passant square. | |
| """ | |
| if board.turn == chess.WHITE: | |
| return board, False | |
| return board.mirror(), True | |
| def flip_square(sq: int) -> int: | |
| return sq ^ 56 | |
| def flip_move(move: chess.Move) -> chess.Move: | |
| return chess.Move(flip_square(move.from_square), flip_square(move.to_square), | |
| promotion=move.promotion) | |
| # --------------------------------------------------------------------------- | |
| # Board -> feature arrays | |
| # --------------------------------------------------------------------------- | |
| # Per-square categorical: 0 = empty, 1..6 = own P,N,B,R,Q,K, 7..12 = opp | |
| N_PIECE_TOKENS = 13 | |
| # Per-square binary extras | |
| SQ_EXTRA = ["last_from", "last_to", "ep_square", "own_backrank_castle"] | |
| N_SQ_EXTRA = len(SQ_EXTRA) | |
| GLOBAL_FEATS = [ | |
| "own_oo", "own_ooo", "opp_oo", "opp_ooo", | |
| "has_ep", | |
| "halfmove_clock_n", "fullmove_n", "repetition_n", | |
| "own_pawns_n", "own_knights_n", "own_bishops_n", "own_rooks_n", "own_queens_n", | |
| "opp_pawns_n", "opp_knights_n", "opp_bishops_n", "opp_rooks_n", "opp_queens_n", | |
| "in_check", "material_diff_n", | |
| ] | |
| N_GLOBAL = len(GLOBAL_FEATS) | |
| _PT_ORDER = [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN, chess.KING] | |
| _PIECE_VAL = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, | |
| chess.QUEEN: 9, chess.KING: 0} | |
| def encode_board(cboard: chess.Board, last_move: chess.Move | None = None, | |
| repetition: int = 0): | |
| """Encode a *canonical* (White-to-move) board. | |
| Returns (squares int8[64], extras int8[64,N_SQ_EXTRA], glob float32[N_GLOBAL]). | |
| """ | |
| sq = np.zeros(64, dtype=np.int8) | |
| for s, piece in cboard.piece_map().items(): | |
| idx = _PT_ORDER.index(piece.piece_type) + 1 | |
| if piece.color == chess.BLACK: | |
| idx += 6 | |
| sq[s] = idx | |
| extras = np.zeros((64, N_SQ_EXTRA), dtype=np.int8) | |
| if last_move is not None: | |
| extras[last_move.from_square, 0] = 1 | |
| extras[last_move.to_square, 1] = 1 | |
| if cboard.ep_square is not None: | |
| extras[cboard.ep_square, 2] = 1 | |
| if cboard.has_kingside_castling_rights(chess.WHITE): | |
| extras[chess.H1, 3] = 1 | |
| extras[chess.E1, 3] = 1 | |
| if cboard.has_queenside_castling_rights(chess.WHITE): | |
| extras[chess.A1, 3] = 1 | |
| extras[chess.E1, 3] = 1 | |
| g = np.zeros(N_GLOBAL, dtype=np.float32) | |
| g[0] = cboard.has_kingside_castling_rights(chess.WHITE) | |
| g[1] = cboard.has_queenside_castling_rights(chess.WHITE) | |
| g[2] = cboard.has_kingside_castling_rights(chess.BLACK) | |
| g[3] = cboard.has_queenside_castling_rights(chess.BLACK) | |
| g[4] = cboard.ep_square is not None | |
| g[5] = min(cboard.halfmove_clock, 100) / 100.0 | |
| g[6] = min(cboard.fullmove_number, 120) / 120.0 | |
| g[7] = min(repetition, 3) / 3.0 | |
| mat = 0 | |
| for i, pt in enumerate(_PT_ORDER[:5]): | |
| nw = len(cboard.pieces(pt, chess.WHITE)) | |
| nb = len(cboard.pieces(pt, chess.BLACK)) | |
| g[8 + i] = nw / 8.0 | |
| g[13 + i] = nb / 8.0 | |
| mat += _PIECE_VAL[pt] * (nw - nb) | |
| g[18] = cboard.is_check() | |
| g[19] = np.tanh(mat / 10.0) | |
| return sq, extras, g | |
| def encode_position(board: chess.Board, last_move: chess.Move | None = None, | |
| repetition: int = 0): | |
| """Canonicalise then encode. Returns (squares, extras, glob, flipped).""" | |
| cb, flipped = canonical_board(board) | |
| lm = last_move | |
| if flipped and lm is not None: | |
| lm = flip_move(lm) | |
| sq, ex, g = encode_board(cb, lm, repetition) | |
| return sq, ex, g, flipped | |
| def legal_packed(board: chess.Board): | |
| """Return (packed candidate array int32[L], canonical board, flipped).""" | |
| cb, flipped = canonical_board(board) | |
| return np.array([pack_move(cb, m) for m in cb.legal_moves], dtype=np.int32), cb, flipped | |