YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Charles the Chess Bot
This is Charles, a bot designed by me for EnderChef's chess model competition. While not the best player on earth, Charles moves quickly and doesn't really worry about the future, instead opting to choose whichever move feels right given the current board.
I like to describe Charles' architecture "0 shot" as after hearing about the competition, I, without looking at any other model architectures, came up with the whole thing while walking home (around 20 mins). Also during training there were no iterations or changes, making it truly a 0 shot design. Because of this Charles has some clear design flaws which are evident in the fact that it only reaches around 2000 elo and many mistakes became obvious to me while training.
Architecture
Charles contains 32,537,088 parameters and consists of two primary parts: the Board Encoder and the Move Decider. The data flow is:
- The Board Encoder encodes the current board state.
- The Board Encoder encodes the board state after every legal move.
- Move embeddings are created by subtracting the current board embedding from the corresponding successor-board embedding. This produces one transition embedding per legal move, typically around 20.
- The original current-board embedding is preserved unchanged as the fixed scoring query.
- A copy of the current-board embedding is prepended to the move embeddings and passed through the Move Decider.
- The Move Decider contextualizes the copied board token and all move tokens using bidirectional attention.
- The contextualized move embeddings are projected through a learned, bias-free key matrix.
- The original, unaltered current-board embedding is projected through a learned, bias-free query matrix.
- Query and key vectors are RMS-normalized and compared with scaled dot products to produce one logit per legal move.
- The highest-scoring legal move is selected greedily.
Board Encoder
| Component | Details |
|---|---|
| Input | Tokenized board [64] |
| Piece vocabulary | 13 IDs: empty, mover pieces, opponent pieces |
| Piece embedding | 13 x 384 |
| Square embedding | Learned absolute 64 x 384 embeddings |
| Board tokens | Piece embedding + square embedding |
| Summary token | Learned 384-dimensional token prepended |
| Transformer | 6 bidirectional transformer layers |
| Attention | 6 heads, 64 dimensions per head |
| Feed-forward | SwiGLU, hidden size 1,040 |
| Output | One 384-dimensional board embedding |
| Causal masking | None |
Move Decider
| Component | Details |
|---|---|
| Current board | Encoded once by the Board Encoder |
| Successor boards | Encoded once per legal move |
| Move embeddings | successor_embedding - current_embedding |
| Candidate count | Typically around 20 legal moves |
| Candidate sequence | Current board copy + all move embeddings |
| Type embeddings | Separate board-token and move-token embeddings |
| Candidate positions | No candidate position embeddings |
| Transformer | 12 bidirectional transformer layers |
| Attention | 6 heads, 64 dimensions per head |
| Feed-forward | SwiGLU, hidden size 1,040 |
| Query source | Original, unaltered current-board embedding |
| Query projection | Bias-free 384 x 384 linear layer |
| Key source | Contextualized move embeddings |
| Key projection | Bias-free 384 x 384 linear layer |
| Normalization | Parameter-free RMSNorm on queries and keys |
| Scoring | Scaled query-key dot product |
| Output | One logit per legal move |
| Padding | Invalid candidates masked to -inf |
Training
The model was trained in three phases on tournament chess positions. Legal moves and successor boards were generated with python-chess.
| Phase | Data and process | Objective and details | Approx. Elo |
|---|---|---|---|
| 1. Supervised policy | Human tournament positions; the recorded human move is the target | Cross-entropy over all legal moves. AdamW; learning rate 4e-4 -> 3e-5; 2,000-step warmup; batch size 32; gradient accumulation 4; BF16 autocast; 0.0 dropout |
1300-1400 |
| 2. Stockfish distillation | The same positions annotated with Stockfish scores for every legal move | KL distillation against the full Stockfish move distribution. Stockfish depth 12; teacher temperature 30; student temperature 1.0; fine-tuned from Phase 1 | 1700-1800 |
| 3. On-policy training | The model generates fresh games while Stockfish plays the opponent; completed rollouts are used once | For the move selected by the model, minimize (model_probability - Stockfish_probability) ** 2. Unchosen moves receive no direct loss. Muon for matrix weights plus AdamW for other parameters; Opponent depth 8; Teacher depth 10; 10,000-step schedule |
~2000 |
All phases use fully bidirectional scaled-dot-product attention. Phase 3 keeps the model's own actions rather than replacing them with Stockfish's best move, so the rollout distribution remains on-policy.
Changes
Changes id make if I had the time to iterate include:
- Static + RoPE Square embeddings: As squares both have absolute identities and relative identities
- Replace move subtraction: I reckon a linear subtraction is a rich enough transform for this task
- More Capacity: Since this Charles relies on alot of emergent properties such as next move planning and overall generalization, more capacity would almost certainly help
- Self Play: This was originally planned but ran out of time
- Downloads last month
- -