File size: 3,322 Bytes
a162612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Position + move -> Laya `score` question. Must match the training notebook exactly."""
import chess
import numpy as np

NAMES = {chess.PAWN: "pawn", chess.KNIGHT: "knight", chess.BISHOP: "bishop",
         chess.ROOK: "rook", chess.QUEEN: "queen", chess.KING: "king"}
ORDER = [chess.KING, chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT, chess.PAWN]

DEFAULT_N_LEVELS = 10
DEFAULT_INS_TMPL = "{side} plays {san} ({piece} {frm}-{to}{extra}). Win chance for {side}?"


def piece_list(board, color):
    out = []
    for pt in ORDER:
        for sq in sorted(board.pieces(pt, color)):
            out.append(chess.piece_symbol(pt).upper() + chess.square_name(sq))
    return " ".join(out)


def board_state(board):
    return {
        "to_move": "white" if board.turn else "black",
        "white": piece_list(board, chess.WHITE),
        "black": piece_list(board, chess.BLACK),
        "castling": board.castling_xfen() if board.castling_rights else "-",
        "en_passant": chess.square_name(board.ep_square) if board.has_legal_en_passant() else "-",
    }


class MoveEncoder:
    """Builds the token ids for "how good is `move` in `board`?" exactly as in training.

    Level labels and the instruction template come from the checkpoint's chess_meta.json, so the
    engine always asks the question the model was trained on.
    """

    def __init__(self, tokenizer, laya_cfg, meta=None):
        from laya.common import build_sequence
        meta = meta or {}
        if meta.get("state_format", "piece lists v2") != "piece lists v2":
            raise ValueError(f"checkpoint uses state format {meta['state_format']!r}, engine supports 'piece lists v2'")
        self.n_levels = int(meta.get("n_levels", DEFAULT_N_LEVELS))
        edges = np.linspace(0, 1, self.n_levels + 1)
        self.centers = ((edges[:-1] + edges[1:]) / 2).astype(np.float32)
        self.crit = meta.get("crit") or [f"{edges[i]*100:.0f}-{edges[i+1]*100:.0f}%" for i in range(self.n_levels)]
        self.ins_tmpl = meta.get("ins_template") or DEFAULT_INS_TMPL
        self._build_sequence = build_sequence
        self.tokenizer, self.max_len, self.head_max_len = tokenizer, laya_cfg["max_len"], laya_cfg["head_max_len"]

    def question(self, board, move):
        piece = board.piece_at(move.from_square)
        extra = ""
        if board.is_capture(move):
            cap = board.piece_at(move.to_square)
            extra += f", takes {NAMES[cap.piece_type] if cap else 'pawn'}"
        if move.promotion:
            extra += f", promotes to {NAMES[move.promotion]}"
        if board.gives_check(move):
            extra += ", check"
        side = "white" if board.turn else "black"
        ins = self.ins_tmpl.format(side=side, san=board.san(move), piece=NAMES[piece.piece_type],
                                   frm=chess.square_name(move.from_square), to=chess.square_name(move.to_square),
                                   extra=extra)
        return {"t": "score", "ins": ins, "crit": self.crit}

    def encode(self, board, move, state=None):
        state = state if state is not None else board_state(board)
        ids, markers = self._build_sequence(self.tokenizer, state, self.question(board, move),
                                            self.max_len, self.head_max_len)[:2]
        return ids, markers