Chessmate 2026

;; ≈ 2,940 Elo

Chessmate 500ms

You

;; warming the interpreter…

Play As
Game Mode
White
Black
Aesthetics

    ;; the 2005 engine:

    Settings

    What's new in Chessmate 2026

    ;; the post: · this engine:

    ;; drag a piece, or tap it and then its square · ← ↑ → ↓ move on the board · Enter picks up and puts down · Esc puts down

    Source

    ;; 12 namespaces, 4289 lines, as this machine runs them

    chessmate.v2.pesto · 137 lines
    (ns chessmate.v2.pesto
      "The piece values and piece-square tables of Chessmate 2026's evaluation: Ronald
      Friederich's PeSTO (\"Piece-Square Tables Only\"), tuned by Texel's method for his engine
      RofChade, as the Chess Programming Wiki publishes them (CC BY-SA 3.0):
      https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function
    
      Each piece has two values and two tables, one for the middlegame and one for the endgame,
      in centipawns. chessmate.v2.board blends them by how much material is left.")
    
    (def mg-values
      "The middlegame values, by type: none, pawn, knight, bishop, rook, queen and king."
      #js [0 82 337 365 477 1025 0])
    
    (def eg-values
      "The endgame values, by type, as mg-values."
      #js [0 94 281 297 512 936 0])
    
    (def phase-weights
      "How much each type counts towards the middlegame, by type: 24 with every piece on the
      board, and 0 with only kings and pawns."
      #js [0 0 1 1 2 4 0])
    
    (def mg-tables
      "The middlegame tables, by type: pawn, knight, bishop, rook, queen and king, each
      a8 first, as White sees the board."
      #js [#js [;; pawn
                0       0    0    0    0    0    0    0
                98    134   61   95   68  126   34  -11
                -6      7   26   31   65   56   25  -20
                -14    13    6   21   23   12   17  -23
                -27    -2   -5   12   17    6   10  -25
                -26    -4   -4  -10    3    3   33  -12
                -35    -1  -20  -23  -15   24   38  -22
                0       0    0    0    0    0    0    0]
           #js [;; knight
                -167  -89  -34  -49   61  -97  -15 -107
                -73   -41   72   36   23   62    7  -17
                -47    60   37   65   84  129   73   44
                -9     17   19   53   37   69   18   22
                -13     4   16   13   28   19   21   -8
                -23    -9   12   10   19   17   25  -16
                -29   -53  -12   -3   -1   18  -14  -19
                -105  -21  -58  -33  -17  -28  -19  -23]
           #js [;; bishop
                -29     4  -82  -37  -25  -42    7   -8
                -26    16  -18  -13   30   59   18  -47
                -16    37   43   40   35   50   37   -2
                -4      5   19   50   37   37    7   -2
                -6     13   13   26   34   12   10    4
                0      15   15   15   14   27   18   10
                4      15   16    0    7   21   33    1
                -33    -3  -14  -21  -13  -12  -39  -21]
           #js [;; rook
                32     42   32   51   63    9   31   43
                27     32   58   62   80   67   26   44
                -5     19   26   36   17   45   61   16
                -24   -11    7   26   24   35   -8  -20
                -36   -26  -12   -1    9   -7    6  -23
                -45   -25  -16  -17    3    0   -5  -33
                -44   -16  -20   -9   -1   11   -6  -71
                -19   -13    1   17   16    7  -37  -26]
           #js [;; queen
                -28     0   29   12   59   44   43   45
                -24   -39   -5    1  -16   57   28   54
                -13   -17    7    8   29   56   47   57
                -27   -27  -16  -16   -1   17   -2    1
                -9    -26   -9  -10   -2   -4    3   -3
                -14     2  -11   -2   -5    2   14    5
                -35    -8   11    2    8   15   -3    1
                -1    -18   -9   10  -15  -25  -31  -50]
           #js [;; king
                -65    23   16  -15  -56  -34    2   13
                29     -1  -20   -7   -8   -4  -38  -29
                -9     24    2  -16  -20    6   22  -22
                -17   -20  -12  -27  -30  -25  -14  -36
                -49    -1  -27  -39  -46  -44  -33  -51
                -14   -14  -22  -46  -44  -30  -15  -27
                1       7   -8  -64  -43  -16    9    8
                -15    36   12  -54    8  -28   24   14]])
    
    (def eg-tables
      "The endgame tables, by type: pawn, knight, bishop, rook, queen and king, each
      a8 first, as White sees the board."
      #js [#js [;; pawn
                0       0    0    0    0    0    0    0
                178   173  158  134  147  132  165  187
                94    100   85   67   56   53   82   84
                32     24   13    5   -2    4   17   17
                13      9   -3   -7   -7   -8    3   -1
                4       7   -6    1    0   -5   -1   -8
                13      8    8   10   13    0    2   -7
                0       0    0    0    0    0    0    0]
           #js [;; knight
                -58   -38  -13  -28  -31  -27  -63  -99
                -25    -8  -25   -2   -9  -25  -24  -52
                -24   -20   10    9   -1   -9  -19  -41
                -17     3   22   22   22   11    8  -18
                -18    -6   16   25   16   17    4  -18
                -23    -3   -1   15   10   -3  -20  -22
                -42   -20  -10   -5   -2  -20  -23  -44
                -29   -51  -23  -15  -22  -18  -50  -64]
           #js [;; bishop
                -14   -21  -11   -8   -7   -9  -17  -24
                -8     -4    7  -12   -3  -13   -4  -14
                2      -8    0   -1   -2    6    0    4
                -3      9   12    9   14   10    3    2
                -6      3   13   19    7   10   -3   -9
                -12    -3    8   10   13    3   -7  -15
                -14   -18   -7   -1    4   -9  -15  -27
                -23    -9  -23   -5   -9  -16   -5  -17]
           #js [;; rook
                13     10   18   15   12   12    8    5
                11     13   13   11   -3    3    8    3
                7       7    7    5    4   -3   -5   -3
                4       3   13    1    2    1   -1    2
                3       5    8    4   -5   -6   -8  -11
                -4      0   -5   -1   -7  -12   -8  -16
                -6     -6    0    2   -9   -9  -11   -3
                -9      2    3   -1   -5  -13    4  -20]
           #js [;; queen
                -9     22   22   27   27   19   10   20
                -17    20   32   41   58   25   30    0
                -20     6    9   49   47   35   19    9
                3      22   24   45   57   40   57   36
                -18    28   19   47   31   34   39   23
                -16   -27   15    6    9   17   10    5
                -22   -23  -30  -16  -16  -23  -36  -32
                -33   -28  -22  -43   -5  -32  -20  -41]
           #js [;; king
                -74   -35  -18  -18  -11   15    4  -17
                -12    17   14   17   17   38   23   11
                10     17   23   15   20   45   44   13
                -8     22   24   27   26   33   26    3
                -18    -4   21   24   27   23    9  -11
                -19    -3   11   21   23   16    7   -9
                -27   -11    4   13   14    4   -5  -17
                -53   -34  -21  -11  -28  -14  -24  -43]])
    chessmate.v2.board · 774 lines
    (ns chessmate.v2.board
      "Chessmate 2026's board, built for speed. There is one position, changed in place by make!
      and taken back by unmake!, and it lives in typed arrays that the whole engine shares, as the
      2005 search kept its state in statics. The 2005 rules (chessmate.board and chessmate.moves) still judge the game; this
      board is the search's, and perft checks it against the published counts.
    
      Squares are 0x88: rank * 16 + file, counting from 0, so a1 is 0, h1 is 7 and a8 is 112. A
      number with a bit of 0x88 set is off the board, which is how a step off the edge is seen
      without a border, and the difference of two squares names the line between them.
    
      Pieces are a type, 1 to 6 for pawn, knight, bishop, rook, queen and king, plus 8 for
      Black's. Sides are 0 for White and 1 for Black.
    
      Moves are numbers: from + 128 * to + 16384 * the type a pawn promotes to, plus a flag for
      an en passant capture, a castle or a pawn's double step.
    
      Alongside the pieces, the board keeps what the search asks about most: the position's
      Zobrist hash, in two 32-bit halves, and its PeSTO score, both updated move by move."
      (:require
       [chessmate.v2.pesto :as pesto]
       [clojure.string :as str]))
    
    ;; Pieces and sides
    
    (def ^:const pawn 1)
    (def ^:const knight 2)
    (def ^:const bishop 3)
    (def ^:const rook 4)
    (def ^:const queen 5)
    (def ^:const king 6)
    (def ^:const black 8)
    
    (defn kind "A piece's type, 1 to 6." [piece] (bit-and piece 7))
    
    (defn div
      "a divided by b, truncated towards zero as quot does, for numbers within 32 bits. quot
      compiles to a function call; this compiles to one division."
      [a b]
      (bit-or (/ a b) 0))
    
    (defn larger "The larger of two numbers: max, which compiles to a function call, as a comparison." [a b] (if (> a b) a b))
    
    (defn smaller "The smaller of two numbers, as min." [a b] (if (< a b) a b))
    
    (defn side-of "A piece's side: 0 for White, 1 for Black." [piece] (bit-shift-right piece 3))
    
    ;; Moves
    
    (def ^:const en-passant-flag 131072)
    (def ^:const castle-flag 262144)
    (def ^:const double-step-flag 524288)
    
    (defn move-from [m] (bit-and m 127))
    
    (defn move-to [m] (bit-and (bit-shift-right m 7) 127))
    
    (defn promotion "The type a move promotes to, or 0." [m] (bit-and (bit-shift-right m 14) 7))
    
    ;; Squares
    
    (defn square "The 0x88 square of a file and rank, each from 0." [file rank] (+ (* rank 16) file))
    
    (defn file-of [sq] (bit-and sq 7))
    
    (defn rank-of [sq] (bit-shift-right sq 4))
    
    (defn square-name [sq] (str (nth "abcdefgh" (file-of sq)) (inc (rank-of sq))))
    
    (defn parse-square
      "The 0x88 square a name such as \"e4\" names, or -1."
      [s]
      (if (re-matches #"[a-h][1-8]" s)
        (square (- (.charCodeAt s 0) 97) (- (.charCodeAt s 1) 49))
        -1))
    
    (defn uci
      "A move in UCI's coordinates, such as e2e4, or e7e8q for a promotion."
      [m]
      (str (square-name (move-from m)) (square-name (move-to m)) (nth ["" "" "n" "b" "r" "q"] (promotion m))))
    
    ;; The position
    
    (def squares "The piece on each 0x88 square, or 0." (js/Int8Array. 128))
    
    (def lists
      "Each side's pieces, by square, in no order: White's in the first 16 places, Black's in the
      next 16."
      (js/Int32Array. 32))
    
    (def list-index "Where the piece on each square stands in its side's list." (js/Int32Array. 128))
    
    (def counts "How many pieces each side has." (js/Int32Array. 2))
    
    (def kings "Each side's king's square." (js/Int32Array. 2))
    
    ;; The state, as slots of one Int32Array
    (def ^:const side-slot 0) ; the side to move
    (def ^:const castling-slot 1) ; the castling rights: 1 and 2 White's short and long, 4 and 8 Black's
    (def ^:const ep-slot 2) ; the square a pawn has just passed, when an enemy pawn could take on it; else -1
    (def ^:const halfmove-slot 3) ; half-moves since the last capture or pawn move
    (def ^:const ply-slot 4) ; moves made since the position was set, which index the history
    (def ^:const hash-lo-slot 5) ; the Zobrist hash, low half
    (def ^:const hash-hi-slot 6) ; and high half
    (def ^:const mg-slot 7) ; the PeSTO middlegame score, White's minus Black's
    (def ^:const eg-slot 8) ; and the endgame score
    (def ^:const phase-slot 9) ; the material left, 24 at the start
    (def ^:const fullmove-slot 10)
    
    (def state "The side to move and the rest, by the slots above." (js/Int32Array. 11))
    
    (defn side "The side to move." [] (aget state side-slot))
    
    ;; The history: what each move changed, for unmake! and for the repetition test
    (def ^:const undo-stride 12)
    (def ^:const max-history 1024)
    (def undo "Per move made, by ply-slot: the move and the state before it (see make!)." (js/Int32Array. (* max-history undo-stride)))
    
    ;; Zobrist keys: pieces at piece * 128 + square, castling rights at 2048 + rights, the en
    ;; passant file at 2064 + file, the side at 2072. Drawn from a fixed mulberry32 sequence,
    ;; so that every runtime hashes alike. The generator must not be linear: with xorshift32, each
    ;; key's high half was a linear function of its low half, so every hash's was too, and the
    ;; hash had 32 bits, not 64 (found in review, 2026-09-25).
    
    (def ^:private zobrist-lo (js/Int32Array. 2080))
    (def ^:private zobrist-hi (js/Int32Array. 2080))
    
    (let [x (js/Int32Array. #js [0x2545F491])
          next! (fn []
                  (let [a (bit-or (+ (aget x 0) 0x6D2B79F5) 0)
                        _ (aset x 0 a)
                        t (js/Math.imul (bit-xor a (unsigned-bit-shift-right a 15)) (bit-or a 1))
                        t (bit-xor t (+ t (js/Math.imul (bit-xor t (unsigned-bit-shift-right t 7)) (bit-or t 61))))]
                    (bit-or (bit-xor t (unsigned-bit-shift-right t 14)) 0)))]
      (dotimes [i 2080]
        (aset zobrist-lo i (next!))
        (aset zobrist-hi i (next!))))
    
    (def ^:const castling-keys 2048)
    (def ^:const ep-keys 2064)
    (def ^:const side-key 2072)
    
    (defn- toggle! [k]
      (aset state hash-lo-slot (bit-xor (aget state hash-lo-slot) (aget zobrist-lo k)))
      (aset state hash-hi-slot (bit-xor (aget state hash-hi-slot) (aget zobrist-hi k))))
    
    ;; PeSTO's tables per 0x88 square, White's positive and Black's negative, values included
    
    (def pst-mg "By piece * 128 + square." (js/Int32Array. 2048))
    (def pst-eg "By piece * 128 + square." (js/Int32Array. 2048))
    (def phase-weight "By piece." (js/Int32Array. 16))
    
    (defn- fill-psts! []
      (dotimes [t 6]
        (let [kind (inc t)]
          (aset phase-weight kind (aget pesto/phase-weights kind))
          (aset phase-weight (+ kind black) (aget pesto/phase-weights kind))
          (dotimes [i 64]
            (let [file (bit-and i 7)
                  rank (- 7 (bit-shift-right i 3)) ; the tables start at a8
                  white-sq (square file rank)
                  black-sq (square file (- 7 rank))] ; Black's pieces read the table upside down
              (aset pst-mg (+ (* kind 128) white-sq) (+ (aget pesto/mg-values kind) (aget (aget pesto/mg-tables t) i)))
              (aset pst-eg (+ (* kind 128) white-sq) (+ (aget pesto/eg-values kind) (aget (aget pesto/eg-tables t) i)))
              (aset pst-mg (+ (* (+ kind black) 128) black-sq) (- (+ (aget pesto/mg-values kind) (aget (aget pesto/mg-tables t) i))))
              (aset pst-eg (+ (* (+ kind black) 128) black-sq) (- (+ (aget pesto/eg-values kind) (aget (aget pesto/eg-tables t) i)))))))))
    
    (fill-psts!)
    
    ;; Putting pieces down and picking them up, keeping the lists, the hash and the scores
    
    (defn- put!
      "Place piece on the empty square sq, last in its side's list."
      [piece sq]
      (let [side (bit-shift-right piece 3)
            i (aget counts side)
            k (+ (* piece 128) sq)]
        (aset squares sq piece)
        (aset lists (+ (* side 16) i) sq)
        (aset list-index sq i)
        (aset counts side (inc i))
        (when (== (bit-and piece 7) king) (aset kings side sq))
        (toggle! k)
        (aset state mg-slot (+ (aget state mg-slot) (aget pst-mg k)))
        (aset state eg-slot (+ (aget state eg-slot) (aget pst-eg k)))
        (aset state phase-slot (+ (aget state phase-slot) (aget phase-weight piece)))))
    
    (defn- lift!
      "Take the piece off sq, moving the last of its side's list into its place. Returns its
      place in the list, which restore! puts it back in."
      [sq]
      (let [piece (aget squares sq)
            side (bit-shift-right piece 3)
            base (* side 16)
            i (aget list-index sq)
            last (dec (aget counts side))
            moved (aget lists (+ base last))
            k (+ (* piece 128) sq)]
        (aset lists (+ base i) moved)
        (aset list-index moved i)
        (aset counts side last)
        (aset squares sq 0)
        (toggle! k)
        (aset state mg-slot (- (aget state mg-slot) (aget pst-mg k)))
        (aset state eg-slot (- (aget state eg-slot) (aget pst-eg k)))
        (aset state phase-slot (- (aget state phase-slot) (aget phase-weight piece)))
        i))
    
    (defn- shift!
      "Move the piece on from to the empty square to."
      [from to]
      (let [piece (aget squares from)
            side (bit-shift-right piece 3)
            a (+ (* piece 128) from)
            b (+ (* piece 128) to)]
        (aset squares to piece)
        (aset squares from 0)
        (aset lists (+ (* side 16) (aget list-index from)) to)
        (aset list-index to (aget list-index from))
        (when (== (bit-and piece 7) king) (aset kings side to))
        (toggle! a)
        (toggle! b)
        (aset state mg-slot (+ (aget state mg-slot) (- (aget pst-mg b) (aget pst-mg a))))
        (aset state eg-slot (+ (aget state eg-slot) (- (aget pst-eg b) (aget pst-eg a))))))
    
    (defn- promote!
      "Turn the pawn on sq into piece."
      [sq piece]
      (let [a (+ (* (aget squares sq) 128) sq)
            b (+ (* piece 128) sq)]
        (aset squares sq piece)
        (toggle! a)
        (toggle! b)
        (aset state mg-slot (+ (aget state mg-slot) (- (aget pst-mg b) (aget pst-mg a))))
        (aset state eg-slot (+ (aget state eg-slot) (- (aget pst-eg b) (aget pst-eg a))))
        (aset state phase-slot (+ (aget state phase-slot) (aget phase-weight piece)))))
    
    ;; unmake! restores the hash and the scores from the history, so it moves pieces plainly
    
    (defn- relocate! [from to]
      (let [piece (aget squares from)]
        (aset squares to piece)
        (aset squares from 0)
        (aset lists (+ (* (bit-shift-right piece 3) 16) (aget list-index from)) to)
        (aset list-index to (aget list-index from))
        (when (== (bit-and piece 7) king) (aset kings (bit-shift-right piece 3) to))))
    
    (defn- restore!
      "Put piece back on sq at place i of its side's list, undoing lift!."
      [piece sq i]
      (let [side (bit-shift-right piece 3)
            base (* side 16)
            n (aget counts side)
            moved (aget lists (+ base i))]
        (aset lists (+ base n) moved)
        (aset list-index moved n)
        (aset lists (+ base i) sq)
        (aset list-index sq i)
        (aset counts side (inc n))
        (aset squares sq piece)))
    
    ;; Attacks. attack-bits holds, for each difference of two squares (to - from + 119), which
    ;; pieces could attack along it; attack-steps holds the step of the line between them.
    
    (def ^:private attack-bits (js/Int32Array. 240))
    (def ^:private attack-steps (js/Int32Array. 240))
    
    (def ^:private piece-bits
      "Which bits of attack-bits each piece attacks with: a knight 1, a bishop 2, a rook 4, a
      queen both, a king 8, a White pawn 16 and a Black pawn 32."
      (js/Int32Array. #js [0 16 1 2 4 6 8 0 0 32 1 2 4 6 8 0]))
    
    (def ^:private steps
      "The steps pieces take: a knight's eight, then the diagonals and the lines."
      (js/Int32Array. #js [33 31 18 14 -14 -18 -31 -33 15 17 -15 -17 1 -1 16 -16]))
    
    (def ^:private step-start "Where each type's steps start in steps." (js/Int32Array. #js [0 0 0 8 12 8 8]))
    (def ^:private step-end "Where they end." (js/Int32Array. #js [0 0 8 12 16 16 16]))
    
    (defn on-board? [sq] (zero? (bit-and sq 0x88)))
    
    (dotimes [from 128]
      (when (on-board? from)
        (dotimes [i 16]
          (let [step (aget steps i)]
            (if (< i 8)
              (when (on-board? (+ from step))
                (aset attack-bits (+ step 119) 1))
              (loop [to (+ from step)
                     near? true]
                (when (on-board? to)
                  (let [d (+ (- to from) 119)]
                    (aset attack-bits d (bit-or (aget attack-bits d)
                                                (if (< i 12) 2 4)
                                                (if near? 8 0)
                                                (if (and near? (or (== step 15) (== step 17))) 16 0)
                                                (if (and near? (or (== step -15) (== step -17))) 32 0)))
                    (aset attack-steps d step)
                    (recur (+ to step) false)))))))))
    
    (defn- ^boolean ray-clear?
      "Are the squares between from and to, along step, empty?"
      [from to step]
      (loop [sq (+ from step)]
        (if (== sq to)
          true
          (if (zero? (aget squares sq)) (recur (+ sq step)) false))))
    
    (defn ^boolean attacks?
      "Does the piece on from attack sq, on the board as it stands?"
      [from piece sq]
      (let [d (+ (- sq from) 119)]
        (if (zero? (bit-and (aget attack-bits d) (aget piece-bits piece)))
          false
          (let [k (bit-and piece 7)]
            (if (or (< k bishop) (== k king)) true (ray-clear? from sq (aget attack-steps d)))))))
    
    (defn ^boolean attacked?
      "Is sq attacked by side's pieces?"
      [sq side]
      (let [base (* side 16)
            n (aget counts side)]
        (loop [i 0]
          (if (< i n)
            (let [from (aget lists (+ base i))]
              (if (attacks? from (aget squares from) sq) true (recur (inc i))))
            false))))
    
    (defn ^boolean in-check?
      "Is the side to move in check?"
      []
      (let [us (aget state side-slot)]
        (attacked? (aget kings us) (bit-xor us 1))))
    
    (defn ^boolean legal?
      "After make!: did the move leave its side's king unattacked?"
      []
      (let [them (aget state side-slot)]
        (not (attacked? (aget kings (bit-xor them 1)) them))))
    
    ;; Move generation
    
    (defn- add! [out n m]
      (aset out n m)
      (inc n))
    
    (defn- promotions! [out n from to ^boolean captures?]
      (let [m (+ from (* to 128))
            n (add! out n (+ m (* queen 16384)))]
        (if captures?
          n
          (let [n (add! out n (+ m (* knight 16384)))
                n (add! out n (+ m (* rook 16384)))]
            (add! out n (+ m (* bishop 16384)))))))
    
    (defn- pawn-capture! [out n from to them ^boolean last-rank? ^boolean captures?]
      (if (on-board? to)
        (let [target (aget squares to)]
          (if (and (not (zero? target)) (== (bit-shift-right target 3) them))
            (if last-rank? (promotions! out n from to captures?) (add! out n (+ from (* to 128))))
            (if (== to (aget state ep-slot)) (add! out n (+ from (* to 128) en-passant-flag)) n)))
        n))
    
    (defn- pawn-moves! [out n from us ^boolean captures?]
      (let [forward (if (zero? us) 16 -16)
            to (+ from forward)
            last-rank? (== (bit-shift-right to 4) (if (zero? us) 7 0))
            them (bit-xor us 1)
            n (if (zero? (aget squares to))
                (if last-rank?
                  (promotions! out n from to captures?)
                  (if captures?
                    n
                    (if (and (== (bit-shift-right from 4) (if (zero? us) 1 6)) (zero? (aget squares (+ to forward))))
                      (add! out (add! out n (+ from (* to 128))) (+ from (* (+ to forward) 128) double-step-flag))
                      (add! out n (+ from (* to 128))))))
                n)
            n (pawn-capture! out n from (dec to) them last-rank? captures?)]
        (pawn-capture! out n from (inc to) them last-rank? captures?)))
    
    (defn- ray! [out n from step them ^boolean slides? ^boolean captures?]
      (loop [to (+ from step)
             n n]
        (if (on-board? to)
          (let [target (aget squares to)]
            (if (zero? target)
              (let [n (if captures? n (add! out n (+ from (* to 128))))]
                (if slides? (recur (+ to step) n) n))
              (if (== (bit-shift-right target 3) them) (add! out n (+ from (* to 128))) n)))
          n)))
    
    (defn- piece-moves! [out n from piece them ^boolean captures?]
      (let [k (bit-and piece 7)
            slides? (and (>= k bishop) (<= k queen))
            end (aget step-end k)]
        (loop [i (aget step-start k)
               n n]
          (if (< i end)
            (recur (inc i) (ray! out n from (aget steps i) them slides? captures?))
            n))))
    
    (defn- castle! [out n right king-sq rook-sq between safe]
      (if (and (not (zero? (bit-and (aget state castling-slot) right)))
               (== (aget squares king-sq) (+ king (if (> king-sq 7) black 0)))
               (== (aget squares rook-sq) (+ rook (if (> king-sq 7) black 0)))
               (every? #(zero? (aget squares %)) between)
               (not-any? #(attacked? % (if (> king-sq 7) 0 1)) safe))
        (add! out n (+ king-sq (* (first (rest safe)) 128) castle-flag))
        n))
    
    (defn- castles! [out n us]
      (if (zero? us)
        (-> (castle! out n 1 4 7 [5 6] [4 6 5])
            (as-> n (castle! out n 2 4 0 [3 2 1] [4 2 3])))
        (-> (castle! out n 4 116 119 [117 118] [116 118 117])
            (as-> n (castle! out n 8 116 112 [115 114 113] [116 114 115])))))
    
    (defn generate!
      "Write the side to move's moves into out from index start, and return the index after the
      last. With captures? true, only captures and promotions to a queen, for the quiescence
      search. The moves are pseudo-legal: after make!, legal? says whether the mover's king is
      safe. Castling is generated only when legal, as the probe cannot see a king cross an
      attacked square."
      [out start ^boolean captures?]
      (let [us (aget state side-slot)
            them (bit-xor us 1)
            base (* us 16)
            n (aget counts us)]
        (loop [i 0
               end start]
          (if (< i n)
            (let [from (aget lists (+ base i))
                  piece (aget squares from)]
              (recur (inc i) (if (== (bit-and piece 7) pawn)
                               (pawn-moves! out end from us captures?)
                               (piece-moves! out end from piece them captures?))))
            (if (or captures? (zero? (aget state castling-slot))) end (castles! out end us))))))
    
    ;; Making and unmaking moves
    
    (def ^:private castle-mask
      "The castling rights that survive a move from or to each square."
      (let [a (js/Int32Array. 128)]
        (.fill a 15)
        (aset a 4 12)
        (aset a 0 13)
        (aset a 7 14)
        (aset a 116 3)
        (aset a 112 7)
        (aset a 119 11)
        a))
    
    (defn- ^boolean ep-capturable?
      "Could side's pawns take en passant on ep, the square just passed? Only then is ep part of
      the position: two positions that differ only in an untakeable en passant square are one."
      [ep side]
      (let [from (+ ep (if (zero? side) -16 16))
            pawn (+ 1 (* side 8))]
        (or (and (on-board? (dec from)) (== (aget squares (dec from)) pawn))
            (and (on-board? (inc from)) (== (aget squares (inc from)) pawn)))))
    
    (defn- set-ep! [sq]
      (let [old (aget state ep-slot)]
        (when-not (== old -1) (toggle! (+ ep-keys (bit-and old 7))))
        (aset state ep-slot sq)
        (when-not (== sq -1) (toggle! (+ ep-keys (bit-and sq 7))))))
    
    (defn- set-castling! [rights]
      (let [old (aget state castling-slot)]
        (when-not (== old rights)
          (toggle! (+ castling-keys old))
          (toggle! (+ castling-keys rights))
          (aset state castling-slot rights))))
    
    (defn make!
      "Play move m, which generate! made for this position. The history keeps what unmake!
      needs: the move, the piece it took and that piece's place in its list, the castling
      rights, the en passant square, the halfmove clock, the hash and the scores."
      [m]
      (let [from (bit-and m 127)
            to (bit-and (bit-shift-right m 7) 127)
            us (aget state side-slot)
            piece (aget squares from)
            u (* (aget state ply-slot) undo-stride)]
        (aset undo u m)
        (aset undo (+ u 3) (aget state castling-slot))
        (aset undo (+ u 4) (aget state ep-slot))
        (aset undo (+ u 5) (aget state halfmove-slot))
        (aset undo (+ u 6) (aget state hash-lo-slot))
        (aset undo (+ u 7) (aget state hash-hi-slot))
        (aset undo (+ u 8) (aget state mg-slot))
        (aset undo (+ u 9) (aget state eg-slot))
        (aset undo (+ u 10) (aget state phase-slot))
        (set-ep! -1)
        (if (zero? (bit-and m castle-flag))
          (let [ep? (not (zero? (bit-and m en-passant-flag)))
                at (if ep? (+ to (if (zero? us) -16 16)) to)
                captured (aget squares at)]
            (aset undo (+ u 1) captured)
            (aset undo (+ u 2) (if (zero? captured) 0 (lift! at)))
            (shift! from to)
            (when-not (zero? (bit-and m 114688)) ; 16384 * 7: a promotion
              (promote! to (+ (bit-and (bit-shift-right m 14) 7) (* us 8))))
            (aset state halfmove-slot (if (or (== (bit-and piece 7) pawn) (not (zero? captured))) 0 (inc (aget state halfmove-slot))))
            (when (and (not (zero? (bit-and m double-step-flag))) (ep-capturable? (bit-shift-right (+ from to) 1) (bit-xor us 1)))
              (set-ep! (bit-shift-right (+ from to) 1))))
          (do (aset undo (+ u 1) 0)
              (shift! from to)
              (if (> to from) (shift! (+ to 1) (- to 1)) (shift! (- to 2) (+ to 1)))
              (aset state halfmove-slot (inc (aget state halfmove-slot)))))
        (set-castling! (bit-and (aget state castling-slot) (aget castle-mask from) (aget castle-mask to)))
        (toggle! side-key)
        (aset state side-slot (bit-xor us 1))
        (aset state ply-slot (inc (aget state ply-slot)))
        (when-not (zero? us) (aset state fullmove-slot (inc (aget state fullmove-slot))))))
    
    (defn unmake!
      "Take back the last move make! played."
      []
      (let [h (dec (aget state ply-slot))
            u (* h undo-stride)
            m (aget undo u)
            from (bit-and m 127)
            to (bit-and (bit-shift-right m 7) 127)
            us (bit-xor (aget state side-slot) 1)]
        (if (zero? (bit-and m castle-flag))
          (let [captured (aget undo (+ u 1))]
            (when-not (zero? (bit-and m 114688)) (aset squares to (+ pawn (* us 8))))
            (relocate! to from)
            (when-not (zero? captured)
              (restore! captured (if (zero? (bit-and m en-passant-flag)) to (+ to (if (zero? us) -16 16))) (aget undo (+ u 2)))))
          (do (relocate! to from)
              (if (> to from) (relocate! (- to 1) (+ to 1)) (relocate! (+ to 1) (- to 2)))))
        (aset state side-slot us)
        (aset state ply-slot h)
        (aset state castling-slot (aget undo (+ u 3)))
        (aset state ep-slot (aget undo (+ u 4)))
        (aset state halfmove-slot (aget undo (+ u 5)))
        (aset state hash-lo-slot (aget undo (+ u 6)))
        (aset state hash-hi-slot (aget undo (+ u 7)))
        (aset state mg-slot (aget undo (+ u 8)))
        (aset state eg-slot (aget undo (+ u 9)))
        (aset state phase-slot (aget undo (+ u 10)))
        (when-not (zero? us) (aset state fullmove-slot (dec (aget state fullmove-slot))))))
    
    (defn make-null!
      "Pass: let the other side move, for the null-move search. The halfmove clock restarts, so
      the repetition test never looks back across a pass."
      []
      (let [u (* (aget state ply-slot) undo-stride)]
        (aset undo u 0)
        (aset undo (+ u 4) (aget state ep-slot))
        (aset undo (+ u 5) (aget state halfmove-slot))
        (aset undo (+ u 6) (aget state hash-lo-slot))
        (aset undo (+ u 7) (aget state hash-hi-slot))
        (set-ep! -1)
        (toggle! side-key)
        (aset state halfmove-slot 0)
        (aset state side-slot (bit-xor (aget state side-slot) 1))
        (aset state ply-slot (inc (aget state ply-slot)))))
    
    (defn unmake-null! []
      (let [h (dec (aget state ply-slot))
            u (* h undo-stride)]
        (aset state side-slot (bit-xor (aget state side-slot) 1))
        (aset state ply-slot h)
        (aset state ep-slot (aget undo (+ u 4)))
        (aset state halfmove-slot (aget undo (+ u 5)))
        (aset state hash-lo-slot (aget undo (+ u 6)))
        (aset state hash-hi-slot (aget undo (+ u 7)))))
    
    ;; Draws the search can see
    
    (defn ^boolean repetition?
      "Has the position occurred before, since the last capture or pawn move? Positions before
      the search's root count too: the game's history is played onto the board first."
      []
      (let [h (aget state ply-slot)
            lo (aget state hash-lo-slot)
            hi (aget state hash-hi-slot)
            oldest (larger 0 (- h (aget state halfmove-slot)))]
        (loop [i (- h 2)]
          (if (< i oldest)
            false
            (let [u (* i undo-stride)]
              (if (and (== lo (aget undo (+ u 6))) (== hi (aget undo (+ u 7))))
                true
                (recur (- i 2))))))))
    
    (defn ^boolean insufficient-material?
      "Kings alone, or a king and one knight or bishop against a king: no mate is possible."
      []
      (let [w (aget counts 0)
            b (aget counts 1)]
        (or (and (== w 1) (== b 1))
            (and (== (+ w b) 3)
                 (let [sq (if (== w 2)
                            (if (== (aget lists 0) (aget kings 0)) (aget lists 1) (aget lists 0))
                            (if (== (aget lists 16) (aget kings 1)) (aget lists 17) (aget lists 16)))
                       k (bit-and (aget squares sq) 7)]
                   (or (== k knight) (== k bishop)))))))
    
    ;; Positions from and to FEN
    
    (defn- clear! []
      (.fill squares 0)
      (.fill counts 0)
      (.fill state 0)
      (aset state ep-slot -1))
    
    (def ^:private piece-letters "PNBRQK")
    
    (defn set-position!
      "Set the board to a position in FEN, which must be legal, as chessmate.notation/read-fen
      checks. The history is emptied."
      [fen]
      (let [[placement side castling ep halfmove fullmove] (str/split (str/trim fen) #"\s+")]
        (clear!)
        (loop [cs (seq placement)
               file 0
               rank 7]
          (when-let [c (first cs)]
            (cond (= c "/") (recur (rest cs) 0 (dec rank))
                  (re-matches #"[1-8]" c) (recur (rest cs) (+ file (js/parseInt c)) rank)
                  :else (let [upper (str/upper-case c)
                              piece (+ (inc (str/index-of piece-letters upper)) (if (= c upper) 0 black))]
                          (put! piece (square file rank))
                          (recur (rest cs) (inc file) rank)))))
        (when (or (> (aget counts 0) 16) (> (aget counts 1) 16))
          (throw (js/Error. (str "more than 16 pieces a side: " fen))))
        (when (= side "b")
          (aset state side-slot 1)
          (toggle! side-key))
        (set-castling! (reduce + 0 (map {"K" 1 "Q" 2 "k" 4 "q" 8} (remove #{"-"} (seq castling)))))
        (let [sq (if (or (nil? ep) (= ep "-")) -1 (parse-square ep))]
          (when (and (not (== sq -1)) (ep-capturable? sq (aget state side-slot)))
            (set-ep! sq)))
        (aset state halfmove-slot (if halfmove (js/parseInt halfmove) 0))
        (aset state fullmove-slot (if fullmove (js/parseInt fullmove) 1))))
    
    (defn fen
      "The position in FEN. Its en passant square is given only when a pawn could take there."
      []
      (str (str/join "/" (for [rank (range 7 -1 -1)]
                           (->> (range 8)
                                (map #(let [p (aget squares (square % rank))]
                                        (if (zero? p)
                                          "1"
                                          (let [c (nth piece-letters (dec (bit-and p 7)))] (if (zero? (bit-and p black)) c (str/lower-case c))))))
                                (partition-by #(= "1" %))
                                (map #(if (= "1" (first %)) (str (count %)) (apply str %)))
                                (apply str))))
           " " (if (zero? (aget state side-slot)) "w" "b")
           " " (let [c (aget state castling-slot)]
                 (if (zero? c) "-" (apply str (keep (fn [[bit letter]] (when-not (zero? (bit-and c bit)) letter)) [[1 "K"] [2 "Q"] [4 "k"] [8 "q"]]))))
           " " (if (== -1 (aget state ep-slot)) "-" (square-name (aget state ep-slot)))
           " " (aget state halfmove-slot)
           " " (aget state fullmove-slot)))
    
    (def ^:private scratch (js/Int32Array. 256))
    
    (defn ^boolean has-legal-move?
      "Has the side to move a legal move? Without one, it is mated or stalemated."
      []
      (let [end (generate! scratch 0 false)]
        (loop [i 0]
          (if (< i end)
            (do (make! (aget scratch i))
                (let [ok (legal?)]
                  (unmake!)
                  (if ok true (recur (inc i)))))
            false))))
    
    (defn legal-moves
      "The side to move's legal moves, in generation order."
      []
      (let [buffer (js/Int32Array. 256)
            end (generate! buffer 0 false)]
        (loop [i 0
               found []]
          (if (< i end)
            (let [m (aget buffer i)]
              (make! m)
              (let [ok (legal?)]
                (unmake!)
                (recur (inc i) (if ok (conj found m) found))))
            found))))
    
    (defn parse-uci
      "The legal move a string in UCI's coordinates names, such as \"e2e4\" or \"e7e8q\", or nil."
      [s]
      (first (filter #(= s (uci %)) (legal-moves))))
    
    (defn play-uci!
      "Play a legal move given in UCI, or throw if it is not one."
      [s]
      (if-let [m (parse-uci s)]
        (make! m)
        (throw (js/Error. (str "not a legal move here: " s)))))
    
    (defn- perft-from [buffer depth start]
      (let [end (generate! buffer start false)]
        (loop [i start
               nodes 0]
          (if (< i end)
            (let [m (aget buffer i)]
              (make! m)
              (let [nodes (if (legal?) (+ nodes (if (== depth 1) 1 (perft-from buffer (dec depth) end))) nodes)]
                (unmake!)
                (recur (inc i) nodes)))
            nodes))))
    
    (defn perft
      "How many move sequences of depth moves the position has: the move generator's test."
      [depth]
      (if (zero? depth) 1 (perft-from (js/Int32Array. (* 256 depth)) depth 0)))
    
    ;; Static exchange evaluation
    
    (def see-values "Values for exchanges, by piece." (js/Int32Array. #js [0 100 320 330 500 900 20000 0 0 100 320 330 500 900 20000 0]))
    
    (def ^:private see-gain (js/Int32Array. 40))
    (def ^:private see-removed (js/Int32Array. 40))
    (def ^:private see-pieces (js/Int32Array. 40))
    
    (defn- least-attacker
      "The square of side's least valuable piece that attacks sq, on the board as it now stands,
      or -1."
      [sq side]
      (let [base (* side 16)
            n (aget counts side)]
        (loop [i 0
               best -1
               best-value 100000]
          (if (< i n)
            (let [from (aget lists (+ base i))
                  piece (aget squares from)]
              (if (and (not (zero? piece)) (< (aget see-values piece) best-value) (attacks? from piece sq))
                (recur (inc i) from (aget see-values piece))
                (recur (inc i) best best-value)))
            best))))
    
    (defn see
      "The static exchange evaluation of move m, in centipawns: what the mover wins on m's
      square if both sides then take there in turn, each with its least valuable piece, and each
      stops when going on would cost it."
      [m]
      (let [from (bit-and m 127)
            to (bit-and (bit-shift-right m 7) 127)
            piece (aget squares from)
            target (aget squares to)]
        (aset see-gain 0 (if (zero? (bit-and m en-passant-flag)) (aget see-values target) 100))
        (aset see-removed 0 from)
        (aset see-pieces 0 piece)
        (aset squares from 0)
        (loop [d 0
               side (bit-xor (bit-shift-right piece 3) 1)
               on (aget see-values piece)]
          (let [a (least-attacker to side)]
            (if (== a -1)
              (do (loop [i d]
                    (when (>= i 0)
                      (aset squares (aget see-removed i) (aget see-pieces i))
                      (recur (dec i))))
                  (loop [i d]
                    (if (pos? i)
                      (do (aset see-gain (dec i) (- (larger (- (aget see-gain (dec i))) (aget see-gain i))))
                          (recur (dec i)))
                      (aget see-gain 0))))
              (let [d (inc d)]
                (aset see-gain d (- on (aget see-gain (dec d))))
                (aset see-removed d a)
                (aset see-pieces d (aget squares a))
                (aset squares a 0)
                (recur d (bit-xor side 1) (aget see-values (aget see-pieces d)))))))))
    chessmate.v2.eval · 334 lines
    (ns chessmate.v2.eval
      "Chessmate 2026's evaluation: what a position is worth to the side to move, in centipawns.
    
      Its base is PeSTO (chessmate.v2.pesto): each piece's value and square, blended between a
      middlegame and an endgame table by how much material is left, which the board keeps up to
      date move by move. On top of that it looks at what the tables cannot see:
    
        mobility       the squares each piece reaches that no enemy pawn guards
        king safety    enemy pieces bearing on the squares around each king, and the pawns before it
        pawns          passed pawns, most of all in the endgame, and isolated and doubled ones
        pieces         the bishop pair, and rooks on open and half-open files
        endgames       a lone king driven to the edge, and to the right corner against bishop and
                       knight; drawish material scaled towards a draw
        tempo          a little for the side to move
    
      Every term is a middlegame and an endgame value, blended as the tables are."
      (:require
       [chessmate.v2.board :as board]))
    
    (def ^:private acc
      "The running score while evaluating: middlegame and endgame, White's minus Black's."
      (js/Int32Array. 2))
    
    (defn- add!
      "Add a term, for White when sign is 1 and for Black when it is -1."
      [sign mg eg]
      (aset acc 0 (+ (aget acc 0) (* sign mg)))
      (aset acc 1 (+ (aget acc 1) (* sign eg))))
    
    ;; Pawns, by file: how many each side has, and the most and least advanced, by rank
    
    (def ^:private pawn-count (js/Int32Array. 16)) ; side * 8 + file
    (def ^:private pawn-lowest (js/Int32Array. 16)) ; lowest rank of the side's pawns on the file, 8 for none
    (def ^:private pawn-highest (js/Int32Array. 16)) ; highest rank, -1 for none
    
    (def ^:private pawn-guard
      "Which squares each side's pawns attack, by side * 128 + square: a square is guarded when it
      holds this evaluation's stamp, so nothing is cleared between evaluations."
      (js/Int32Array. 256))
    
    (def ^:private stamp (js/Int32Array. 1))
    
    (defn- scan-pawns!
      "Fill the pawn tables for this position, and mark the squares pawns guard."
      []
      (let [s (inc (aget stamp 0))]
        (aset stamp 0 s)
        (.fill pawn-count 0)
        (.fill pawn-lowest 8)
        (.fill pawn-highest -1)
        (dotimes [side 2]
          (let [base (* side 16)
                n (aget board/counts side)
                forward (if (zero? side) 16 -16)]
            (dotimes [i n]
              (let [sq (aget board/lists (+ base i))]
                (when (== (bit-and (aget board/squares sq) 7) board/pawn)
                  (let [f (+ (* side 8) (board/file-of sq))
                        r (board/rank-of sq)]
                    (aset pawn-count f (inc (aget pawn-count f)))
                    (when (< r (aget pawn-lowest f)) (aset pawn-lowest f r))
                    (when (> r (aget pawn-highest f)) (aset pawn-highest f r))
                    (when (board/on-board? (+ sq forward -1)) (aset pawn-guard (+ (* side 128) sq forward -1) s))
                    (when (board/on-board? (+ sq forward 1)) (aset pawn-guard (+ (* side 128) sq forward 1) s))))))))))
    
    (defn- ^boolean guarded-by-pawn? [side sq] (== (aget pawn-guard (+ (* side 128) sq)) (aget stamp 0)))
    
    ;; Mobility and king attacks
    
    (def ^:private steps (js/Int32Array. #js [33 31 18 14 -14 -18 -31 -33 15 17 -15 -17 1 -1 16 -16]))
    (def ^:private step-start (js/Int32Array. #js [0 0 0 8 12 8 8]))
    (def ^:private step-end (js/Int32Array. #js [0 0 8 12 16 16 16]))
    
    (def ^:private mobility-mg "Per safe square, by type, less a typical count (mobility-centre)." (js/Int32Array. #js [0 0 4 5 2 1 0]))
    (def ^:private mobility-eg (js/Int32Array. #js [0 0 4 5 4 2 0]))
    (def ^:private mobility-centre (js/Int32Array. #js [0 0 4 6 6 12 0]))
    
    (def ^:private attack-weight "How much a piece's attack on the king's zone counts, by type." (js/Int32Array. #js [0 0 2 2 3 5 0]))
    
    (def ^:private king-zone
      "The squares around each side's king that an attacker aims at: the king's and its
      neighbours', and the three two ranks ahead of it. By side * 240 + (square - king) + 119."
      (let [z (js/Int8Array. 480)]
        (doseq [side [0 1]
                df [-1 0 1]
                dr [-1 0 1 (if (zero? side) 2 -2)]]
          (aset z (+ (* side 240) (* dr 16) df 119) 1))
        z))
    
    (def ^:private attack-units (js/Int32Array. 2)) ; by the attacked side
    (def ^:private attackers (js/Int32Array. 2)) ; by the attacked side
    
    (defn- piece-activity!
      "The mobility of the piece on sq, a knight, bishop, rook or queen of side, and its attacks
      on the enemy king's zone."
      [piece sq side sign]
      (let [k (bit-and piece 7)
            them (bit-xor side 1)
            enemy-king (aget board/kings them)
            zone (* them 240)
            slides? (>= k board/bishop)
            start (aget step-start k)
            end (aget step-end k)]
        (loop [i start
               to (+ sq (aget steps start))
               mobility 0
               hits 0]
          (if (< i end)
            (if (board/on-board? to)
              (let [target (aget board/squares to)
                    mobility (if (and (or (zero? target) (not (== (bit-shift-right target 3) side)))
                                      (not (guarded-by-pawn? them to)))
                               (inc mobility)
                               mobility)
                    hits (if (zero? (aget king-zone (+ zone (- to enemy-king) 119))) hits (inc hits))]
                (if (and slides? (zero? target))
                  (recur i (+ to (aget steps i)) mobility hits)
                  (recur (inc i) (if (< (inc i) end) (+ sq (aget steps (inc i))) 0) mobility hits)))
              (recur (inc i) (if (< (inc i) end) (+ sq (aget steps (inc i))) 0) mobility hits))
            (let [m (- mobility (aget mobility-centre k))]
              (add! sign (* m (aget mobility-mg k)) (* m (aget mobility-eg k)))
              (when (pos? hits)
                (aset attack-units them (+ (aget attack-units them) (* hits (aget attack-weight k))))
                (aset attackers them (inc (aget attackers them)))))))))
    
    ;; Pawn structure
    
    (def ^:private passed-mg "By rank from the pawn's own side." (js/Int32Array. #js [0 0 5 10 20 35 60 0]))
    (def ^:private passed-eg (js/Int32Array. #js [0 10 15 25 45 75 120 0]))
    
    (defn- distance [a b]
      (board/larger (js/Math.abs (- (board/file-of a) (board/file-of b))) (js/Math.abs (- (board/rank-of a) (board/rank-of b)))))
    
    (defn- pawn-structure!
      "Passed, isolated and doubled pawns of side."
      [side sign]
      (let [base (* side 16)
            n (aget board/counts side)
            them (bit-xor side 1)
            own (* side 8)
            other (* them 8)]
        (dotimes [f 8]
          (let [c (aget pawn-count (+ own f))]
            (when (> c 1) (add! sign (* -10 (dec c)) (* -20 (dec c))))
            (when (and (pos? c)
                       (or (zero? f) (zero? (aget pawn-count (+ own f -1))))
                       (or (== f 7) (zero? (aget pawn-count (+ own f 1)))))
              (add! sign (* -10 c) (* -12 c)))))
        (dotimes [i n]
          (let [sq (aget board/lists (+ base i))]
            (when (== (bit-and (aget board/squares sq) 7) board/pawn)
              (let [f (board/file-of sq)
                    r (board/rank-of sq)
                    passed? (if (zero? side)
                              (and (< (aget pawn-highest (+ other f)) r)
                                   (or (zero? f) (<= (aget pawn-highest (+ other f -1)) r))
                                   (or (== f 7) (<= (aget pawn-highest (+ other f 1)) r)))
                              (and (> (aget pawn-lowest (+ other f)) r)
                                   (or (zero? f) (>= (aget pawn-lowest (+ other f -1)) r))
                                   (or (== f 7) (>= (aget pawn-lowest (+ other f 1)) r))))]
                (when passed?
                  (let [rel (if (zero? side) r (- 7 r))
                        ahead (+ sq (if (zero? side) 16 -16))
                        blocked? (not (zero? (aget board/squares ahead)))
                        ;; in the endgame, the kings' distances to the square ahead decide races
                        kings (- (* 5 (distance (aget board/kings them) ahead)) (* 2 (distance (aget board/kings side) ahead)))]
                    (add! sign
                          (if blocked? (board/div (aget passed-mg rel) 2) (aget passed-mg rel))
                          (+ (if blocked? (board/div (aget passed-eg rel) 2) (aget passed-eg rel)) (* kings (board/larger 0 (- rel 2)))))))))))))
    
    ;; Kings
    
    (defn- shelter!
      "The pawns before side's king, when it stands on its back ranks away from the centre files:
      a pawn a rank ahead is best, one two ranks ahead is next, and a file with none is a hole."
      [side sign]
      (let [ksq (aget board/kings side)
            kf (board/file-of ksq)
            rel (if (zero? side) (board/rank-of ksq) (- 7 (board/rank-of ksq)))]
        (when (and (<= rel 1) (not (== kf 3)) (not (== kf 4)))
          (let [own (* side 8)
                forward (if (zero? side) 16 -16)
                pawn (+ board/pawn (* side 8))]
            (dotimes [j 3]
              (let [f (+ kf j -1)]
                (when (and (>= f 0) (<= f 7))
                  (let [one (+ ksq forward (- f kf))
                        two (+ one forward)]
                    (cond (== (aget board/squares one) pawn) (add! sign 12 0)
                          (and (board/on-board? two) (== (aget board/squares two) pawn)) (add! sign 6 0)
                          (zero? (aget pawn-count (+ own f))) (add! sign -18 0)
                          :else nil)))))))))
    
    ;; Material, by side * 6: the pieces' worth (knight and bishop 300, rook 500, queen 900), and
    ;; the counts of pawns, minor pieces, bishops, light-squared bishops and queens
    
    (def ^:private worth (js/Int32Array. #js [0 0 300 300 500 900 0]))
    (def ^:private mat (js/Int32Array. 12))
    
    (defn- bump! [i] (aset mat i (inc (aget mat i))))
    
    (defn- count-material! [side]
      (let [base (* side 16)
            n (aget board/counts side)
            m (* side 6)]
        (dotimes [j 6] (aset mat (+ m j) 0))
        (dotimes [i n]
          (let [sq (aget board/lists (+ base i))
                k (bit-and (aget board/squares sq) 7)]
            (aset mat m (+ (aget mat m) (aget worth k)))
            (cond (== k board/pawn) (bump! (+ m 1))
                  (== k board/knight) (bump! (+ m 2))
                  (== k board/bishop) (do (bump! (+ m 2))
                                          (bump! (+ m 3))
                                          (when (== 1 (bit-and (+ (board/file-of sq) (board/rank-of sq)) 1)) (bump! (+ m 4))))
                  (== k board/queen) (bump! (+ m 5))
                  :else nil)))))
    
    (defn- king-danger!
      "The attack on side's king: the enemy pieces bearing on its zone, counted by their weights
      and worth more than in proportion as they gather. One attacker alone is no attack, and
      without a queen an attack seldom mates."
      [side sign]
      (when (>= (aget attackers side) 2)
        (let [units (aget attack-units side)
              ;; units squared over 8: over 4 lost 23 Elo in 800 games of self-play, and over 16 was no better
              danger (board/smaller 600 (board/div (* units units) 8))]
          (add! sign (- (if (pos? (aget mat (+ (* (bit-xor side 1) 6) 5))) danger (board/div danger 2))) 0))))
    
    ;; Endgames
    
    (defn- endgame-scale
      "How much of the endgame score to keep, in sixteenths: none when the side ahead cannot mate
      (a lone minor piece, or two knights, without pawns), little when it has no pawns and at most
      a minor piece more, and half with bishops of opposite colours and pawns."
      [eg]
      (let [s (if (pos? eg) 0 6)
            w (if (pos? eg) 6 0)]
        (cond (and (zero? (aget mat (+ s 1))) (<= (aget mat s) 300)) 0
              (and (zero? (aget mat (+ s 1))) (== (aget mat s) 600) (== (aget mat (+ s 2)) 2) (zero? (aget mat (+ s 3)))) 0
              (and (zero? (aget mat (+ s 1))) (<= (- (aget mat s) (aget mat w)) 300)) 2
              (and (== 300 (aget mat 0)) (== 300 (aget mat 6)) (== 1 (aget mat 3)) (== 1 (aget mat 9))
                   (not (== (aget mat 4) (aget mat 10))))
              8
              :else 16)))
    
    (defn- centre-distance [sq]
      (board/larger (- 3 (board/smaller (board/file-of sq) (- 7 (board/file-of sq)))) (- 3 (board/smaller (board/rank-of sq) (- 7 (board/rank-of sq))))))
    
    (defn- drive
      "Against a lone king, for the strong side: its king to the edge and the strong king to it;
      with bishop and knight, to a corner of the bishop's colour, where the mate is."
      [strong weak]
      (let [wk (aget board/kings weak)
            m (* strong 6)
            f (board/file-of wk)
            r (board/rank-of wk)
            corner (if (and (== (aget mat m) 600) (== 2 (aget mat (+ m 2))) (== 1 (aget mat (+ m 3))) (zero? (aget mat (+ m 1)))) ; a bishop and a knight
                     (* 80 (- 7 (if (== 1 (aget mat (+ m 4)))
                                  (board/smaller (+ f (- 7 r)) (+ (- 7 f) r)) ; light: a8 and h1
                                  (board/smaller (+ f r) (- 14 f r))))) ; dark: a1 and h8
                     0)]
        (+ (* 30 (centre-distance wk)) (* 20 (- 7 (distance wk (aget board/kings strong)))) corner)))
    
    (defn- mop-up
      "The drive against a lone king, with neither pieces nor pawns, from White's point of view,
      when the other side has the material to mate with. Against pawns, stopping them comes first."
      []
      (cond (and (zero? (aget mat 6)) (zero? (aget mat 7)) (>= (aget mat 0) 500)) (drive 0 1)
            (and (zero? (aget mat 0)) (zero? (aget mat 1)) (>= (aget mat 6) 500)) (- (drive 1 0))
            :else 0))
    
    ;; The evaluation
    
    (def ^:private cache
      "Evaluations already made, by the hash's low 16 bits: the high half of the hash, with its
      lowest bit set so that an empty entry never matches, and the score."
      (js/Int32Array. (* 2 65536)))
    
    (defn- evaluate-now []
      (if (board/insufficient-material?)
        0
        (let [phase (board/smaller 24 (aget board/state board/phase-slot))]
          (aset acc 0 (aget board/state board/mg-slot))
          (aset acc 1 (aget board/state board/eg-slot))
          (aset attack-units 0 0)
          (aset attack-units 1 0)
          (aset attackers 0 0)
          (aset attackers 1 0)
          (scan-pawns!)
          (count-material! 0)
          (count-material! 1)
          (dotimes [side 2]
            (let [base (* side 16)
                  sign (if (zero? side) 1 -1)
                  n (aget board/counts side)
                  own (* side 8)]
              (loop [i 0
                     bishops 0]
                (if (< i n)
                  (let [sq (aget board/lists (+ base i))
                        piece (aget board/squares sq)
                        k (bit-and piece 7)]
                    (when (and (> k board/pawn) (< k board/king))
                      (piece-activity! piece sq side sign))
                    (when (== k board/rook)
                      (let [f (board/file-of sq)]
                        (when (zero? (aget pawn-count (+ own f)))
                          (if (zero? (aget pawn-count (+ (* (bit-xor side 1) 8) f))) (add! sign 25 10) (add! sign 10 5)))))
                    (recur (inc i) (if (== k board/bishop) (inc bishops) bishops)))
                  (when (>= bishops 2) (add! sign 30 50))))
              (pawn-structure! side sign)
              (shelter! side sign)))
          (king-danger! 0 1)
          (king-danger! 1 -1)
          (let [mg (aget acc 0)
                eg (+ (aget acc 1) (mop-up))
                eg (board/div (* eg (endgame-scale eg)) 16)
                score (board/div (+ (* mg phase) (* eg (- 24 phase))) 24)
                us (aget board/state board/side-slot)]
            (+ 10 (if (zero? us) score (- score)))))))
    
    (defn evaluate
      "The position's score for the side to move, in centipawns: PeSTO's material and squares, and
      the terms above, blended by the material left."
      []
      (let [i (* 2 (bit-and (aget board/state board/hash-lo-slot) 65535))
            check (bit-or (aget board/state board/hash-hi-slot) 1)]
        (if (== (aget cache i) check)
          (aget cache (inc i))
          (let [score (evaluate-now)]
            (aset cache i check)
            (aset cache (inc i) score)
            score))))
    chessmate.v2.search · 588 lines
    (ns chessmate.v2.search
      "Chessmate 2026's search: negamax, as the 2005 search was, each side maximising the
      negation of the other's score, with alpha-beta cutoffs, and the techniques chess programs
      have found since, each named where it is used:
    
        principal variation search   the first move with the full window, the rest with a null
                                     window, searched again only if they beat it
        transposition table          positions met before, by Zobrist hash, with their scores,
                                     depths and best moves
        iterative deepening          depth 1, 2, 3, ... until the time is spent, each iteration
                                     ordering the next, within an aspiration window
        quiescence search            at the horizon, captures until the position is quiet, so
                                     that no exchange is cut off half way
        move ordering                the table's move, good captures by most valuable victim and
                                     least valuable attacker, killer moves, the countermove, and
                                     the history of quiet moves that caused cutoffs
        pruning and reductions       null move, reverse futility, futility, late move pruning,
                                     static exchange pruning and late move reductions
        extensions                   a check is searched a ply deeper
        draws                        repetition, the fifty-move rule and insufficient material
    
      Scores are in centipawns for the side to move. A mate in n plies is mate - n."
      (:require
       [chessmate.v2.board :as board]
       [chessmate.v2.eval :as eval]))
    
    (def ^:const infinity 32000)
    (def ^:const mate 31000)
    (def ^:const mate-bound 30000) ; any score beyond this is a mate
    (def ^:const max-ply 96)
    (def ^:const max-moves 256)
    
    ;; Bounds, as the transposition table records them
    (def ^:const upper 1) ; the score is at most this: every move failed low
    (def ^:const lower 2) ; at least this: a move failed high
    (def ^:const exact 3)
    
    ;; The search's state, in typed arrays, as the board's is
    (def ^:private moves "Each ply's moves, from ply * max-moves." (js/Int32Array. (* max-ply max-moves)))
    (def ^:private scores "The moves' ordering scores." (js/Int32Array. (* max-ply max-moves)))
    (def ^:private quiets "Each ply's quiet moves tried, from ply * 64, for the history's malus." (js/Int32Array. (* max-ply 64)))
    (def ^:private killers "Two quiet moves per ply that caused cutoffs there." (js/Int32Array. (* max-ply 2)))
    (def ^:private history "Quiet moves' success, by side * 16384 + from * 128 + to." (js/Int32Array. 32768))
    (def ^:private counters "The quiet move that refuted each move, by its piece * 128 + its square." (js/Int32Array. 2048))
    (def ^:private played "The move made at each ply, 0 for a pass." (js/Int32Array. max-ply))
    (def ^:private evals "The static evaluation at each ply, for improving?." (js/Int32Array. max-ply))
    (def ^:private pv "The principal variation from each ply, from ply * max-ply." (js/Int32Array. (* max-ply max-ply)))
    (def ^:private pv-length "Where each ply's variation ends." (js/Int32Array. max-ply))
    
    (def ^:const stopped-slot 0)
    (def ^:const seldepth-slot 1)
    (def ^:const generation-slot 2)
    (def ^:const mask-slot 3)
    (def ^:const contempt-slot 4)
    (def ^:const root-move-slot 5) ; the best root move of the iteration under way, once one is known
    (def ^:const root-score-slot 6) ; and its score
    (def ^:const ply-limit-slot 7) ; the deepest ply any line may reach, captures and checks included
    (def ^:private si (js/Int32Array. 8))
    
    (def ^:const nodes-slot 0)
    (def ^:const hard-slot 1) ; the clock's reading at which the search stops
    (def ^:const max-nodes-slot 2)
    (def ^:private sf (js/Float64Array. 3))
    
    (def ^:private tables "The transposition table, allocated by the first search that needs it." #js [nil])
    (def ^:private clock "The clock, a function of no arguments returning milliseconds." #js [nil])
    
    (def ^:private lmr
      "Late move reductions, by depth * 64 + the move's number: more for later moves and deeper
      searches."
      (let [t (js/Int32Array. 4096)]
        (dotimes [d 64]
          (dotimes [n 64]
            (when (and (pos? d) (pos? n))
              (aset t (+ (* d 64) n) (js/Math.floor (+ 0.75 (/ (* (js/Math.log d) (js/Math.log n)) 2.25)))))))
        t))
    
    ;; The transposition table: four 32-bit numbers per entry, indexed by the hash's low half:
    ;;   the hash's high half, to tell positions apart
    ;;   the best move
    ;;   the score (low 16 bits) and the static evaluation (high 16)
    ;;   the depth + 1 (8 bits), the bound (2 bits) and the search's generation
    
    (defn resize!
      "Give the transposition table 2^bits entries of 16 bytes each: 20 bits is 16 MB."
      [bits]
      (aset tables 0 (js/Int32Array. (* 4 (bit-shift-left 1 bits))))
      (aset si mask-slot (dec (bit-shift-left 1 bits))))
    
    (defn table? "Has the transposition table been allocated?" [] (some? (aget tables 0)))
    
    (defn clear!
      "Forget everything learnt: the table, the history, the killers and the countermoves."
      []
      (when (aget tables 0) (.fill (aget tables 0) 0))
      (.fill history 0)
      (.fill killers 0)
      (.fill counters 0))
    
    (defn- to-table
      "A score for the table: a mate counted from this position, not from the root."
      [score ply]
      (cond (>= score mate-bound) (+ score ply)
            (<= score (- mate-bound)) (- score ply)
            :else score))
    
    (defn- from-table [score ply]
      (cond (>= score mate-bound) (- score ply)
            (<= score (- mate-bound)) (+ score ply)
            :else score))
    
    (defn- store!
      "Record a search's result for the position on the board. A deeper result for another
      position is kept unless it is from an earlier search."
      [depth bound score static m ply]
      (let [tt (aget tables 0)
            hi (aget board/state board/hash-hi-slot)
            e (* 4 (bit-and (aget board/state board/hash-lo-slot) (aget si mask-slot)))
            info (aget tt (+ e 3))
            generation (aget si generation-slot)
            same? (== (aget tt e) hi)]
        (when (or (zero? info)
                  (not (== (bit-shift-right info 10) generation))
                  (== bound exact)
                  (>= (+ depth 3) (dec (bit-and info 255))))
          (aset tt e hi)
          (when (or (not same?) (not (zero? m))) (aset tt (+ e 1) m))
          (aset tt (+ e 2) (bit-or (bit-and (to-table score ply) 0xFFFF) (bit-shift-left static 16)))
          (aset tt (+ e 3) (bit-or (inc depth) (bit-shift-left bound 8) (bit-shift-left generation 10))))))
    
    ;; Counting nodes, and the clock
    
    (defn- count-node!
      "One more node; every 256 a look at the clock and the node limit: a few milliseconds apart
      even in the page's interpreter, which searches forty times slower than the worker."
      []
      (let [n (inc (aget sf nodes-slot))]
        (aset sf nodes-slot n)
        (when (and (zero? (bit-and n 255))
                   (or (>= n (aget sf max-nodes-slot)) (> ((aget clock 0)) (aget sf hard-slot))))
          (aset si stopped-slot 1))))
    
    (defn- ^boolean stopped? [] (not (zero? (aget si stopped-slot))))
    
    (defn- draw-score
      "A draw, as the side to move at ply sees it: the side to move at the root values it at
      -contempt, so that it plays on in level positions."
      [ply]
      (if (zero? (bit-and ply 1)) (- (aget si contempt-slot)) (aget si contempt-slot)))
    
    ;; Move ordering
    
    (defn- counter-move
      "The move that last refuted the move that led to ply, or 0."
      [ply]
      (if (zero? ply)
        0
        (let [prev (aget played (dec ply))]
          (if (zero? prev)
            0
            (let [to (board/move-to prev)]
              (aget counters (+ (* (aget board/squares to) 128) to)))))))
    
    (defn- move-score
      "How promising m looks, for ordering: the table's move first, then captures that do not lose
      material, by most valuable victim and least valuable attacker, then queen promotions, killer
      moves and the countermove, then quiet moves by their history, then captures that lose
      material, and under-promotions last."
      [m tt-move k1 k2 counter us]
      (let [from (bit-and m 127)
            to (bit-and (bit-shift-right m 7) 127)
            victim (aget board/squares to)
            promo (board/promotion m)]
        (cond (== m tt-move) 30000000
              (or (not (zero? victim)) (not (zero? (bit-and m board/en-passant-flag))))
              (let [v (if (zero? victim) board/pawn (bit-and victim 7))
                    a (bit-and (aget board/squares from) 7)
                    order (+ (* v 16) (- 8 a) (if (== promo board/queen) 64 0))]
                (if (or (<= (aget board/see-values a) (aget board/see-values v)) (>= (board/see m) 0))
                  (+ 20000000 order)
                  (+ -1000000 order)))
              (== promo board/queen) 19000000
              (pos? promo) -2000000
              (== m k1) 15000000
              (== m k2) 14000000
              (== m counter) 13000000
              :else (aget history (+ (* us 16384) (* from 128) to)))))
    
    (defn- score-moves! [base end tt-move ply]
      (let [us (board/side)
            k1 (aget killers (* ply 2))
            k2 (aget killers (inc (* ply 2)))
            counter (counter-move ply)]
        (loop [i base]
          (when (< i end)
            (aset scores i (move-score (aget moves i) tt-move k1 k2 counter us))
            (recur (inc i))))))
    
    (defn- score-captures!
      "Order captures by most valuable victim and least valuable attacker, for the quiescence
      search."
      [base end]
      (loop [i base]
        (when (< i end)
          (let [m (aget moves i)
                victim (aget board/squares (board/move-to m))]
            (aset scores i (+ (* 16 (if (zero? victim) board/pawn (bit-and victim 7)))
                              (- 8 (bit-and (aget board/squares (board/move-from m)) 7))
                              (if (== (board/promotion m) board/queen) 64 0))))
          (recur (inc i)))))
    
    (defn- pick!
      "The best-scored move from place i to end, swapped into place i."
      [i end]
      (loop [j (inc i)
             best i]
        (if (< j end)
          (recur (inc j) (if (> (aget scores j) (aget scores best)) j best))
          (let [m (aget moves best)
                s (aget scores best)]
            (aset moves best (aget moves i))
            (aset scores best (aget scores i))
            (aset moves i m)
            (aset scores i s)
            m))))
    
    (defn- reward!
      "A quiet move m caused a cutoff at ply: make it a killer and the countermove, raise its
      history and lower that of the quiet moves tried before it. The history saturates at
      +-16,384."
      [m ply depth n-quiets]
      (let [us (board/side)
            bonus (board/smaller 1600 (* 16 depth depth))
            k (* ply 2)]
        (when-not (== m (aget killers k))
          (aset killers (inc k) (aget killers k))
          (aset killers k m))
        (when (pos? ply)
          (let [prev (aget played (dec ply))]
            (when-not (zero? prev)
              (let [to (board/move-to prev)]
                (aset counters (+ (* (aget board/squares to) 128) to) m)))))
        (let [i (+ (* us 16384) (* (board/move-from m) 128) (board/move-to m))
              h (aget history i)]
          (aset history i (+ h (- bonus (board/div (* h bonus) 16384)))))
        (dotimes [j n-quiets]
          (let [q (aget quiets (+ (* ply 64) j))]
            (when-not (== q m)
              (let [i (+ (* us 16384) (* (board/move-from q) 128) (board/move-to q))
                    h (aget history i)]
                (aset history i (- h bonus (board/div (* h bonus) 16384)))))))))
    
    (defn- new-pv!
      "m is the best move at ply so far: the variation from ply is m, then the child's."
      [m ply]
      (let [row (* ply max-ply)
            child (* (inc ply) max-ply)
            end (aget pv-length (inc ply))]
        (aset pv (+ row ply) m)
        (loop [i (inc ply)]
          (when (< i end)
            (aset pv (+ row i) (aget pv (+ child i)))
            (recur (inc i))))
        (aset pv-length ply (board/larger end (inc ply)))))
    
    ;; The quiescence search
    
    (defn- quiesce
      "The score of the position once the captures are played out: the side to move may stand
      on its static evaluation, or capture. Captures that lose material by the static exchange,
      or that could not lift the score to alpha even if the piece were free, are skipped. In
      check, every move is tried, as standing is no option."
      [alpha beta ply]
      (aset pv-length ply ply)
      (count-node!)
      (when (> ply (aget si seldepth-slot)) (aset si seldepth-slot ply))
      (if (>= ply (aget si ply-limit-slot))
        (eval/evaluate)
        (let [in-check (board/in-check?)
              stand (if in-check (- infinity) (eval/evaluate))]
          (if (>= stand beta)
            stand
            (let [base (* ply max-moves)
                  end (board/generate! moves base (not in-check))]
              (if in-check (score-moves! base end 0 ply) (score-captures! base end))
              (loop [i base
                     alpha (board/larger alpha stand)
                     best stand
                     legal 0]
                (if (>= i end)
                  (if (and in-check (zero? legal)) (+ (- mate) ply) best)
                  (let [m (pick! i end)
                        victim (if (zero? (bit-and m board/en-passant-flag)) (aget board/see-values (aget board/squares (board/move-to m))) 100)]
                    (if (and (not in-check)
                             (not (== (board/promotion m) board/queen))
                             (or (< (+ stand victim 200) alpha)
                                 (and (> (aget board/see-values (aget board/squares (board/move-from m))) victim)
                                      (neg? (board/see m)))))
                      (recur (inc i) alpha best legal)
                      (do (board/make! m)
                          (if (board/legal?)
                            (let [score (- (quiesce (- beta) (- alpha) (inc ply)))]
                              (board/unmake!)
                              (cond (stopped?) 0
                                    (>= score beta) score
                                    (> score best) (do (when (> score alpha) (new-pv! m ply))
                                                       (recur (inc i) (board/larger alpha score) score (inc legal)))
                                    :else (recur (inc i) alpha best (inc legal))))
                            (do (board/unmake!)
                                (recur (inc i) alpha best legal)))))))))))))
    
    ;; The main search
    
    (declare search)
    
    (defn- child
      "Search m's reply, m made: the first move with the full window; later ones with a null
      window, reduced when late and quiet, and searched again at full depth and then with the
      full window only if they beat alpha (principal variation search)."
      [depth alpha beta ply ^boolean pv? legal ^boolean quiet? ^boolean checks? ^boolean improving? ^boolean killer? h]
      (let [new-depth (dec depth)]
        (if (== legal 1)
          (- (search new-depth (- beta) (- alpha) (inc ply) pv? true))
          (let [r (if (and quiet? (>= depth 3))
                    (let [r (aget lmr (+ (* (board/smaller depth 63) 64) (board/smaller legal 63)))
                          r (if pv? (dec r) r)
                          r (if improving? r (inc r))
                          r (if killer? (dec r) r)
                          r (if checks? (dec r) r)
                          r (- r (board/div h 5000))]
                      (board/larger 0 (board/smaller r (- depth 2))))
                    0)
                score (- (search (- new-depth r) (- (inc alpha)) (- alpha) (inc ply) false true))
                score (if (and (> score alpha) (pos? r))
                        (- (search new-depth (- (inc alpha)) (- alpha) (inc ply) false true))
                        score)]
            (if (and pv? (> score alpha) (< score beta))
              (- (search new-depth (- beta) (- alpha) (inc ply) true true))
              score)))))
    
    (defn- null-search
      "The null move: if passing still leaves the side to move above beta after a reduced search,
      a real move would too, and the node fails high without searching one."
      [depth beta ply static]
      (let [r (+ 3 (board/div depth 3) (board/smaller 3 (board/div (- static beta) 200)))]
        (aset played ply 0)
        (board/make-null!)
        (let [score (- (search (- depth 1 r) (- beta) (- 1 beta) (inc ply) false false))]
          (board/unmake-null!)
          score)))
    
    (defn- ^boolean non-pawn-material?
      "Has the side to move a piece besides pawns and its king? Without one, zugzwang is common and
      the null move unsound."
      []
      (let [us (board/side)
            base (* us 16)]
        (loop [i 0]
          (if (< i (aget board/counts us))
            (let [k (bit-and (aget board/squares (aget board/lists (+ base i))) 7)]
              (if (and (> k board/pawn) (< k board/king)) true (recur (inc i))))
            false))))
    
    (defn- root-move!
      "At the root, remember the best move so far and its score: a search stopped part way
      through an iteration may play it."
      [ply m score]
      (when (zero? ply)
        (aset si root-move-slot m)
        (aset si root-score-slot score)))
    
    (defn- moves-loop
      "Try the moves in order, pruning what cannot matter, and return the best score. Stores the
      result in the table."
      [depth alpha beta ply ^boolean pv? ^boolean in-check static ^boolean improving? tt-move]
      (let [base (* ply max-moves)
            end (board/generate! moves base false)
            us (board/side)
            k1 (aget killers (* ply 2))
            k2 (aget killers (inc (* ply 2)))
            original-alpha alpha]
        (score-moves! base end tt-move ply)
        (loop [i base
               alpha alpha
               best (- infinity)
               best-move 0
               legal 0
               n-quiets 0 ; quiet moves seen, pruned or searched, for late-move pruning
               n-tried 0] ; quiet moves searched, which the history's malus may lower
          (if (>= i end)
            (cond (pos? legal) (do (store! depth (if (> alpha original-alpha) exact upper) best static best-move ply)
                                   best)
                  in-check (+ (- mate) ply) ; checkmated
                  :else (draw-score ply)) ; stalemate
            (let [m (pick! i end)
                  to (board/move-to m)
                  capture? (or (not (zero? (aget board/squares to))) (not (zero? (bit-and m board/en-passant-flag))))
                  quiet? (and (not capture?) (zero? (board/promotion m)))
                  prunable? (and (not pv?) (not in-check) (> best (- mate-bound)))]
              (if (and prunable? capture? (<= depth 6) (< (board/see m) (* -100 depth)))
                (recur (inc i) alpha best best-move legal n-quiets n-tried) ; a capture that loses too much
                (do (board/make! m)
                    (if-not (board/legal?)
                      (do (board/unmake!)
                          (recur (inc i) alpha best best-move legal n-quiets n-tried))
                      (let [checks? (board/in-check?)
                            legal (inc legal)]
                        (if (and prunable? quiet? (not checks?)
                                 (or (and (<= depth 8) (>= n-quiets (board/div (+ 3 (* depth depth)) (if improving? 1 2)))) ; late moves
                                     (and (<= depth 6) (<= (+ static 100 (* 100 depth)) alpha)))) ; futility
                          (do (board/unmake!)
                              (recur (inc i) alpha best best-move legal (inc n-quiets) n-tried))
                          (do (aset played ply m)
                              (let [h (if quiet? (aget history (+ (* us 16384) (* (board/move-from m) 128) to)) 0)
                                    score (child depth alpha beta ply pv? legal quiet? checks? improving? (or (== m k1) (== m k2)) h)]
                                (board/unmake!)
                                (when (and quiet? (< n-tried 64)) (aset quiets (+ (* ply 64) n-tried) m))
                                (cond (stopped?) 0
                                      (>= score beta) (do (when quiet? (reward! m ply depth (board/smaller 64 (inc n-tried))))
                                                          (store! depth lower score static m ply)
                                                          (root-move! ply m score)
                                                          score)
                                      (> score alpha) (do (new-pv! m ply)
                                                          (root-move! ply m score)
                                                          (recur (inc i) score score m legal (if quiet? (inc n-quiets) n-quiets) (if quiet? (inc n-tried) n-tried)))
                                      :else (recur (inc i) alpha (board/larger best score) best-move legal
                                                   (if quiet? (inc n-quiets) n-quiets) (if quiet? (inc n-tried) n-tried)))))))))))))))
    
    (defn- search
      "The score of the position on the board for the side to move, searched depth plies deep
      (plus extensions, less reductions), within the window alpha to beta, at ply plies from the
      root. pv? is true on the principal variation, where no pruning is allowed; null-ok? false
      forbids a second pass in a row."
      [depth alpha beta ply ^boolean pv? ^boolean null-ok?]
      (aset pv-length ply ply)
      (cond
        (stopped?) 0 ; the caller discards it
    
        (and (pos? ply) (or (board/repetition?) (board/insufficient-material?)))
        (draw-score ply)
    
        ;; the fifty-move rule, unless the last move mated: as in the game, checkmate comes first
        (and (pos? ply) (>= (aget board/state board/halfmove-slot) 100))
        (if (and (board/in-check?) (not (board/has-legal-move?))) (+ (- mate) ply) (draw-score ply))
    
        (>= ply (aget si ply-limit-slot)) (eval/evaluate) ; as deep as a line may go
    
        :else
        (let [alpha (if (pos? ply) (board/larger alpha (+ (- mate) ply)) alpha) ; mate distance pruning: nothing here beats mating now
              beta (if (pos? ply) (board/smaller beta (- mate ply 1)) beta)
              in-check (board/in-check?)
              depth (if in-check (inc depth) depth)] ; the check extension
          (cond
            (>= alpha beta) alpha
            (<= depth 0) (quiesce alpha beta ply)
            :else
            (let [_ (count-node!)
                  tt (aget tables 0)
                  e (* 4 (bit-and (aget board/state board/hash-lo-slot) (aget si mask-slot)))
                  info (aget tt (+ e 3))
                  hit? (and (== (aget tt e) (aget board/state board/hash-hi-slot)) (not (zero? info)))
                  packed (aget tt (+ e 2))
                  tt-score (from-table (bit-shift-right (bit-shift-left packed 16) 16) ply)
                  tt-bound (bit-and (bit-shift-right info 8) 3)]
              (if (and hit? (not pv?) (>= (dec (bit-and info 255)) depth)
                       (or (== tt-bound exact)
                           (and (== tt-bound lower) (>= tt-score beta))
                           (and (== tt-bound upper) (<= tt-score alpha))))
                tt-score ; the table knows
                (let [static (cond in-check (- infinity)
                                   hit? (bit-shift-right packed 16)
                                   :else (eval/evaluate))
                      improving? (and (not in-check) (>= ply 2) (> static (aget evals (- ply 2))))
                      tt-move (if hit? (aget tt (+ e 1)) 0)]
                  (aset evals ply static)
                  (cond
                    ;; reverse futility: so far above beta that a quiet move would not bring it back
                    (and (not pv?) (not in-check) (<= depth 7) (< (js/Math.abs beta) mate-bound)
                         (>= (- static (* depth (if improving? 60 80))) beta))
                    static
    
                    ;; the null move
                    (and (not pv?) (not in-check) null-ok? (>= depth 3) (>= static beta) (< (js/Math.abs beta) mate-bound)
                         (non-pawn-material?)
                         (let [score (null-search depth beta ply static)]
                           (and (not (stopped?)) (>= score beta))))
                    beta
    
                    :else
                    ;; internal iterative reduction: with no move from the table, search less deep
                    (moves-loop (if (and (>= depth 4) (zero? tt-move)) (dec depth) depth)
                                alpha beta ply pv? in-check static improving? tt-move)))))))))
    
    ;; Iterative deepening
    
    (defn- mate-in
      "Moves to mate for a mate score, negative when the side to move is mated; else nil."
      [score]
      (cond (>= score mate-bound) (board/div (inc (- mate score)) 2)
            (<= score (- mate-bound)) (- (board/div (- mate (- score)) 2))
            :else nil))
    
    (defn- line []
      (mapv #(board/uci (aget pv %)) (range (aget pv-length 0))))
    
    (defn think
      "Chessmate 2026's move for the position on the board, which is unchanged afterwards. Options:
        :ms            the time to aim for, in milliseconds (2,000)
        :max-ms        the time never to exceed (2.5 times :ms)
        :depth         the deepest iteration (64)
        :ply-limit     the deepest ply any line may reach, the captures and checks followed beyond
                       the iteration's depth included (95, the most the search's arrays hold); an
                       iteration goes no deeper than it
        :nodes         the most nodes to search, for tests that must not depend on the clock
        :contempt      what a draw costs the side to move, in centipawns (0)
        :now           the clock, in milliseconds
        :on-iteration  called with each completed iteration's report
      Returns the report of the last completed iteration, or of the iteration under way if it
      had already found a better move: :move in UCI, :score in centipawns for the side to move,
      :mate, :depth, :seldepth, :pv, :nodes, :ms and :nps. With no legal move, :move is nil and
      :mate 0 when the side to move is mated."
      [{:keys [ms max-ms depth ply-limit nodes contempt on-iteration now] :or {ms 2000 depth 64 contempt 0}}]
      (let [now (or now #(.now js/performance))
            limit (if ply-limit (board/larger 1 (board/smaller ply-limit (dec max-ply))) (dec max-ply))
            depth (board/smaller depth limit)
            start (now)
            hard (+ start (or max-ms (* 2.5 ms)))
            legal (board/legal-moves)]
        (if (empty? legal)
          {:move nil :score (if (board/in-check?) (- mate) 0) :mate (when (board/in-check?) 0)
           :depth 0 :seldepth 0 :pv [] :nodes 0 :ms 0 :nps 0}
          (do
            (when-not (aget tables 0) (resize! 20))
            (aset clock 0 now)
            (aset sf nodes-slot 0)
            (aset sf hard-slot hard)
            (aset sf max-nodes-slot (or nodes js/Infinity))
            (aset si stopped-slot 0)
            (aset si contempt-slot contempt)
            (aset si ply-limit-slot limit)
            (aset si generation-slot (bit-and (inc (aget si generation-slot)) 1023))
            (.fill killers 0)
            (dotimes [i 32768] (aset history i (board/div (aget history i) 2)))
            (loop [d 1
                   done nil
                   stable 0]
              (aset si seldepth-slot 0)
              (aset si root-move-slot 0)
              (let [prev (:score done 0)
                    score (loop [delta 25
                                 alpha (if (>= d 5) (board/larger (- infinity) (- prev delta)) (- infinity))
                                 beta (if (>= d 5) (board/smaller infinity (+ prev delta)) infinity)]
                            (let [s (search d alpha beta 0 true false)]
                              (cond (stopped?) s
                                    (<= s alpha) (recur (* 2 delta) (board/larger (- infinity) (- s (* 2 delta))) (board/div (+ alpha beta) 2))
                                    (>= s beta) (recur (* 2 delta) alpha (board/smaller infinity (+ s (* 2 delta))))
                                    :else s)))
                    elapsed (- (now) start)
                    n (aget sf nodes-slot)
                    partial? (stopped?)
                    report (fn [move score]
                             {:move (board/uci move)
                              :score score
                              :mate (mate-in score)
                              :depth (if partial? (dec d) d)
                              :seldepth (aget si seldepth-slot)
                              ;; the root's variation, when it is move's; a stopped iteration may have
                              ;; found move without writing its line
                              :pv (if (and (pos? (aget pv-length 0)) (== (aget pv 0) move)) (line) [(board/uci move)])
                              :nodes n
                              :ms (js/Math.round elapsed)
                              :nps (if (pos? elapsed) (js/Math.round (/ (* 1000 n) elapsed)) 0)})]
                (if partial?
                  ;; stopped: the iteration under way counts only if it found a new best move
                  (let [m (aget si root-move-slot)]
                    (cond (and (nil? done) (zero? m)) (report (first legal) 0)
                          (and (not (zero? m)) (or (nil? done) (not (= (board/uci m) (:move done))))) (report m (aget si root-score-slot))
                          :else (assoc done :nodes n :ms (js/Math.round elapsed))))
                  (let [r (report (aget pv 0) score)
                        stable (if (= (:move r) (:move done)) (inc stable) 0)]
                    (when on-iteration (on-iteration r))
                    (if (or (== 1 (count legal))
                            (>= d depth)
                            (and (mate-in score) (>= d (+ 2 (* 2 (js/Math.abs (mate-in score))))))
                            ;; time: the next iteration takes longer than this one; stop early when the move is settled
                            (> elapsed (* ms (cond (>= stable 4) 0.3 (zero? stable) 0.7 :else 0.5))))
                      r
                      (recur (inc d) r stable))))))))))
    chessmate.v2.worker · 92 lines
    (ns chessmate.v2.worker
      "Chessmate 2026's background search: what its compiled worker does with each message.
      Evaluating this namespace only defines functions; chessmate.v2.worker-main starts it.
    
      Unlike the 2005 search's worker, this one lives on between Chessmate's moves, so its
      transposition table and move history carry what it learnt thinking about one move into
      the next."
      (:require
       [chessmate.v2.board :as board]
       [chessmate.v2.eval :as eval]
       [chessmate.v2.search :as search]
       [clojure.string :as str]))
    
    (defn since-irreversible
      "The game's positions and moves from the last capture or pawn move on, as [fen moves]:
      what the search needs to know which positions have occurred, and no more. fens are the
      game's positions in FEN, the start first, and moves the moves between them in UCI."
      [fens moves]
      (let [halfmove #(js/parseInt (nth (str/split % #" ") 4))
            i (or (last (filter #(zero? (halfmove (nth fens %))) (range (count fens)))) 0)]
        [(nth fens i) (subvec (vec moves) i)]))
    
    (defn request
      "A message for the worker (D41): its type, \"search\", or \"perft\" or \"evaluate\" for the
      tests; its id; the position, as a FEN and the moves played from it in UCI; and more fields
      from extra, a map with string keys."
      [type id fen moves extra]
      (let [msg #js {"type" type
                     "id" id
                     "fen" fen
                     "moves" (to-array moves)}]
        (doseq [[k v] extra] (unchecked-set msg k v))
        msg))
    
    (defn set-position!
      "Set the board to a request's position: its FEN, then its moves."
      [^js msg]
      (board/set-position! (unchecked-get msg "fen"))
      (doseq [m (unchecked-get msg "moves")] (board/play-uci! m)))
    
    (defn- reply
      "An info or best message from a search report: the score for the side to move, in
      centipawns, and the line in UCI."
      [type id {:keys [move score mate depth seldepth pv nodes nps ms]}]
      #js {"type" type
           "id" id
           "move" move
           "score" score
           "mate" mate
           "depth" depth
           "reached" seldepth
           "pv" (to-array pv)
           "nodes" nodes
           "nps" nps
           "ms" ms})
    
    (defn handle
      "Answer one message from the page, passing each reply to post. Messages and replies are
      plain JavaScript objects with string keys, so the compiled worker never loses a field to
      renaming."
      [^js msg post]
      (let [kind (unchecked-get msg "type")
            id (unchecked-get msg "id")]
        (try
          (case kind
            "ping" (post #js {"type" "pong" "id" id})
            "search" (do (post #js {"type" "started" "id" id}) ; at once: the page waits 5 s for this, not for a result
                         (set-position! msg)
                         (post (reply "best" id (search/think {:ms (or (unchecked-get msg "ms") 2000)
                                                               :max-ms (unchecked-get msg "maxMs")
                                                               :depth (or (unchecked-get msg "depth") 64)
                                                               :ply-limit (unchecked-get msg "plyLimit")
                                                               :nodes (unchecked-get msg "nodes")
                                                               :contempt (or (unchecked-get msg "contempt") 0)
                                                               :on-iteration #(post (reply "info" id %))}))))
            "evaluate" (do (set-position! msg)
                           (post #js {"type" "evaluate" "id" id "score" (eval/evaluate)}))
            "perft" (let [_ (set-position! msg)
                          start (.now js/performance)
                          nodes (board/perft (unchecked-get msg "depth"))]
                      (post #js {"type" "perft" "id" id "nodes" nodes "ms" (- (.now js/performance) start)}))
            "clear" (do (search/clear!) (post #js {"type" "cleared" "id" id}))
            (post #js {"type" "error" "id" id "message" (str "unknown message type " (pr-str kind))}))
          (catch :default e
            (post #js {"type" "error" "id" id "message" (ex-message e)})))))
    
    (defn start!
      "Answer the page's messages. Only the compiled worker calls this, from
      chessmate.v2.worker-main."
      []
      (set! (.-onmessage js/self)
            (fn [^js e] (handle (.-data e) (fn [reply] (.postMessage js/self reply))))))
    chessmate.v2.core · 971 lines
    (ns chessmate.v2.core
      "Play Chessmate 2026 on /chessmate: the game, and the commands the Listener reaches as
      chess/.... The game is the one chessmate.game describes, played by the new engine, which
      searches in a background worker. Chessmate 2005's game, chessmate.core, reached as
      chessmate-2005/... on /chessmate-2005, is a separate copy of this namespace, so that nothing
      here can change it.
    
      Every move, the visitor's and Chessmate's, is a (chess/move! !game ...) form evaluated through
      the Listener, so the transcript is the whole game."
      (:require
       [chessmate.board :as board]
       [chessmate.book :as book]
       [chessmate.game :as game]
       [chessmate.moves :as moves]
       [chessmate.notation :as notation]
       [chessmate.v2.board :as v2]
       [chessmate.v2.eval :as eval]
       [chessmate.v2.search :as search]
       [chessmate.v2.worker :as worker]
       [clojure.string :as str]
       [pt.host :as host]
       [pt.listener :as listener]))
    
    (defonce ^{:doc "The game on the page, as chessmate.game makes it: the REPL's !game, which @!game shows,
      and which every command that changes the game takes first."}
      !game (atom nil))
    (defonce ^:private !engine (atom {:status :idle})) ; what Chessmate is doing, for the status and the live view
    (defonce ^:private !book (atom nil)) ; the opening book, once fetched
    (defonce ^:private !search-id (atom 0)) ; each search's number: a reply to an older one is stale
    (defonce ^:private !replying (atom false)) ; true while Chessmate's own move is evaluated
    (defonce ^:private !mounted (atom false)) ; true while /chessmate shows the game: only then does Chessmate think
    (defonce ^:private !flag-timer (atom nil)) ; ends the game when the side to move's clock runs out
    
    (def ^:private storage-key "pt87.chess2")
    
    (def thinking-times
      "The thinking times the settings offer, in ms: 10 ms to 5 minutes."
      [10 20 25 50 100 250 500 1000 1500 2000 3000 5000 10000 20000 30000 60000 120000 180000 300000])
    
    (def ^:private level-ms
      "The thinking time at each level, 1 to 6, in ms: the level set the time before the time
      itself did, and (chess/level! !game n) and a game saved then still use it."
      {1 500 2 1000 3 2000 4 3000 5 5000 6 10000})
    
    (def default-level 1)
    (def default-think-ms 500)
    
    (defn- think-time
      "How long Chessmate thinks for side in game g, in ms: its own time, or its level's, in a game
      saved when the level set the time."
      [g side]
      (or (game/setting g :think-ms side) (level-ms (game/setting g :level side)) default-think-ms))
    
    (defn- clock-think-ms
      "The longest thinking time the settings offer that clock allows a move at the start of a
      game: a twentieth of its time and the increment, as Chessmate budgets a clock. nil without
      a clock."
      [clock]
      (when-let [{:keys [base inc]} clock]
        (last (filter #(<= % (+ (/ base 20) inc)) thinking-times))))
    
    (defn- capped
      "A thinking time for game g: ms, or what its clock allows a move, if that is less. A
      longer time would never be used, as the clock's budget caps each search."
      [g ms]
      (if-let [cap (clock-think-ms (:clock g))] (min ms cap) ms))
    
    (defn- time-text "A thinking time as the page writes it: 50 ms, 500 ms, 1.5 s, 2 min." [ms]
      (cond (< ms 1000) (str ms " ms")
            (< ms 60000) (str (/ ms 1000) " s")
            (= ms 60000) "60 s"
            :else (str (/ ms 60000) " min")))
    
    (def ^:private contempt
      "What a draw costs Chessmate, in centipawns: enough to play on in a level position, rather
      than repeat moves, against a visitor it expects to outplay."
      15)
    
    (def ^:private fallback-slice-ms 40) ; when the worker cannot run: the page searches this long per task
    (def ^:private fallback-ms 1000) ; for this long in all
    
    (def ^:private glyphs
      "The pieces as Unicode characters, by piece code. U+FE0E asks for the pawn's text form: ♟
      is the only chess character with an emoji form."
      {0 "·" 1 "♙" 2 "♘" 3 "♗" 4 "♖" 5 "♕" 6 "♔" -1 "♟\uFE0E" -2 "♞" -3 "♝" -4 "♜" -5 "♛" -6 "♚"})
    
    (defn- clock-now [] (.now js/Date)) ; the clocks' time, in ms
    
    (defn- side-name [side] (str/capitalize (name side)))
    
    (defn- set-board!
      "Put the game's position on the new engine's board, with the moves since the last capture
      or pawn move, so that repetitions count."
      [g]
      (let [[fen ucis] (worker/since-irreversible (:fens g) (:moves g))]
        (v2/set-position! fen)
        (doseq [u ucis] (v2/play-uci! u))))
    
    (defonce ^:private !verdict (atom [nil 0])) ; the last position evaluated, by FEN, and its score
    
    (defn- white-score
      "The new engine's evaluation of the game's position from White's point of view, in pawns:
      the evaluation alone needs no history. The page redraws while Chessmate thinks, so the last
      one is kept."
      [g]
      (let [fen (game/fen g)
            [seen score] @!verdict]
        (if (= fen seen)
          score
          (let [_ (v2/set-position! fen)
                s (eval/evaluate)
                score (/ (if (zero? (v2/side)) s (- s)) 100)]
            (reset! !verdict [fen score])
            score))))
    
    (def ^:private level-margin
      "How far ahead, in pawns, a side must be for the verdict to name it: the evaluation counts
      a tenth of a pawn for the side to move, and a level position should read as level."
      0.25)
    
    (defn- winning
      "The side a score for White, in pawns, favours: :white, :black, or :neither when neither is
      ahead by more than level-margin."
      [score]
      (cond (> score level-margin) :white
            (< score (- level-margin)) :black
            :else :neither))
    
    (defn- chessmates-turn? [g] (and g (nil? (:result g)) (not (game/humans-turn? g))))
    
    (defn- line-sans
      "The moves of a line from position p, given in UCI, as SAN."
      [p ucis]
      (let [q (board/copy p)]
        (reduce (fn [sans uci]
                  (if-let [m (notation/parse-uci q uci)]
                    (let [s (notation/san q m)]
                      (board/make! (:board q) (:state q) m)
                      (conj sans s))
                    (reduced sans)))
                []
                ucis)))
    
    (defn- search-state
      "A search's report, with its moves in UCI and its score in centipawns for the side to move,
      as the status and the live view show it: the score in pawns from White's point of view, the
      line in SAN. The depth is the iteration's, which the maximum depth limits; reached, the
      selective depth, is the deepest line it looked at, following captures and checks, and never
      less than the depth, though a search that the table answers may not have counted it."
      [p {:keys [depth reached score mate pv nodes nps ms]}]
      (let [white? (board/white-to-move? (:state p))
            white-score (/ (if white? score (- score)) 100)]
        {:depth depth
         :reached (max (or reached 0) (or depth 0))
         :score (if mate
                  (str (if (= (pos? mate) white?) "White" "Black") " mates in " (js/Math.abs mate))
                  (str (when-not (neg? white-score) "+") (.toFixed white-score 2)))
         :ahead (if mate (if (= (pos? mate) white?) :white :black) (winning white-score))
         :line (line-sans p pv)
         :pv (vec pv)
         :nodes nodes
         :nps nps
         :ms ms}))
    
    ;; What the page shows
    
    (defn- result-text
      "How game g ended, in words: to the visitor when one side is theirs, and else by colour."
      [{:keys [reason winner flagged]} g]
      (let [you (game/human-side g)
            ;; a function, so that a draw, which has no winner, never names one
            wins #(cond (nil? you) (str (side-name winner) " wins") (= winner you) "You win" :else "Chessmate wins")
            who #(cond (nil? you) (side-name %) (= % you) "you" :else "Chessmate")
            whose #(cond (nil? you) (str (side-name %) "'s") (= % you) "your" :else "Chessmate's")]
        (case reason
          :checkmate (str (wins) " by checkmate")
          :resignation (str (wins) ": " (who (game/other winner)) " resigned")
          :time (str (wins) " on time")
          :time-no-mate (str "Drawn: " (whose flagged) " time ran out, and " (who (game/other flagged)) " cannot checkmate")
          :stalemate "Drawn by stalemate"
          :repetition "Drawn by threefold repetition"
          :fifty-moves "Drawn by the fifty-move rule"
          :insufficient-material "Drawn by insufficient material")))
    
    (defn- status-text [g p {:keys [status]}]
      (let [itself? (not-any? #(= :human (game/player g %)) game/sides) ; Chessmate plays itself
            for-side (when itself? (str " for " (side-name (game/to-move g))))]
        (cond (:result g) (result-text (:result g) g)
              (and (game/waiting? g) (chessmates-turn? g)) "Waiting to start" ; short enough for a phone's line
              (and (:paused? g) (chessmates-turn? g)) "Paused"
              (:paused? g) (str "Paused: " (if (game/human-side g) "your move" (str (side-name (game/to-move g)) " to move")))
              ;; short, so that the line never grows: the opening is in the game's state, the depth under the status
              (= status :book) (str "Chessmate plays" for-side " from the book")
              (#{:thinking :fallback} status) (str "Chessmate is thinking" for-side (when (= status :fallback) ", at reduced strength"))
              (chessmates-turn? g) (str "Chessmate is to move" for-side)
              :else (str (if (game/human-side g) "Your move" (str (side-name (game/to-move g)) " to move"))
                         (when (board/king-attacked? (:board p) (board/white-to-move? (:state p))) ": check")))))
    
    (defn- live-state
      "The game as the live view shows it, in one map, its keys always in one order, so that no
      line moves but the ones that change: who plays each side and how, the clock, the side to
      move and the moves so far, the opening, what Chessmate is doing, the side the new engine's
      evaluation favours, and Chessmate's last search."
      [g {:keys [status depth reached score line nodes nps ms]}]
      (->> [[:white (game/player g :white)]
            [:black (game/player g :black)]
            [:thinks (let [w (time-text (think-time g :white)) b (time-text (think-time g :black))] (if (= w b) w {:white w :black b}))]
            (when (some #(game/setting g :max-depth %) game/sides) [:depth-limit (:max-depth g)]) ; the page's Depth
            (when (some #(game/setting g :max-ply %) game/sides) [:max-depth (:max-ply g)]) ; and its Max Depth
            (when (not-every? #(game/book-on? g %) game/sides) [:book (:book g)])
            (when (:clock g) [:clock (game/clock-name (:clock g))])
            [:to-move (game/to-move g)]
            [:moves (count (:moves g))]
            (when (:opening g) [:opening (:opening g)])
            (when (:paused? g) [:paused true])
            (when (:result g) [:result (:score (:result g))])
            (when (not= status :idle) [:status status])
            [:winning (winning (white-score g))]
            (when depth [:depth depth])
            (when depth [:seldepth reached])
            (when depth [:score score])
            (when depth [:line line])
            (when depth [:nodes nodes])
            (when depth [:nps nps])
            (when depth [:time (str (.toFixed (/ ms 1000) 1) " s")])]
           (remove nil?)
           (apply concat)
           (apply array-map))) ; an array map keeps its order, however many keys
    
    (defn- clock-model
      "The clocks, for the board to show and run: each side's time left at now, and the side whose
      clock runs, if one does."
      [g now]
      (when (:clock g)
        {:white (game/time-left g :white now)
         :black (game/time-left g :black now)
         :running (when (and (game/clock-running? g) (:turn-began g)) (name (game/to-move g)))
         :at now
         :control (game/clock-name (:clock g))}))
    
    ;; The figures under the board: who is winning, and the last search's depth, nodes and speed
    
    (defn- signed [x] (let [r (/ (js/Math.round (* x 100)) 100)] (str (when (>= r 0) "+") (.toFixed r 2))))
    
    (defn- verdict
      "Who is winning, in words: the result once the game is over, a mate when one is found, or
      else the side ahead and the score."
      [g ahead score]
      (cond (:result g) (case (:winner (:result g)) :white "White won" :black "Black won" "Drawn")
            (str/includes? score "mates") score
            :else (str (case ahead :white "White is winning" :black "Black is winning" "Level") ", " score)))
    
    (defn- stats
      "Who is winning, by the last search while its score is this position's, or else by the
      evaluation, in pawns; and that search's depth, nodes, speed and time. A search's figures
      are forgotten when the position changes."
      [g {:keys [depth reached score ahead nodes nps ms]}]
      (let [[ahead score] (if depth [ahead score] (let [w (white-score g)] [(winning w) (signed w)]))]
        (cond-> {:verdict (verdict g ahead score)}
          depth (assoc :depth depth :seldepth reached :nodes nodes :nps nps :ms ms))))
    
    (defn- legal-targets
      "The visitor's legal moves as {from [to ...]}, for the board to offer: none on Chessmate's
      turn, and none once the game is over."
      [g p]
      (if (or (:result g) (not (game/humans-turn? g)))
        {}
        (reduce (fn [targets m]
                  (update targets (board/square-name (board/from-sq m)) (fnil conj #{}) (board/square-name (board/to-sq m))))
                {}
                (moves/legal-moves p))))
    
    (defn- refused-targets
      "The visitor's moves that the rules refuse only because they would leave the king in check,
      as {from [to ...]}, so that the board can say why: none on Chessmate's turn, and none once
      the game is over."
      [g {b :board s :state}]
      (if (or (:result g) (not (game/humans-turn? g)))
        {}
        (reduce (fn [targets m]
                  (if (moves/legal? b s m)
                    targets
                    (update targets (board/square-name (board/from-sq m)) (fnil conj #{}) (board/square-name (board/to-sq m)))))
                {}
                (array-seq (moves/possible-moves b s true)))))
    
    (defn- move-rows
      "The moves as numbered rows, [number white black], as the move list shows them: a game set
      up with Black to move starts its first row with no White move."
      [g]
      (let [start (:state (notation/read-fen (:start-fen g)))
            sans (cond->> (:sans g) (not (board/white-to-move? start)) (cons nil))]
        (map-indexed (fn [i [white black]] [(+ i (board/fullmove-number start)) white black]) (partition-all 2 sans))))
    
    (defn- draw-now! []
      (let [g @!game
            e @!engine
            {b :board state :state :as p} (game/position g)
            white? (board/white-to-move? state)
            last-move (peek (:moves g))]
        (.draw (host/chess-host)
               (clj->js {:squares (for [rank (range 7 -1 -1) file (range 8)] (aget b (board/square file rank)))
                         :bottom (name (game/bottom g))
                         :human (some-> (game/human-side g) name) ; the visitor's side, when one side is theirs
                         :players {:white (name (game/player g :white)) :black (name (game/player g :black))}
                         :turn (name (game/to-move g))
                         :clock (clock-model g (clock-now))
                         :last (when last-move [(subs last-move 0 2) (subs last-move 2 4)])
                         :check (when (board/king-attacked? b white?) (board/square-name (board/king-square b white?)))
                         :legal (legal-targets g p)
                         :refused (refused-targets g p)
                         :checked (and (nil? (:result g)) (game/humans-turn? g) (board/king-attacked? b white?))
                         :thinking (contains? #{:book :thinking :fallback} (:status e))
                         :since (:since e)
                         :status (status-text g p e)
                         :over (some? (:result g))
                         ;; how it ended, for the board's sounds and its colour when the visitor is mated
                         :result (when-let [{:keys [reason winner]} (:result g)] {:reason (name reason) :winner (some-> winner name)})
                         :undoable (boolean (or (seq (second (game/undo g))) (:previous g)))
                         :level (:level g)
                         :think-ms {:white (think-time g :white) :black (think-time g :black)}
                         :think-cap (clock-think-ms (:clock g)) ; the most a clock allows a move, or nil
                         :max-depth {:white (game/setting g :max-depth :white) :black (game/setting g :max-depth :black)}
                         :max-ply {:white (game/setting g :max-ply :white) :black (game/setting g :max-ply :black)}
                         :book (:book g)
                         :paused (boolean (:paused? g))
                         ;; a new game, paused until started: start game
                         :start (game/waiting? g)
                         ;; the line Chessmate is weighing, for the board's thinking lines
                         :pv (when (#{:thinking :fallback} (:status e)) (:pv e))
                         ;; the side to move's pieces the other side attacks, for show threats
                         :threats (when (nil? (:result g)) (game/threats g))
                         :stats (stats g e)
                         :rows (move-rows g)
                         :state (listener/pretty (live-state g e) 1000)})))) ; every item: the view scrolls
    
    (defn- draw!
      "Show the game on /chessmate: the board, the status and the live view."
      []
      (when @!mounted
        (draw-now!)))
    
    (defn- announce! [text] (when @!mounted (.announce (host/chess-host) text)))
    
    ;; Storage: the game as chess data, validated when read, and never evaluated
    
    (defn- save! []
      (try
        (if (:result @!game)
          (.removeItem js/localStorage storage-key)
          (.setItem js/localStorage storage-key (game/encode @!game)))
        (catch :default _ nil)))
    
    (defn- unplayable
      "Why a position in FEN cannot be played here, or nil. The rules accept any number of pieces,
      but no game reaches more than 16 a side or 8 pawns a side, and the new engine's board holds
      16 a side."
      [fen]
      (let [placement (first (str/split fen #" "))
            count-of #(count (filter % placement))]
        (cond (> (count-of #(re-matches #"[PNBRQK]" %)) 16) "White has more than 16 pieces"
              (> (count-of #(re-matches #"[pnbrqk]" %)) 16) "Black has more than 16 pieces"
              (> (count-of #{"P"}) 8) "White has more than 8 pawns"
              (> (count-of #{"p"}) 8) "Black has more than 8 pawns"
              :else nil)))
    
    (defn- restore []
      (try (let [g (game/decode (.getItem js/localStorage storage-key))]
             (when (and g (nil? (unplayable (:start-fen g)))) g))
           (catch :default _ nil)))
    
    ;; Chessmate's replies: each a (chess/move! !game ...) form, built by code from a legal move
    
    (defn- move-form [uci]
      (str "(chess/move! !game :" (subs uci 0 2) " :" (subs uci 2 4) (when (= 5 (count uci)) (str " :" (subs uci 4))) ")"))
    
    (defn- reply!
      "Play Chessmate's move, in UCI, found by search id at ply, unless the game has moved on."
      [id ply uci tag]
      (let [g @!game]
        (when (and (= id @!search-id) (= ply (count (:moves g))) (chessmates-turn? g) (string? uci)
                   (notation/parse-uci (game/position g) uci))
          (reset! !replying true)
          (try (host/run! (move-form uci) {:origin "machine" :tag tag})
               (finally (reset! !replying false))))))
    
    (defn- search-tag [{:keys [depth reached score nodes ms]}]
      (str "chessmate 2026 · depth " depth " · sel " reached " · " score " · " (.toLocaleString nodes "en") " nodes · "
           (.toFixed (/ ms 1000) 1) " s"))
    
    (defn- finish-search!
      "Show a search's result, and play its move."
      [id ply p {:keys [move] :as report}]
      (let [shown (search-state p report)]
        (swap! !engine merge shown)
        (reply! id ply move (search-tag shown))))
    
    (defn- think-ms
      "How long Chessmate aims to think, and the most it may, in ms: its level's time, and less
      when its clock demands it: a twentieth of the time it has left and the increment, and never
      more than a quarter of what is left, as scripts/chess/grade.mjs grades it."
      [g]
      (let [level-ms (think-time g (game/to-move g))
            most (min (* 2.5 level-ms) (+ level-ms 30000))] ; minutes of thinking overrun by half a minute at most
        (if-let [left (and (game/clock-running? g) (:turn-began g) (game/time-left g (game/to-move g) (clock-now)))]
          (let [aim (+ (/ left 20) (get-in g [:clock :inc]))
                hard (max 10 (min (/ left 4) (* 2.5 aim)))]
            [(js/Math.round (min level-ms aim (/ hard 2))) (js/Math.round (min most hard))])
          [level-ms (when (> level-ms 12000) most)])))
    
    (defn- fallback!
      "Search on the page, when the worker cannot run: the new engine in this machine's
      interpreter, fallback-slice-ms per task so that the page never freezes, for fallback-ms in
      all. Each slice deepens what the last one learnt, which the transposition table keeps."
      [id ply g]
      (let [p (game/position g)
            start (.now js/performance)]
        (swap! !engine assoc :status :fallback)
        (draw!)
        (when-not (search/table?) (search/resize! 16)) ; 1 MB: the page is no place for the worker's 16
        ((fn next-slice [best nodes]
           (when (= id @!search-id)
             (set-board! g) ; the board may have been used between slices
             (let [r (search/think {:ms fallback-slice-ms :max-ms fallback-slice-ms :contempt contempt :depth (or (game/setting g :max-depth (game/to-move g)) 64)
                                    :ply-limit (game/setting g :max-ply (game/to-move g))})
                   nodes (+ nodes (:nodes r))
                   ms (- (.now js/performance) start)
                   best (-> (if (or (nil? best) (>= (:depth r) (:depth best))) r best)
                            (assoc :reached (:seldepth r) :nodes nodes :ms ms :nps (js/Math.round (/ (* 1000 nodes) ms))))]
               (swap! !engine merge (search-state p best))
               (if (> ms (min fallback-ms (first (think-ms g))))
                 (finish-search! id ply p best)
                 (do (draw!) (js/setTimeout #(next-slice best nodes) 0))))))
         nil 0)))
    
    (defn- search!
      "Search in the background worker, for the side's thinking time, or less on a clock.
      Each completed iteration redraws the status; the best move found is played. If the worker
      cannot run, the page searches itself."
      [id ply g]
      (let [p (game/position g)
            [fen ucis] (worker/since-irreversible (:fens g) (:moves g))
            [ms max-ms] (think-ms g)]
        (swap! !engine assoc :status :thinking)
        (.search2 (host/chess-host) ; the new engine's worker, which lives on between searches
                  (worker/request "search" id fen ucis (cond-> {"ms" ms "maxMs" max-ms "contempt" contempt}
                                                         (game/setting g :max-depth (game/to-move g))
                                                         (assoc "depth" (game/setting g :max-depth (game/to-move g)))
                                                         (game/setting g :max-ply (game/to-move g))
                                                         (assoc "plyLimit" (game/setting g :max-ply (game/to-move g)))))
                  (fn [^js msg]
                    (when (= id @!search-id)
                      (let [{:keys [type] :as report} (js->clj msg :keywordize-keys true)]
                        (case type
                          "info" (do (swap! !engine merge (search-state p report)) (draw!))
                          "best" (finish-search! id ply p report)
                          "error" (fallback! id ply g)
                          nil)))))))
    
    (defn- chessmate-move!
      "Let Chessmate move: from the book when the position is in it, after 250 ms, and otherwise
      by searching."
      []
      (let [g @!game
            id (swap! !search-id inc)
            ply (count (:moves g))
            p (game/position g)
            m (when (game/book-on? g (game/to-move g)) (book/choose @!book p rand))]
        (reset! !engine {:status (if m :book :thinking) :since (.now js/performance)})
        (if m
          (let [after (board/copy p)
                _ (board/make! (:board after) (:state after) m)
                opening (or (book/opening @!book after) (:opening g))] ; the line Chessmate's move enters
            (swap! !engine assoc :opening opening)
            (js/setTimeout #(reply! id ply (board/uci m) (str "chessmate 2026 · book" (when opening (str " · " opening)))) 250))
          (search! id ply g))
        (draw!)))
    
    (defn- stop-thinking!
      "Forget any search in progress, so that its reply is never played, and stop it. An idle
      worker is kept, with what it has learnt."
      []
      (swap! !search-id inc)
      (.stop2 (host/chess-host))
      (reset! !engine {:status :idle}))
    
    ;; After every change
    
    (defn- flag!
      "End the game on time: the side to move's clock has run out."
      []
      (stop-thinking!)
      (let [g (swap! !game #(game/flag % (game/to-move %)))]
        (announce! (result-text (:result g) g))))
    
    (defn- out-of-time? [g now]
      (boolean (and (game/clock-running? g) (:turn-began g) (<= (game/time-left g (game/to-move g) now) 0))))
    
    (declare changed!)
    
    (defn- arm-flag!
      "Set a timer for the moment the side to move's clock runs out, while the game is on the page;
      it ends the game then, unless a move has been made."
      []
      (some-> @!flag-timer js/clearTimeout)
      (reset! !flag-timer nil)
      (let [g @!game]
        (when (and @!mounted (game/clock-running? g) (:turn-began g))
          (let [ply (count (:moves g))]
            (reset! !flag-timer
                    (js/setTimeout (fn []
                                     (let [g @!game]
                                       (when (and (= ply (count (:moves g))) (nil? (:result g)))
                                         (if (out-of-time? g (clock-now))
                                           (do (flag!) (changed!))
                                           (arm-flag!)))))
                                   (+ 20 (max 0 (game/time-left g (game/to-move g) (clock-now))))))))))
    
    (defn- changed!
      "Save and show the game, let Chessmate move if it is its turn and the game is on the page,
      and watch the clock. The last search's figures stay in the live view until the next search."
      []
      (save!)
      (if (and @!mounted (chessmates-turn? @!game) (not (:paused? @!game)))
        (chessmate-move!)
        (do (swap! !engine assoc :status :idle) (draw!)))
      (arm-flag!))
    
    (defn- start-game!
      "Start game g. A new game starts a new worker, and clears the page's own search: what
      either learnt about the last game, with its draws valued for the side Chessmate played
      there, has no place in this one."
      [g]
      (stop-thinking!)
      (.stop (host/chess-host))
      (search/clear!)
      (reset! !game g)
      (announce! (str "New game: " (if-let [you (game/human-side g)]
                                     (str "you play " (name you))
                                     (if (game/humans-turn? g) "two players, White to move" "Chessmate plays itself"))))
      (changed!))
    
    (defn- note [text] (println (str ";; " text)))
    
    ;; The commands, reached as chess/...
    
    (defn- game-over-error [g]
      (ex-info (str "The game is over: " (:score (:result g)) ", " (str/lower-case (result-text (:result g) g)))
               {:pt/hint "(chess/play! :white) or (chess/play! :black) starts a new game"}))
    
    (defn- parse-move
      "The legal move that the arguments of chess/move! name, or an error saying why there is
      none."
      [args]
      (let [p (game/position @!game)
            [a b piece] args
            text (cond (and (= 1 (count args)) (string? a)) (str/trim a)
                       (and (<= 2 (count args) 3) (every? #(or (keyword? %) (string? %)) args))
                       (str/join (map name (remove nil? [a b piece])))
                       :else nil)]
        (cond (nil? text) (throw (ex-info (str (pr-str (apply list 'chess/move! '!game args)) " is not a move")
                                          {:pt/hint "give two squares, as (chess/move! !game :e2 :e4), or a move in a string, as (chess/move! !game \"Nf3\")"}))
              (re-matches #"[a-h][1-8][a-h][1-8][qrbn]?" (str/lower-case text))
              (or (notation/parse-uci p text)
                  (throw (ex-info (str (str/lower-case text) " is not a legal move here")
                                  {:pt/hint (if (and (= 4 (count text)) (notation/parse-uci p (str text "q")))
                                              "a promotion names its piece: add :q, :r, :b or :n"
                                              "(chess/moves) lists the legal moves")})))
              :else (try (notation/parse-san p text)
                         (catch :default e
                           (throw (ex-info (ex-message e) {:pt/hint "(chess/moves) lists the legal moves"})))))))
    
    (defn- this-game
      "The game a command was given, which must be the page's, !game."
      [a cmd]
      (if (identical? a !game)
        a
        (throw (ex-info (str "(chess/" cmd " …) takes the game first, as (chess/" cmd " !game …)")
                        {:pt/hint "!game is the page's game, and @!game shows it"}))))
    
    (defn- value
      "A game, from the game itself, its atom, or nothing, the page's."
      ([] @!game)
      ([g] (if (map? g) g @g)))
    
    (defn move!
      "Play a move for the side to move, and return it with its number, as \"1. e4\" or \"1...
      e5\", and the result if it ends the game. Name it by its squares, as (chess/move! !game :e2
      :e4), with the piece for a promotion, as (chess/move! !game :e7 :e8 :n); or in a string, in
      coordinates, as (chess/move! !game \"e2e4\"), or in SAN, as (chess/move! !game \"Nf3\") or
      (chess/move! !game \"O-O\"). A move made for Chessmate stops its thinking."
      [a & args]
      (this-game a "move!")
      (let [g @!game
            now (clock-now)]
        (when (:result g) (throw (game-over-error g)))
        (when (out-of-time? g now) ; the mover's time ran out before the flag's timer said so
          (flag!)
          (changed!)
          (throw (game-over-error @!game)))
        (let [m (parse-move args)
              mover (game/to-move g)
              by-chessmate? @!replying]
          (when-not by-chessmate? (stop-thinking!))
          (when (:paused? g) ; a move resumes the game, and the clock runs from now
            (swap! !game #(-> % (assoc :paused? false) (game/start-turn now))))
          (let [g (swap! !game game/play m now)
                p (game/position g)
                g (swap! !game assoc :opening (or (book/opening @!book p) (:opening g)))
                san (peek (:sans g))]
            ;; said in words for a screen reader: \"Chessmate played Bb5, check\", not \"Bb5+\"
            (announce! (str (cond by-chessmate? "Chessmate" (= mover (game/human-side g)) "You" :else (side-name mover))
                            " played " (str/replace san #"[+#]$" "")
                            (cond (:result g) (str ". " (result-text (:result g) g))
                                  (str/ends-with? san "+") ", check")))
            (changed!)
            (game/move-text g)))))
    
    (defn- parse-clock
      "A time control from \"5+3\", minutes and the seconds added a move, or nil from :none."
      [c]
      (if (contains? #{:none "none" nil} c)
        nil
        (let [[_ m s] (when (string? c) (re-matches #"(\d+(?:\.\d+)?)\+(\d+)" c))
              base (when m (js/Math.round (* 60000 (js/parseFloat m))))
              inc (when s (* 1000 (js/parseInt s 10)))]
          (if (and base (<= 1000 base 10800000) (<= 0 inc 60000))
            {:base base :inc inc}
            (throw (ex-info (str "There is no clock " (pr-str c))
                            {:pt/hint "a clock is minutes and the seconds added a move, as \"5+3\", or :none for none"}))))))
    
    (def ^:private players-hint "(chess/players! !game :human :chessmate) gives White to you and Black to Chessmate")
    
    (defn- game-options
      "How a new game is played: side, :white or :black, is the visitor's against Chessmate; or a
      map says who plays :white and :black, :human or :chessmate, and the :clock, as \"5+3\" or
      :none. A clock sets each side's thinking time to what it allows a move. What it leaves out
      is as the game before."
      [arg]
      (let [g @!game
            before {:players (:players g (game/playing :white)) :clock (:clock g) :level (:level g default-level)
                    :think-ms (:think-ms g) :max-depth (:max-depth g) :max-ply (:max-ply g) :book (:book g)}]
        (cond (#{:white :black} arg) (assoc before :players (game/playing arg))
              (map? arg) (let [ps (merge (:players before) (select-keys arg [:white :black]))
                               players {:white (:white ps) :black (:black ps)}
                               clock (when (contains? arg :clock) (parse-clock (:clock arg)))
                               allowed (clock-think-ms clock)]
                           (when-not (every? #{:human :chessmate} (vals players))
                             (throw (ex-info (str "There is no player " (pr-str (first (remove #{:human :chessmate} (vals players)))))
                                             {:pt/hint players-hint})))
                           (cond-> (assoc before :players players)
                             (contains? arg :clock) (assoc :clock clock)
                             allowed (assoc :think-ms {:white allowed :black allowed})))
              :else (throw (ex-info (str "There is no side " (pr-str arg))
                                    {:pt/hint "(chess/play! :white) or (chess/play! :black), or a map, as (chess/play! {:clock \"5+3\"})"})))))
    
    (defn- kept
      "The game to keep as the one before a new game, for undo: g itself if it had a move, else the
      one it kept."
      [g]
      (when g (if (seq (:moves g)) (dissoc g :previous) (:previous g))))
    
    (defn new-game!
      "Start a new game from the standard position, at once: with you playing side, :white or
      :black, against Chessmate; or with a map of who plays :white and :black, :human or
      :chessmate, and the :clock, as \"5+3\" (five minutes each, and three seconds more a move) or
      :none, which sets each side's thinking time to what the clock allows a move; or as the
      last game was played, with each side's settings. The game in progress is
      kept: (chess/undo! !game), with no move to take back, goes back to it. Returns the game,
      !game."
      ([] (new-game! {}))
      ([arg]
       (start-game! (assoc (game/new-game (game-options arg)) :previous (kept @!game)))
       !game))
    
    (def ^{:doc "The same as new-game!, by its first name."} play! new-game!)
    
    (defn players!
      "Say who moves for each side from now on, :human or :chessmate, White's first, in the game
      as it stands: Chessmate can take either side, or both and play itself, and two people can
      play each other on one board. A new game that Chessmate plays against itself waits to be
      started. The board turns to the visitor's side. Returns the players."
      [a white black]
      (this-game a "players!")
      (when-let [bad (first (remove #{:human :chessmate} [white black]))]
        (throw (ex-info (str "There is no player " (pr-str bad)) {:pt/hint players-hint})))
      (let [before @!game
            fresh? (empty? (:moves before))
            g (swap! !game #(cond-> (assoc % :players {:white white :black black})
                              ;; a new game Chessmate plays against itself waits for start game; a human, for no one
                              fresh? (assoc :paused? (and (= :chessmate white) (= :chessmate black)))))
            side (game/to-move g)]
        (announce! (str "White: " (if (= white :human) "human" "Chessmate") ". Black: " (if (= black :human) "human" "Chessmate")))
        (if (and (= (game/player before side) (game/player g side)) (= (:paused? before) (:paused? g)))
          (do (save!) (draw!))
          (do (stop-thinking!) (changed!)))
        (:players g)))
    
    (defn pause!
      "Pause the game: Chessmate stops thinking and the clocks stop, until (chess/resume! !game), or a
      human's move, resumes it. Returns :paused."
      [a]
      (this-game a "pause!")
      (let [g @!game]
        (when (:result g) (throw (game-over-error g)))
        (stop-thinking!)
        (swap! !game #(-> % (game/pause (clock-now)) (assoc :paused? true)))
        (announce! "Paused")
        (changed!)
        :paused))
    
    (defn resume!
      "Go on with a paused game, or start a new one that waits to be started: the clocks run
      again, and Chessmate thinks if it is its move. Returns :playing."
      [a]
      (this-game a "resume!")
      (let [g @!game]
        (when (:result g) (throw (game-over-error g)))
        (swap! !game #(-> % (assoc :paused? false) (game/start-turn (clock-now))))
        (announce! (if (empty? (:moves g)) "Started" "Resumed"))
        (changed!)
        :playing))
    
    (defn undo!
      "Take back the last move a human made, and any moves Chessmate made since, so that it is that
      human's move again; or, with no move to take back, go back to the game before this one.
      Returns the moves taken back, newest first, or :previous-game."
      [a]
      (this-game a "undo!")
      (let [[g taken] (game/undo @!game)
            before (:previous @!game)]
        (cond (seq taken) (do (stop-thinking!)
                              (reset! !game (game/start-turn g (clock-now)))
                              (announce! (str "Took back " (str/join " and " taken)))
                              (changed!)
                              taken)
              before (do (stop-thinking!)
                         (.stop (host/chess-host)) ; its search learnt nothing of the game before
                         (reset! !game (game/start-turn before (clock-now)))
                         (announce! "Back to the game before")
                         (changed!)
                         :previous-game)
              :else (do (note "nothing to take back") nil))))
    
    (defn- rethink!
      "Choose Chessmate's move again if it is choosing side's now, side nil being either: a
      setting that changes takes effect at once, not a move later."
      [side]
      (let [g @!game]
        (when (and @!mounted (chessmates-turn? g) (not (:paused? g))
                   (or (nil? side) (= side (game/to-move g)))
                   (#{:book :thinking :fallback} (:status @!engine)))
          (stop-thinking!)
          (chessmate-move!))))
    
    (defn- set-side!
      "Set settings, a map of settings kept for each side, such as {:think-ms 500}: for side, or
      for both sides when side is nil. Chessmate choosing that side's move starts again, with them."
      [side settings]
      (swap! !game (fn [g] (reduce-kv (fn [g k v]
                                        (reduce #(assoc-in %1 [k %2] v)
                                                (update g k #(if (map? %) % {:white % :black %}))
                                                (if side [side] game/sides)))
                                      g
                                      settings)))
      (save!)
      (draw!)
      (rethink! side))
    
    (defn- side-arg
      "Check the side a setting's command was given."
      [cmd side]
      (when-not (#{:white :black} side)
        (throw (ex-info (str "There is no side " (pr-str side)) {:pt/hint (str "a side, :white or :black, as (chess/" cmd " !game :black …)")}))))
    
    (defn level!
      "Set how long Chessmate thinks about each move from now on, for both sides, or for side,
      :white or :black: level 1 to 6 is 500 ms, or 1, 2, 3, 5 or 10 seconds. The longer it
      thinks, the stronger it plays."
      ([a n] (level! a nil n))
      ([a side n]
       (this-game a "level!")
       (when side (side-arg "level!" side))
       (when-not (and (integer? n) (<= 1 n 6))
         (throw (ex-info (str "There is no level " (pr-str n)) {:pt/hint "levels go from 1 (500 ms) to 6 (10 s), as (chess/level! !game 3)"})))
       (set-side! side {:level n :think-ms (capped @!game (level-ms n))})
       n))
    
    (defn think!
      "Set how long Chessmate thinks about each move from now on, in milliseconds, from 10 to
      300,000: the settings offer 10 ms to 5 minutes. The longer it thinks, the stronger it plays.
      On a clock, it is never more than the clock allows a move. For both sides, or for side,
      :white or :black. Returns the time set."
      ([a ms] (think! a nil ms))
      ([a side ms]
       (this-game a "think!")
       (when side (side-arg "think!" side))
       (when-not (and (integer? ms) (<= 10 ms 300000))
         (throw (ex-info (str "There is no thinking time " (pr-str ms)) {:pt/hint "a time in milliseconds, from 10 to 300,000, as (chess/think! !game 250)"})))
       (let [ms (capped @!game ms)]
         (set-side! side {:think-ms ms})
         ms)))
    
    (defn depth!
      "Limit how many plies deep Chessmate searches, the page's Depth, from 1 to 64, or not, with 0
      or nil, the default. The depth counts the full-width plies: captures and checks are followed
      beyond it, to the selective depth, which (chess/max-depth! …) limits. It still stops when its
      thinking time is up. For both sides, or for side, :white or :black. Returns the limit, or nil
      for none."
      ([a n] (depth! a nil n))
      ([a side n]
       (this-game a "depth!")
       (when side (side-arg "depth!" side))
       (when-not (or (nil? n) (and (integer? n) (<= 0 n 64)))
         (throw (ex-info (str "There is no depth " (pr-str n)) {:pt/hint "a depth from 1 to 64, as (chess/depth! !game 8), or 0 for none"})))
       (let [n (when (and n (pos? n)) n)]
         (set-side! side {:max-depth n})
         n)))
    
    (defn max-depth!
      "Limit how deep any line of Chessmate's search may go, the page's Max Depth: the captures and
      checks it follows beyond its depth included, in plies from the position, from 1 to 95, or
      not, with 0 or nil, the default. Below the depth, it limits the depth too. For both sides, or
      for side, :white or :black. Returns the limit, or nil for none."
      ([a n] (max-depth! a nil n))
      ([a side n]
       (this-game a "max-depth!")
       (when side (side-arg "max-depth!" side))
       (when-not (or (nil? n) (and (integer? n) (<= 0 n 95)))
         (throw (ex-info (str "There is no max depth " (pr-str n)) {:pt/hint "a depth from 1 to 95, as (chess/max-depth! !game 20), or 0 for none"})))
       (let [n (when (and n (pos? n)) n)]
         (set-side! side {:max-ply n})
         n)))
    
    (defn book!
      "Let Chessmate play its first moves from its opening book, as it does unless told not to, or
      not: for both sides, as (chess/book! !game false), or for one, as (chess/book! !game :white
      false), when it plays White. Returns whether it plays from the book, or the sides'."
      ([a on?]
       (book! a :white on?)
       (book! a :black on?)
       on?)
      ([a side on?]
       (this-game a "book!")
       (when-not (#{:white :black} side)
         (throw (ex-info (str "There is no side " (pr-str side)) {:pt/hint "(chess/book! !game :white false) or (chess/book! !game :black true)"})))
       (when-not (boolean? on?)
         (throw (ex-info (str "The book is on or off: true or false, not " (pr-str on?)) {:pt/hint "(chess/book! !game false) turns the book off"})))
       (swap! !game assoc-in [:book side] on?)
       (save!)
       (draw!)
       (rethink! side)
       (:book @!game)))
    
    (defn setup!
      "Start a new game from a position in FEN, with you on the same side. Returns the FEN as
      Chessmate writes it. A FEN that is not a legal position is refused, with the reason."
      [a fen]
      (this-game a "setup!")
      (let [g @!game
            new (try (game/new-game {:start-fen fen :players (:players g) :level (:level g) :think-ms (:think-ms g)
                                     :max-depth (:max-depth g) :max-ply (:max-ply g) :book (:book g) :clock (:clock g)})
                     (catch :default e
                       (throw (ex-info (ex-message e) {:pt/hint "(chess/fen) shows the present position's FEN"}))))]
        (when-let [why (unplayable (game/fen new))]
          (throw (ex-info (str "No game reaches this position: " why) {:pt/hint "(chess/fen) shows the present position's FEN"})))
        (start-game! (assoc new :previous (kept @!game)))
        (game/fen new)))
    
    (defn flip!
      "Turn the board round. Returns the side now at the bottom."
      [a]
      (this-game a "flip!")
      (let [g (swap! !game update :flipped? not)]
        (save!)
        (draw!)
        (game/bottom g)))
    
    (defn resign!
      "Resign the game: yours, against Chessmate, or the side to move's when two play. Returns the
      result."
      [a]
      (this-game a "resign!")
      (let [g @!game]
        (when (:result g) (throw (game-over-error g)))
        (when-not (some #(= :human (game/player g %)) game/sides)
          (throw (ex-info "Chessmate plays both sides, and neither resigns" {:pt/hint players-hint})))
        (stop-thinking!)
        (let [g (swap! !game game/resign (or (game/human-side g) (game/to-move g)))]
          (announce! (result-text (:result g) g))
          (changed!)
          (:score (:result g)))))
    
    (defn board
      "The position as eight strings of pieces, rank 8 first, with · for an empty square: of the
      page's game, or of g, a game or its atom."
      ([] (board !game))
      ([g]
       (let [b (:board (game/position (value g)))]
         (vec (for [rank (range 7 -1 -1)]
                (apply str (for [file (range 8)] (glyphs (aget b (board/square file rank))))))))))
    
    (defn moves
      "The legal moves of the side to move, in SAN, sorted: in the page's game, or in g, a game or
      its atom."
      ([] (moves !game))
      ([g]
       (let [p (game/position (value g))]
         (vec (sort (map #(notation/san p %) (moves/legal-moves p)))))))
    
    (defn fen "The position, in FEN: the page's game's, or g's." ([] (fen !game)) ([g] (game/fen (value g))))
    
    (defn pgn
      "Print the game as PGN."
      ([] (pgn !game))
      ([g]
       (let [d (js/Date.)
             pad #(.padStart (str %) 2 "0")]
         (println (game/pgn (value g) (str (.getFullYear d) "." (pad (inc (.getMonth d))) "." (pad (.getDate d))))))))
    
    (defn evaluate
      "The new engine's verdict on the position, without searching: the side it favours,
      :white, :black or :neither, and its score from White's point of view, in pawns; and the
      side to move: of the page's game, or of g, a game or its atom. The last search's score, when
      Chessmate has searched, is in (chess/game)."
      ([] (evaluate !game))
      ([g]
       (let [g (value g)
             score (white-score g)]
         {:winning (winning score)
          :score score
          :to-move (game/to-move g)})))
    
    (defn game
      "The whole game, with what Chessmate is doing, in one map."
      []
      (merge @!game (dissoc @!engine :opening :since)))
    
    ;; On /chessmate
    
    (defn stop!
      "Leave the game's page: stop any search, so that no reply is played, stop the clock, which
      waits for the visitor's return, and release the board."
      []
      (reset! !mounted false)
      (some-> @!flag-timer js/clearTimeout)
      (reset! !flag-timer nil)
      (stop-thinking!)
      (swap! !game game/pause (clock-now))
      (save!)
      (.stop (host/chess-host)) ; and end the worker, which holds 16 MB
      (.unmount (host/chess-host)))
    
    (defn start!
      "Show the game on /chessmate: the one in memory, or the one browser storage holds, or a new one.
      A game under way arrives paused, as does a new one that Chessmate would open, so that nothing
      plays until the visitor says so; Chessmate moves if it is its turn and the game is not
      paused, once the opening book has loaded or failed to."
      []
      (when-not @!game (reset! !game (or (restore) (game/new-game {:level default-level :think-ms default-think-ms}))))
      (host/on-leave! stop!)
      (reset! !mounted true)
      (swap! !game #(-> % game/arrive (game/start-turn (clock-now))))
      (.mount (host/chess-host))
      (reset! !engine {:status :idle})
      (draw!)
      (arm-flag!)
      (-> (if @!book (js/Promise.resolve nil) (.loadBook (host/chess-host)))
          (.then #(when % (reset! !book (js/JSON.parse %))))
          (.catch (fn [_] nil))
          (.then #(when (and (chessmates-turn? @!game) (not (:paused? @!game))) (chessmate-move!)))))
    chessmate.v2.worker-main · 6 lines
    (ns chessmate.v2.worker-main
      "Chessmate 2026's compiled worker's entry point. Never evaluated on the page."
      (:require
       [chessmate.v2.worker :as worker]))
    
    (worker/start!)
    chessmate.board · 298 lines
    (ns chessmate.board
      "The 2005 board (ChessPosition.java), and the position state 2005 did not keep (D32).
    
      The board is the Java's int[80], as an Int8Array: eight rows of ten squares, White's back
      rank first, the h-file at x = 0, and 7 in the two squares of each row beyond the board's
      edge. A square's index is rank * 10 + (7 - file), counting both from 0, so h1 is 0, e1 is 3,
      a1 is 7 and a8 is 77. Pieces are 1 to 6 for White's pawn, knight, bishop, rook, queen and
      king, and -1 to -6 for Black's.
    
      The state holds what the rules need beyond the pieces: the side to move, the castling
      rights, the en passant square and the two clocks. make! and unmake! change the board and the
      state in place, as ChessPosition.makeMove did.")
    
    ;; The piece codes, as ChessPosition names them
    (def pawn 1)
    (def knight 2)
    (def bishop 3)
    (def rook 4)
    (def queen 5)
    (def king 6)
    (def off-board 7)
    
    (def piece-movement-table
      "pieceMovementTable, verbatim: each piece's steps, ending with 0. A piece's entry in index
      says where its steps start; bishops use the queen's last four."
      (js/Int8Array. #js [0 -1 1 10 -10 0 ; rook
                          -1 1 10 -10 -9 -11 9 11 0 ; queen, bishop and king
                          8 -8 12 -12 19 -19 21 -21 0 ; knight
                          10 20 0]))
    
    (def index
      "index, verbatim: where each piece's steps start in piece-movement-table, by piece code."
      (js/Int8Array. #js [0 12 15 10 1 6 6]))
    
    (def initial-board
      "initialBoard, verbatim: \"the traditional chess board orientation\"."
      (js/Int8Array. #js [4 2 3 6 5 3 2 4 7 7 ; white pieces
                          1 1 1 1 1 1 1 1 7 7 ; white pawns
                          0 0 0 0 0 0 0 0 7 7
                          0 0 0 0 0 0 0 0 7 7
                          0 0 0 0 0 0 0 0 7 7
                          0 0 0 0 0 0 0 0 7 7
                          -1 -1 -1 -1 -1 -1 -1 -1 7 7 ; black pawns
                          -4 -2 -3 -6 -5 -3 -2 -4 7 7])) ; black pieces
    
    ;; Squares
    
    (defn square
      "The index of a square from its file and rank, each counted from 0: (square 4 1) is e2."
      [file rank]
      (+ (* rank 10) (- 7 file)))
    
    (defn file-of [sq] (- 7 (mod sq 10)))
    
    (defn rank-of [sq] (quot sq 10))
    
    (defn on-board?
      "Is sq one of the 64 squares, not an index beyond the array or the board's edge?"
      [sq]
      (and (>= sq 0) (< sq 80) (< (mod sq 10) 8)))
    
    (defn piece-at
      "The piece on sq, 0 for none, or off-board for an index beyond the array."
      [board sq]
      (if (and (>= sq 0) (< sq 80)) (aget board sq) off-board))
    
    (defn square-name
      "A square's name, such as \"e4\" (squareString in ChessMove.java)."
      [sq]
      (str (nth "hgfedcba" (mod sq 10)) (inc (quot sq 10))))
    
    (defn parse-square
      "The index of the square a string or keyword names, such as \"e4\" or :e4, or nil."
      [s]
      (let [s (if (keyword? s) (name s) (str s))]
        (when (re-matches #"[a-h][1-8]" s)
          (square (- (.charCodeAt s 0) 97) (- (.charCodeAt s 1) 49)))))
    
    ;; Moves: from + 128 * to, plus 16384 * the piece a pawn promotes to, when one is named
    
    (defn move
      ([from to] (+ from (* 128 to)))
      ([from to promotion] (+ from (* 128 to) (* 16384 promotion))))
    
    (defn from-sq [m] (bit-and m 127))
    
    (defn to-sq [m] (bit-and (bit-shift-right m 7) 127))
    
    (defn promotion
      "The piece a move names for its promotion, or 0 for none, which promotes to a queen."
      [m]
      (bit-shift-right m 14))
    
    (defn uci
      "A move in UCI's coordinates, such as e2e4, or e7e8q when it names its promotion."
      [m]
      (str (square-name (from-sq m)) (square-name (to-sq m))
           (when (pos? (promotion m)) (nth " pnbrq" (promotion m)))))
    
    ;; Castling
    
    (def white-kingside 1)
    (def white-queenside 2)
    (def black-kingside 4)
    (def black-queenside 8)
    (def all-castling 15)
    
    (defn- castle [right king-from king-to rook rook-to empty safe]
      {:right right
       :white? (= "1" (subs king-from 1))
       :from (parse-square king-from)
       :to (parse-square king-to)
       :rook (parse-square rook)
       :rook-to (parse-square rook-to)
       :empty (mapv parse-square empty)
       :safe (mapv parse-square safe)})
    
    (def castles
      "The four castles: the right each needs, the king's and the rook's moves, the squares
      between them, which must be empty, and the king's path, which must not be attacked."
      [(castle white-kingside "e1" "g1" "h1" "f1" ["f1" "g1"] ["e1" "f1" "g1"])
       (castle white-queenside "e1" "c1" "a1" "d1" ["d1" "c1" "b1"] ["e1" "d1" "c1"])
       (castle black-kingside "e8" "g8" "h8" "f8" ["f8" "g8"] ["e8" "f8" "g8"])
       (castle black-queenside "e8" "c8" "a8" "d8" ["d8" "c8" "b8"] ["e8" "d8" "c8"])])
    
    (defn- rights-lost
      "The castling rights lost by a move from or to sq: a king leaving its square loses both
      castles, and a rook leaving its corner, or captured there, loses its own."
      [sq]
      (case sq
        3 (bit-or white-kingside white-queenside)
        0 white-kingside
        7 white-queenside
        73 (bit-or black-kingside black-queenside)
        70 black-kingside
        77 black-queenside
        0))
    
    (defn- castle-to
      "The castle whose king lands on sq, for a king move of two squares."
      [sq]
      (first (filter #(== sq (:to %)) castles)))
    
    ;; Positions: a board, and its state as an Int32Array of these slots
    
    (def ^:private side-slot 0) ; 1 when White is to move, -1 when Black is
    (def ^:private castling-slot 1) ; the rights still held, as a sum of the four above
    (def ^:private en-passant-slot 2) ; the square a pawn has just passed, or -1
    (def ^:private halfmove-slot 3)
    (def ^:private fullmove-slot 4)
    
    (defn position
      "A position from its board and the state: the side to move, the castling rights, the en
      passant square (-1 for none), and the halfmove clock and fullmove number."
      [board white? castling en-passant halfmove fullmove]
      {:board board
       :state (js/Int32Array. #js [(if white? 1 -1) castling en-passant halfmove fullmove])})
    
    (defn empty-board
      "A board with no pieces on it."
      []
      (let [b (js/Int8Array. 80)]
        (dotimes [sq 80] (when-not (on-board? sq) (aset b sq off-board)))
        b))
    
    (defn initial-position
      "The position at the start of a game."
      []
      (position (js/Int8Array. initial-board) true all-castling -1 0 1))
    
    (defn copy
      "A copy of a position, for make! to change without touching the original."
      [{:keys [board state]}]
      {:board (js/Int8Array. board) :state (js/Int32Array. state)})
    
    (defn white-to-move? [state] (== 1 (aget state side-slot)))
    
    (defn castling [state] (aget state castling-slot))
    
    (defn en-passant [state] (aget state en-passant-slot))
    
    (defn halfmove-clock [state] (aget state halfmove-slot))
    
    (defn fullmove-number [state] (aget state fullmove-slot))
    
    (defn distance
      "How far apart two squares' indexes are: 2 for a castling king's move, 20 for a double step."
      [a b]
      (if (> a b) (- a b) (- b a)))
    
    (defn make!
      "ChessPosition.makeMove, with the rules 2005 lacked: play move m on board and state, in
      place, and return what unmake! needs to take it back. A pawn reaching the last rank becomes
      the piece the move names, or a queen, which is all the search ever plays, as in 2005.
      En passant removes the passed pawn, and castling moves the rook too. The state records the
      castling rights left, the square a pawn's double step passes, and the clocks."
      [board state m]
      (let [from (from-sq m)
            to (to-sq m)
            piece (aget board from)
            side (if (pos? piece) 1 -1)
            kind (* side piece)
            passed-pawn? (and (== kind pawn) (== to (aget state en-passant-slot)))
            captured-at (if passed-pawn? (- to (* side 10)) to)
            captured (aget board captured-at)
            promoted (promotion m)]
        (aset board captured-at 0)
        (aset board from 0)
        (aset board to (if (and (== kind pawn) (== (rank-of to) (if (pos? side) 7 0)))
                         (* side (if (zero? promoted) queen promoted))
                         piece))
        (when (and (== kind king) (== 2 (distance from to)))
          (let [{:keys [rook rook-to]} (castle-to to)]
            (aset board rook-to (aget board rook))
            (aset board rook 0)))
        (let [undo #js [piece captured captured-at (aget state castling-slot)
                        (aget state en-passant-slot) (aget state halfmove-slot)]]
          (aset state castling-slot (bit-and-not (aget state castling-slot) (rights-lost from) (rights-lost to)))
          (aset state en-passant-slot (if (and (== kind pawn) (== 20 (distance from to))) (quot (+ from to) 2) -1))
          (aset state halfmove-slot (if (or (== kind pawn) (not= 0 captured)) 0 (inc (aget state halfmove-slot))))
          (when (neg? side) (aset state fullmove-slot (inc (aget state fullmove-slot))))
          (aset state side-slot (- side))
          undo)))
    
    (defn unmake!
      "Take back move m on board and state, given what make! returned when it played it."
      [board state m undo]
      (let [from (from-sq m)
            to (to-sq m)
            [piece captured captured-at rights passed halfmove] undo]
        (aset board to 0)
        (aset board captured-at captured)
        (aset board from piece)
        (when (and (== king (if (pos? piece) piece (- piece))) (== 2 (distance from to)))
          (let [{:keys [rook rook-to]} (castle-to to)]
            (aset board rook (aget board rook-to))
            (aset board rook-to 0)))
        (aset state castling-slot rights)
        (aset state en-passant-slot passed)
        (aset state halfmove-slot halfmove)
        (when (neg? piece) (aset state fullmove-slot (dec (aget state fullmove-slot))))
        (aset state side-slot (- (aget state side-slot)))))
    
    ;; Attacks
    
    (defn- steps-reach?
      "Does one of piece's steps from sq land on such a piece? For knights and kings."
      [board sq piece]
      (loop [i (aget index (if (neg? piece) (- piece) piece))]
        (let [step (aget piece-movement-table i)]
          (cond (== step 0) false
                (== piece (piece-at board (+ sq step))) true
                :else (recur (inc i))))))
    
    (defn- first-piece
      "The first piece from sq in the direction of step, or off-board at the edge."
      [board sq step]
      (loop [t (+ sq step)]
        (let [p (piece-at board t)]
          (if (== p 0) (recur (+ t step)) p))))
    
    (defn- lines-reach?
      "Along kind's lines from sq (a bishop's or a rook's), is the first piece a or b?"
      [board sq kind a b]
      (loop [i (aget index kind)]
        (let [step (aget piece-movement-table i)]
          (cond (== step 0) false
                (let [p (first-piece board sq step)] (or (== p a) (== p b))) true
                :else (recur (inc i))))))
    
    (defn attacked?
      "Is square sq attacked by White's pieces, or by Black's when by-white? is false? It looks
      outward from sq, as the reference's kingAttacked does: for a pawn's capture, a knight's
      jump or a king's step onto it, and along its diagonals and lines to the first piece."
      [board sq by-white?]
      (let [s (if by-white? 1 -1)]
        (or (== (* s pawn) (piece-at board (- sq (* s 9))))
            (== (* s pawn) (piece-at board (- sq (* s 11))))
            (steps-reach? board sq (* s knight))
            (steps-reach? board sq (* s king))
            (lines-reach? board sq bishop (* s bishop) (* s queen))
            (lines-reach? board sq rook (* s rook) (* s queen)))))
    
    (defn king-square
      "Where White's king stands, or Black's when white? is false, or -1 if it has none."
      [board white?]
      (let [k (if white? king (- king))]
        (loop [sq 0]
          (cond (== sq 80) -1
                (== k (aget board sq)) sq
                :else (recur (inc sq))))))
    
    (defn king-attacked?
      "kingAttacked from the reference: is White's king attacked, or Black's when white? is
      false? A missing king counts as attacked."
      [board white?]
      (let [sq (king-square board white?)]
        (or (neg? sq) (attacked? board sq (not white?)))))
    chessmate.moves · 220 lines
    (ns chessmate.moves
      "Move generation: calcPieceMoves and calcPossibleMoves from Chess.java, in the same order,
      with castling and en passant added (D30). The legality test, the game's move list and
      perft are built on them."
      (:refer-clojure :exclude [divide])
      (:require
       [chessmate.board :as board]))
    
    (def white-control
      "humanControl in 2005: how strongly White's pieces reach each square, as calcPieceMoves
      counts it. possible-moves and control! fill it, and the evaluation reads it."
      (js/Int32Array. 80))
    
    (def black-control
      "computerControl in 2005: the same, for Black's pieces."
      (js/Int32Array. 80))
    
    (def ^:private targets
      "piece_moves: the squares one piece can move to, as piece-moves finds them."
      (js/Int32Array. 32))
    
    (defn- add-target [n sq]
      (aset targets n sq)
      (inc n))
    
    (defn- enemy? [side target]
      (if (pos? side) (neg? target) (and (pos? target) (not= target board/off-board))))
    
    (defn- pawn-capture
      "One of a pawn's captures, onto to: of an enemy piece, which adds 12 to the pawn's control
      of the square, or en passant, when to is the square an enemy pawn has just passed."
      [b side control en-passant to n]
      (cond (enemy? side (board/piece-at b to))
            (do (aset control to (+ 12 (aget control to)))
                (add-target n to))
            ;; FIX (2026-09-25): -1 means no en passant square, and a black pawn on h2 would reach
            ;; index -1 too, beyond the board's edge, with a capture that vanished the pawn
            (and (>= en-passant 0) (== to en-passant)) (add-target n to)
            :else n))
    
    (defn- pawn-moves
      "calcPieceMoves for a pawn: its two captures, then the double step from its first rank,
      then the single step."
      [b sq side control en-passant]
      (let [ahead (+ sq (* side 10))
            two-ahead (+ ahead (* side 10))
            n (pawn-capture b side control en-passant (inc ahead) 0)
            n (pawn-capture b side control en-passant (dec ahead) n)
            n (if (and (== (board/rank-of sq) (if (pos? side) 1 6))
                       (== 0 (board/piece-at b ahead))
                       (== 0 (board/piece-at b two-ahead)))
                (add-target n two-ahead)
                n)]
        (if (== 0 (board/piece-at b ahead)) (add-target n ahead) n)))
    
    (defn- slide
      "calcPieceMoves along one step: onto each square until the edge or a piece of the mover's
      own, which stop it, or an enemy piece, which it may take. Each square it reaches adds 1 to
      its control. A knight or king takes one step only."
      [b sq side kind control step n]
      (loop [to (+ sq step)
             n n]
        (let [target (board/piece-at b to)]
          (if (or (== target board/off-board) (if (pos? side) (pos? target) (neg? target)))
            n
            (let [n (do (aset control to (inc (aget control to)))
                        (add-target n to))]
              (if (or (not= 0 target) (== kind board/knight) (== kind board/king))
                n
                (recur (+ to step) n)))))))
    
    (defn piece-moves
      "calcPieceMoves: the squares the piece on sq can move to, into targets, and how many there
      are. Each also counts towards the piece's side's control. A pawn may take en passant onto
      the square en-passant, unless it is -1. Castling is added by possible-moves."
      [b sq en-passant]
      (let [piece (aget b sq)
            side (if (pos? piece) 1 -1)
            kind (* side piece)
            control (if (pos? side) white-control black-control)]
        (if (== kind board/pawn)
          (pawn-moves b sq side control en-passant)
          (loop [i (aget board/index kind)
                 n 0]
            (let [step (aget board/piece-movement-table i)]
              (if (== step 0) n (recur (inc i) (slide b sq side kind control step n))))))))
    
    (defn control!
      "Both sides' control of every square, by calcPieceMoves for every piece, without keeping
      the moves. The fixed search computes it for each position it evaluates (F1)."
      [b]
      (.fill white-control 0)
      (.fill black-control 0)
      (dotimes [y 8]
        (dotimes [x 8]
          (let [sq (+ (* y 10) x)]
            (when (not= 0 (aget b sq)) (piece-moves b sq -1))))))
    
    (defn- castle-legal?
      "Can the side to move castle so? It must hold the right, the king and rook must stand on
      their squares, the squares between them must be empty, and no square of the king's path
      may be attacked: the legality test after the move cannot see the squares it passes."
      [b state {:keys [right from rook empty safe white?]}]
      (let [side (if white? 1 -1)]
        (and (not= 0 (bit-and right (board/castling state)))
             (== (aget b from) (* side board/king))
             (== (aget b rook) (* side board/rook))
             (every? #(== 0 (aget b %)) empty)
             (not-any? #(board/attacked? b % (not white?)) safe))))
    
    (def ^:private capture-list
      "possibleCaptures in 2005: the moves searched first."
      (js/Int32Array. 256))
    
    (def ^:private quiet-list
      "possibleMoveList in 2005: the rest."
      (js/Int32Array. 256))
    
    (defn- centre? [sq] (or (== sq 33) (== sq 34) (== sq 43) (== sq 44)))
    
    (defn possible-moves
      "calcPossibleMoves: both sides' control, as control! computes it, and the moves of the side
      to move in the order the search tries them. First the capture list: captures, every pawn
      move, and moves to d4, e4, d5 or e5; then the rest. Squares go from White's back rank up,
      h-file first, and each piece's moves keep calcPieceMoves' order. A king's castles follow
      its other moves. A promotion is one move, to a queen. The moves may leave the mover's king
      in check. Under the 2005 rules (complete? false) there is no castling and no en passant."
      [b state complete?]
      (.fill white-control 0)
      (.fill black-control 0)
      (let [white? (board/white-to-move? state)
            side (if white? 1 -1)
            en-passant (if complete? (board/en-passant state) -1)
            n-captures (volatile! 0)
            n-quiet (volatile! 0)]
        (dotimes [y 8]
          (dotimes [x 8]
            (let [sq (+ (* y 10) x)
                  piece (aget b sq)
                  mine? (if white? (pos? piece) (neg? piece))]
              (when (not= 0 piece)
                (let [n (piece-moves b sq (if mine? en-passant -1))]
                  (when mine?
                    (dotimes [j n]
                      (let [to (aget targets j)]
                        (if (or (not= 0 (aget b to)) (== piece (* side board/pawn)) (centre? to))
                          (do (aset capture-list @n-captures (board/move sq to)) (vswap! n-captures inc))
                          (do (aset quiet-list @n-quiet (board/move sq to)) (vswap! n-quiet inc)))))
                    (when (and complete? (== piece (* side board/king)) (not= 0 (board/castling state)))
                      (doseq [c board/castles]
                        (when (and (= white? (:white? c)) (castle-legal? b state c))
                          (aset quiet-list @n-quiet (board/move (:from c) (:to c)))
                          (vswap! n-quiet inc))))))))))
        (let [moves (js/Int32Array. (+ @n-captures @n-quiet))]
          (.set moves (.subarray capture-list 0 @n-captures) 0)
          (.set moves (.subarray quiet-list 0 @n-quiet) @n-captures)
          moves)))
    
    (defn legal?
      "Does move m leave the mover's king safe? It is played, the king probed, and the move taken
      back: the test the fixed search applies to each move (F2)."
      [b state m]
      (let [white? (board/white-to-move? state)
            undo (board/make! b state m)
            safe? (not (board/king-attacked? b white?))]
        (board/unmake! b state m undo)
        safe?))
    
    (defn- promotes? [b m]
      (let [piece (aget b (board/from-sq m))
            rank (board/rank-of (board/to-sq m))]
        (or (and (== piece board/pawn) (== rank 7))
            (and (== piece (- board/pawn)) (== rank 0)))))
    
    (defn legal-moves
      "The legal moves of the side to move, under the complete rules and in the search's order, as
      a vector. A promotion is four moves, to a queen, rook, bishop and knight, as the game offers
      them; the search promotes only to a queen, as 2005 did."
      [{b :board state :state}]
      (let [moves (possible-moves b state true)]
        (loop [i 0
               legal (transient [])]
          (if (== i (alength moves))
            (persistent! legal)
            (let [m (aget moves i)
                  from (board/from-sq m)
                  to (board/to-sq m)]
              (recur (inc i)
                     (cond (not (legal? b state m)) legal
                           (promotes? b m) (reduce #(conj! %1 (board/move from to %2))
                                                   legal
                                                   [board/queen board/rook board/bishop board/knight])
                           :else (conj! legal m))))))))
    
    (defn perft
      "Perft: how many sequences of legal moves, depth plies long, lead from position, counting
      each promotion as four moves. The standard check of a move generator."
      [{b :board state :state :as position} depth]
      (if (zero? depth)
        1
        (let [moves (legal-moves position)]
          (if (== depth 1)
            (count moves)
            (reduce (fn [total m]
                      (let [undo (board/make! b state m)
                            n (perft position (dec depth))]
                        (board/unmake! b state m undo)
                        (+ total n)))
                    0
                    moves)))))
    
    (defn divide
      "Perft for each legal move: a map from the move's UCI name to the count after it."
      [{b :board state :state :as position} depth]
      (into (sorted-map)
            (for [m (legal-moves position)]
              (let [undo (board/make! b state m)
                    n (perft position (dec depth))]
                (board/unmake! b state m undo)
                [(board/uci m) n]))))
    chessmate.notation · 291 lines
    (ns chessmate.notation
      "Standard notation: FEN and its first four fields (EPD), UCI, SAN and PGN (D35)."
      (:require
       [chessmate.board :as board]
       [chessmate.moves :as moves]
       [clojure.string :as str]))
    
    ;; FEN
    
    (def ^:private piece-letters "pnbrqk")
    
    (defn- piece-letter [piece]
      (let [letter (nth piece-letters (dec (if (pos? piece) piece (- piece))))]
        (if (pos? piece) (str/upper-case letter) letter)))
    
    (defn- bad-fen [reason]
      (throw (ex-info (str "bad FEN: " reason) {:reason reason})))
    
    (defn- read-rank
      "One rank of FEN's first field onto board, which is rank number y from 0."
      [board y text]
      (loop [file 0
             [c & more] (seq text)]
        (cond (nil? c) (when (not= file 8) (bad-fen (str "rank " (inc y) " has " file " squares, not 8")))
              (> file 7) (bad-fen (str "rank " (inc y) " has more than 8 squares"))
              (re-matches #"[1-8]" c) (recur (+ file (js/parseInt c)) more)
              (str/index-of piece-letters (str/lower-case c))
              (let [code (inc (str/index-of piece-letters (str/lower-case c)))]
                (aset board (board/square file y) (if (= c (str/lower-case c)) (- code) code))
                (recur (inc file) more))
              :else (bad-fen (str "rank " (inc y) " has " (pr-str c) ", which is not a piece or a count")))))
    
    (defn- read-castling
      "FEN's third field, checked against the pieces: each right needs its king and rook at home."
      [b text]
      (when-not (re-matches #"-|K?Q?k?q?" text) (bad-fen (str "castling " (pr-str text) " is not - or some of KQkq, in that order")))
      (reduce (fn [rights [{:keys [right from rook white?]} letter]]
                (let [side (if white? 1 -1)]
                  (cond (not (str/includes? text letter)) rights
                        (and (== (aget b from) (* side board/king)) (== (aget b rook) (* side board/rook))) (+ rights right)
                        :else (bad-fen (str "castling " letter " needs " (if white? "White's" "Black's") " king on "
                                            (board/square-name from) " and a rook on " (board/square-name rook))))))
              0
              (map vector board/castles "KQkq")))
    
    (defn- read-en-passant
      "FEN's fourth field: -, or the square a pawn of the side not to move has just passed."
      [b white? text]
      (if (= text "-")
        -1
        (let [sq (board/parse-square text)
              side (if white? 1 -1)]
          (when-not sq (bad-fen (str "en passant " (pr-str text) " is not - or a square")))
          (when-not (and (== (board/rank-of sq) (if white? 5 2))
                         (== 0 (aget b sq) (aget b (+ sq (* side 10))))
                         (== (aget b (- sq (* side 10))) (- (* side board/pawn))))
            (bad-fen (str "en passant " text " is not a square a " (if white? "black" "white") " pawn has just passed")))
          sq)))
    
    (defn- read-count [text what least]
      (if (and (re-matches #"\d+" text) (>= (js/parseInt text) least))
        (js/parseInt text)
        (bad-fen (str what " " (pr-str text) " is not a whole number of at least " least))))
    
    (defn- check-kings-and-pawns [b]
      (doseq [[piece colour] [[board/king "white"] [(- board/king) "black"]]]
        (let [n (count (filter #(== piece (aget b %)) (range 80)))]
          (when (not= n 1) (bad-fen (str "it has " n " " colour " kings; a position has one of each")))))
      (doseq [sq (range 80)
              :when (and (#{0 7} (board/rank-of sq)) (#{board/pawn (- board/pawn)} (aget b sq)))]
        (bad-fen (str "a pawn stands on " (board/square-name sq) ", on the first or eighth rank"))))
    
    (defn read-fen
      "The position a FEN describes. A FEN that is not a legal position is rejected with its
      reason: six fields, one king each, no pawn on the first or eighth rank, and the side that
      has just moved not in check."
      [fen]
      (let [fields (str/split (str/trim (str fen)) #"\s+")
            _ (when (not= 6 (count fields)) (bad-fen (str "a FEN has 6 fields, and this has " (count fields))))
            [placement side castling en-passant halfmove fullmove] fields
            ranks (str/split placement #"/")
            b (board/empty-board)]
        (when (not= 8 (count ranks)) (bad-fen (str "the placement has " (count ranks) " ranks, not 8")))
        (doseq [[i text] (map-indexed vector ranks)] (read-rank b (- 7 i) text))
        (check-kings-and-pawns b)
        (when-not (#{"w" "b"} side) (bad-fen (str "the side to move is " (pr-str side) ", not w or b")))
        (let [white? (= side "w")
              position (board/position b white? (read-castling b castling) (read-en-passant b white? en-passant)
                                       (read-count halfmove "the halfmove clock" 0) (read-count fullmove "the move number" 1))]
          (when (board/king-attacked? b (not white?))
            (bad-fen (str (if white? "Black" "White") " is in check with " (if white? "White" "Black") " to move")))
          position)))
    
    (defn en-passant-square
      "The en passant square, if a pawn can legally capture onto it now, or nil."
      [{b :board state :state}]
      (let [sq (board/en-passant state)
            side (if (board/white-to-move? state) 1 -1)]
        (when (not= sq -1)
          (some (fn [from]
                  (when (and (== (board/piece-at b from) (* side board/pawn)) (moves/legal? b state (board/move from sq)))
                    sq))
                [(- sq (* side 9)) (- sq (* side 11))]))))
    
    (defn epd
      "A position's first four FEN fields, its identity for repetition and the book: the pieces,
      the side to move, the castling rights, and the en passant square only when a capture onto it
      is legal."
      [{b :board state :state :as position}]
      (let [rows (for [y (range 7 -1 -1)]
                   (->> (range 8)
                        (map #(aget b (board/square % y)))
                        (partition-by zero?)
                        (map #(if (zero? (first %)) (count %) (apply str (map piece-letter %))))
                        (apply str)))
            rights (board/castling state)
            castling (apply str (for [[right letter] (map vector [board/white-kingside board/white-queenside
                                                                  board/black-kingside board/black-queenside]
                                                       "KQkq")
                                      :when (not= 0 (bit-and rights right))]
                                  letter))]
        (str (str/join "/" rows) " " (if (board/white-to-move? state) "w" "b") " "
             (if (= "" castling) "-" castling) " "
             (if-let [sq (en-passant-square position)] (board/square-name sq) "-"))))
    
    (defn fen
      "A position's FEN, all six fields."
      [{state :state :as position}]
      (str (epd position) " " (board/halfmove-clock state) " " (board/fullmove-number state)))
    
    ;; UCI
    
    (defn parse-uci
      "The legal move a UCI string names in position, such as \"e2e4\" or \"e7e8q\", or nil."
      [position s]
      (let [s (str/lower-case (str s))]
        (first (filter #(= s (board/uci %)) (moves/legal-moves position)))))
    
    ;; SAN
    
    (def ^:private san-letters " PNBRQK")
    
    (defn- kind [piece] (if (neg? piece) (- piece) piece))
    
    (defn- castle? [b m]
      (and (== board/king (kind (aget b (board/from-sq m))))
           (== 2 (board/distance (board/from-sq m) (board/to-sq m)))))
    
    (defn- capture? [b state m]
      (let [to (board/to-sq m)]
        (or (not= 0 (aget b to))
            (and (== board/pawn (kind (aget b (board/from-sq m)))) (== to (board/en-passant state))))))
    
    (defn- promotes? [b m]
      (let [piece (aget b (board/from-sq m))]
        (and (== board/pawn (kind piece)) (== (board/rank-of (board/to-sq m)) (if (pos? piece) 7 0)))))
    
    (defn- promotion-letter [m]
      (nth san-letters (if (zero? (board/promotion m)) board/queen (board/promotion m))))
    
    (defn- disambiguation
      "What SAN adds to tell m from the same kind of piece's other legal moves to its square:
      nothing, the file, the rank, or both, in that order of preference."
      [b m legal]
      (let [from (board/from-sq m)
            rivals (for [o legal
                         :when (and (== (board/to-sq o) (board/to-sq m)) (not= (board/from-sq o) from)
                                    (== (aget b (board/from-sq o)) (aget b from)))]
                     (board/from-sq o))
            name (board/square-name from)]
        (cond (empty? rivals) ""
              (not-any? #(== (board/file-of %) (board/file-of from)) rivals) (subs name 0 1)
              (not-any? #(== (board/rank-of %) (board/rank-of from)) rivals) (subs name 1)
              :else name)))
    
    (defn san
      "A legal move in Standard Algebraic Notation: the piece's letter, the least disambiguation,
      x for a capture, =Q for a promotion, O-O or O-O-O for castling, and + for check or # for
      checkmate. A move that names no promotion promotes to a queen."
      [{b :board state :state :as position} m]
      (let [from (board/from-sq m)
            to (board/to-sq m)
            piece (aget b from)
            body (cond (castle? b m) (if (< to from) "O-O" "O-O-O")
                       (== board/pawn (kind piece)) (str (when (capture? b state m) (str (subs (board/square-name from) 0 1) "x"))
                                                         (board/square-name to)
                                                         (when (promotes? b m) (str "=" (promotion-letter m))))
                       :else (str (nth san-letters (kind piece))
                                  (disambiguation b m (moves/legal-moves position))
                                  (when (capture? b state m) "x")
                                  (board/square-name to)))
            undo (board/make! b state m)
            check? (board/king-attacked? b (board/white-to-move? state))
            mate? (and check? (empty? (moves/legal-moves position)))]
        (board/unmake! b state m undo)
        (str body (cond mate? "#" check? "+" :else ""))))
    
    (defn- bad-san [reason candidates]
      (throw (ex-info reason {:reason reason :candidates candidates})))
    
    (defn- san-matches?
      "Could this legal move be what the parts of a SAN name?"
      [b m {:keys [castle letter file rank to promotion]}]
      (let [from (board/from-sq m)
            piece-kind (kind (aget b from))]
        (if castle
          (and (castle? b m) (= castle (if (< (board/to-sq m) from) :kingside :queenside)))
          (and (not (castle? b m))
               (== to (board/to-sq m))
               (== piece-kind (if letter (str/index-of san-letters letter) board/pawn))
               (or (nil? file) (== file (board/file-of from)))
               (or (nil? rank) (== rank (board/rank-of from)))
               (or letter file (== (board/file-of from) (board/file-of to))) ; a pawn named by its square only moves straight
               (if (promotes? b m)
                 (or (nil? promotion) (== (board/promotion m) (str/index-of san-letters promotion)))
                 (nil? promotion))))))
    
    (defn- san-parts [text]
      (if-let [[_ queenside] (re-matches #"[O0]-[O0](-[O0])?" text)]
        {:castle (if queenside :queenside :kingside)}
        (when-let [[_ letter file rank to promotion] (re-matches #"([NBRQK])?([a-h])?([1-8])?x?([a-h][1-8])=?([NBRQnbrq])?" text)]
          {:letter letter
           :file (when file (- (.charCodeAt file 0) 97))
           :rank (when rank (dec (js/parseInt rank)))
           :to (board/parse-square to)
           :promotion (when promotion (str/upper-case promotion))})))
    
    (defn parse-san
      "The legal move a SAN string names in position. Check and annotation marks are ignored,
      whether right or not; 0-0 and 0-0-0 are castling; and a promotion may omit its =. SAN that
      names no legal move, or more than one, is rejected, with the reason or the candidates."
      [{b :board :as position} text]
      (let [text (str/replace (str/trim (str text)) #"[+#!?]+$" "")
            parts (or (san-parts text) (bad-san (str (pr-str text) " is not SAN, such as e4, Nf3, exd5, O-O or e8=Q") []))
            legal (moves/legal-moves position)
            found (filter #(san-matches? b % parts) legal)]
        (case (count found)
          1 (first found)
          0 (let [letter (or (:letter parts) (when-not (:castle parts) "P"))
                  same (if letter (filter #(== (str/index-of san-letters letter) (kind (aget b (board/from-sq %)))) legal) [])
                  named (get {"P" "pawn" "N" "knight" "B" "bishop" "R" "rook" "Q" "queen" "K" "king"} letter "king")]
              (bad-san (str text " is not a legal move here; "
                            (if (seq same)
                              (str "legal " named " moves: " (str/join ", " (distinct (map #(san position %) same))))
                              (str "there are no legal " named " moves")))
                       []))
          (let [candidates (mapv #(san position %) found)]
            (bad-san (str text " is ambiguous: it could be " (str/join " or " candidates)) candidates)))))
    
    ;; PGN
    
    (def standard-start "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
    
    (defn- tag [k v]
      (str "[" k " \"" (str/replace (str v) #"[\\\"]" #(str "\\" %)) "\"]"))
    
    (defn- wrap
      "Words joined by spaces into lines of at most width characters."
      [words width]
      (->> words
           (reduce (fn [lines word]
                     (let [line (peek lines)]
                       (if (and line (<= (+ (count line) 1 (count word)) width))
                         (conj (pop lines) (str line " " word))
                         (conj lines word))))
                   [])
           (str/join "\n")))
    
    (defn pgn
      "A game in PGN: the seven-tag roster, then TimeControl and Termination when given, and SetUp
      and FEN when it began from any position but the standard one, then its moves in SAN,
      numbered and ending with the result."
      [{:keys [event site date round white black result time-control termination start-fen sans]}]
      (let [start-fen (or start-fen standard-start)
            start (:state (read-fen start-fen))
            white-first? (board/white-to-move? start)
            words (map-indexed (fn [i san]
                                 (let [white? (= white-first? (even? i))
                                       number (+ (board/fullmove-number start) (quot (+ i (if white-first? 0 1)) 2))]
                                   (cond white? (str number ". " san)
                                         (zero? i) (str number "... " san)
                                         :else san)))
                               sans)]
        (str (str/join "\n" (concat [(tag "Event" event) (tag "Site" site) (tag "Date" date) (tag "Round" round)
                                     (tag "White" white) (tag "Black" black) (tag "Result" result)]
                                    (when time-control [(tag "TimeControl" time-control)])
                                    (when termination [(tag "Termination" termination)])
                                    (when (not= start-fen standard-start) [(tag "SetUp" "1") (tag "FEN" start-fen)])))
             "\n\n"
             (wrap (concat words [result]) 80)
             "\n")))
    chessmate.book · 107 lines
    (ns chessmate.book
      "The opening book (D36): every position within the first 12 half-moves of the curated
      lichess-org/chess-openings lines, keyed by its EPD, with the moves the lines play from it,
      each weighted by how many lines play it. Chessmate chooses among them at random, by weight.
    
      The book is plain JSON, as the build writes it and the page fetches it:
        {\"positions\" {epd {\"moves\" {uci weight ...}, \"name\" name, \"eco\" eco}}}
      A position has a name and ECO code only where a whole line ends."
      (:require
       [chessmate.board :as board]
       [chessmate.notation :as notation]
       [clojure.string :as str]))
    
    (def ply-limit
      "The book uses each line's first 12 half-moves."
      12)
    
    (def floor-share
      "Chessmate chooses only moves whose weight is at least this share of the heaviest move's,
      which keeps refuted sidelines out of its own play. They stay in the book, so it still
      answers them."
      0.05)
    
    (defn- sans [pgn]
      (remove #(re-matches #"\d+\.+" %) (str/split (str/trim pgn) #"\s+")))
    
    (defn- play-line
      "The moves of a line, as [epd uci] pairs, up to the ply limit, and the EPD where the whole
      line ends, if it ends within the limit. A move that is not legal throws, naming the line."
      [{:keys [name pgn]} parse]
      (let [position (board/initial-position)
            moves (take ply-limit (sans pgn))
            steps (mapv (fn [i san]
                          (let [epd (notation/epd position)
                                m (try (parse epd position san)
                                       (catch :default e
                                         (throw (ex-info (str "the book's line " (pr-str name) " has an illegal move, "
                                                              (inc (quot i 2)) (if (even? i) ". " "... ") san ": " (ex-message e))
                                                         {:line name :move san}))))]
                            (board/make! (:board position) (:state position) m)
                            [epd (board/uci m)]))
                        (range)
                        moves)]
        {:steps steps
         :end (when (<= (count (sans pgn)) ply-limit) (notation/epd position))}))
    
    (defn build
      "The book from the curated lines, each a map of :eco, :name and :pgn, as JSON-ready data.
      Where several lines end in one position, the first names it."
      [lines]
      (let [cache (atom {})
            parse (fn [epd position san]
                    (or (get @cache [epd san])
                        (let [m (notation/parse-san position san)]
                          (swap! cache assoc [epd san] m)
                          m)))]
        (clj->js
          {"positions"
           (reduce (fn [book {:keys [eco name] :as line}]
                     (let [{:keys [steps end]} (play-line line parse)
                           book (reduce (fn [book [epd uci]] (update-in book [epd "moves" uci] (fnil inc 0))) book steps)]
                       (if (and end (not (get-in book [end "name"])))
                         (update book end assoc "name" name "eco" eco)
                         book)))
                   {}
                   lines)})))
    
    (defn entry
      "The book's entry for position, or nil if it is not in the book."
      [book position]
      (when book (unchecked-get (unchecked-get book "positions") (notation/epd position))))
    
    (defn opening
      "The name of the line that ends in position, with its ECO code, such as \"C25 Vienna Game\", or nil."
      [book position]
      (when-let [e (entry book position)]
        (when-let [name (unchecked-get e "name")]
          (str (unchecked-get e "eco") " " name))))
    
    (defn weights
      "A book entry's moves as [uci weight] pairs, the heaviest first, and by name among equals."
      [e]
      (sort-by (fn [[uci weight]] [(- weight) uci]) (js->clj (unchecked-get e "moves"))))
    
    (defn eligible
      "The moves of a book entry that Chessmate may choose: those weighing at least floor-share of
      the heaviest."
      [e]
      (let [ws (weights e)
            heaviest (second (first ws))]
        (filter (fn [[_ weight]] (>= weight (* floor-share heaviest))) ws)))
    
    (defn choose
      "A move for position from the book, or nil if it is not in the book: one of its eligible
      moves that is legal, chosen at random with probability in proportion to its weight. rand
      returns a number from 0 up to 1, as cljs.core/rand does."
      [book position rand]
      (when-let [e (entry book position)]
        (let [options (keep (fn [[uci weight]]
                              (when-let [m (notation/parse-uci position uci)] [m weight]))
                            (eligible e))
              target (* (rand) (reduce + (map second options)))]
          (loop [[[m weight] & more] options
                 below 0]
            (cond (nil? m) nil
                  (< target (+ below weight)) m
                  :else (recur more (+ below weight)))))))
    chessmate.game · 471 lines
    (ns chessmate.game
      "A game as data: its moves, its result, its PGN, and how it is stored. Pure functions: the
      game's effects are chessmate.core's.
    
      A game is a map:
        :start-fen   where it began
        :fens        the FEN of every position so far, the start first and the current last
        :moves       its moves in UCI, such as \"e2e4\"
        :sans        the same moves in SAN, such as \"e4\"
        :players     who moves for each side, :human or :chessmate, as {:white :human :black :chessmate}
        :level       the 2005 engine's search depth for each side, 1 to 64, or 0 for no limit
        :think-ms    Chessmate 2026's thinking time for each side, in ms; :max-depth the depth its
                     search's iterations may reach, the page's Depth; and :max-ply the deepest ply
                     any line may reach, the captures and checks followed beyond that depth
                     included, the page's Max Depth; each nil for no limit
        :flipped?    whether the board is turned round from the visitor's side
        :book        whether Chessmate plays each side's first moves from its opening book
        Each side's settings are maps by side, {:white … :black …}.
        :paused?     whether the game is paused: no thinking, and the clocks stopped
        :clock       the time control, {:base ms :inc ms}, or nil for none
        :times       each side's time left, {:white ms :black ms}, with a clock
        :turn-began  when the side to move's clock started, in ms, while it runs
        :result      nil while it goes on; then its :score, :reason and, unless a draw, :winner
        :opening     the name of the last book line it reached, or nil"
      (:require
       [chessmate.board :as board]
       [chessmate.moves :as moves]
       [chessmate.notation :as notation]
       [clojure.string :as str]))
    
    (def default-level 4)
    
    (def sides [:white :black])
    
    (defn other [side] (if (= side :white) :black :white))
    
    (defn playing
      "The players when the visitor plays side against Chessmate: White is the visitor's and Black
      Chessmate's unless told otherwise."
      [side]
      {side :human (other side) :chessmate})
    
    (defn- epd [fen] (str/join " " (take 4 (str/split fen #" "))))
    
    (defn- kind [piece] (if (neg? piece) (- piece) piece))
    
    (defn- square-colour [sq] (mod (+ (board/file-of sq) (board/rank-of sq)) 2))
    
    (defn- insufficient-material?
      "Only kings; a king and one bishop or knight against a king; or a king and bishop against a
      king and bishop, the bishops on squares of one colour."
      [b]
      (let [others (for [sq (range 80)
                         :let [piece (aget b sq)]
                         :when (and (board/on-board? sq) (not= 0 piece) (not= board/king (kind piece)))]
                     [sq piece])]
        (case (count others)
          0 true
          1 (contains? #{board/bishop board/knight} (kind (second (first others))))
          2 (let [[[sq1 p1] [sq2 p2]] others]
              (and (== p1 (- p2))
                   (== board/bishop (kind p1))
                   (== (square-colour sq1) (square-colour sq2))))
          false)))
    
    (defn ending
      "How a game with these positions (FENs, the current last) has ended, or nil if it goes on.
      Checkmate comes first, even when the move also completes the fifty-move count."
      [fens]
      (let [{b :board state :state :as p} (notation/read-fen (peek fens))
            white? (board/white-to-move? state)]
        (cond (empty? (moves/legal-moves p))
              (if (board/king-attacked? b white?)
                {:score (if white? "0-1" "1-0") :winner (if white? :black :white) :reason :checkmate}
                {:score "1/2-1/2" :reason :stalemate})
              (insufficient-material? b) {:score "1/2-1/2" :reason :insufficient-material}
              (<= 3 (count (filter #{(epd (peek fens))} (map epd fens)))) {:score "1/2-1/2" :reason :repetition}
              (<= 100 (board/halfmove-clock state)) {:score "1/2-1/2" :reason :fifty-moves}
              :else nil)))
    
    (defn- per-side
      "A setting for each side, from v: a map by side, or one value for both, or nil for dflt."
      [v dflt]
      (let [one #(if (some? %) % dflt)]
        (if (map? v)
          {:white (one (:white v)) :black (one (:black v))}
          {:white (one v) :black (one v)})))
    
    (defn setting
      "Side's value of a setting kept for each side, such as :think-ms; a game made before the sides
      had their own holds one value for both."
      [game k side]
      (let [v (get game k)] (if (map? v) (get v side) v)))
    
    (defn book-on? "Whether Chessmate plays side's first moves from its book." [game side] (not (false? (setting game :book side))))
    
    (defn new-game
      "A game from a FEN, the standard start by default. A FEN that is not a legal position throws
      with the reason. The players are the visitor as White against Chessmate unless players, or
      human, the visitor's side, says otherwise; Chessmate plays from its opening book for a side
      unless book says otherwise, a map by side or a boolean for both; there is no clock unless
      clock is a time control, {:base ms :inc ms}; and a game Chessmate plays against itself waits
      to be started, paused. Chessmate 2026 thinks for think-ms, to max-depth and max-ply when
      they are given."
      ([] (new-game {}))
      ([{:keys [start-fen players human level think-ms max-depth max-ply flipped? book clock]}]
       (let [fen (notation/fen (notation/read-fen (or start-fen notation/standard-start)))]
         {:start-fen fen
          :fens [fen]
          :moves []
          :sans []
          :players (or players (playing (or human :white)))
          :level (per-side level default-level)
          :think-ms (per-side think-ms nil)
          :max-depth (per-side max-depth nil)
          :max-ply (per-side max-ply nil)
          :flipped? (boolean flipped?)
          :book (per-side book true)
          :paused? (not-any? #{:human} (vals (or players (playing (or human :white)))))
          :clock clock
          :times (when clock {:white (:base clock) :black (:base clock)})
          :turn-began nil
          :result (ending [fen])
          :opening nil})))
    
    (defn fen [game] (peek (:fens game)))
    
    (defn position
      "The current position, for the rules and the search to work on."
      [game]
      (notation/read-fen (fen game)))
    
    (defn to-move
      "The side to move: :white or :black."
      [game]
      (if (board/white-to-move? (:state (position game))) :white :black))
    
    (defn player "Who moves for side: :human or :chessmate." [game side] (get-in game [:players side] :chessmate))
    
    (defn humans-turn? [game] (= :human (player game (to-move game))))
    
    (defn waiting?
      "Whether game is new and paused: it waits to be started."
      [game]
      (boolean (and (:paused? game) (empty? (:moves game)) (nil? (:result game)))))
    
    (defn arrive
      "The game as a page shows it on arriving: paused, so that nothing plays and no clock runs
      until the visitor says so, unless it is over, or new and waiting for a human's first move."
      [game]
      (if (or (:result game) (and (empty? (:moves game)) (humans-turn? game)))
        game
        (assoc game :paused? true)))
    
    (defn threats
      "The squares of the side to move's pieces, all but its king, that the other side attacks:
      what show threats marks. The king has its own mark, for check."
      [game]
      (let [{b :board state :state} (position game)
            white? (board/white-to-move? state)]
        (vec (for [sq (range 80)
                   :let [piece (aget b sq)]
                   :when (and (board/on-board? sq) (not= 0 piece) (not= board/king (kind piece))
                              (= white? (pos? piece))
                              (board/attacked? b sq (not white?)))]
               (board/square-name sq)))))
    
    (defn human-side
      "The side the visitor plays against Chessmate, or nil when both sides are human or neither
      is: the side the page calls \"you\"."
      [game]
      (let [humans (filter #(= :human (player game %)) sides)]
        (when (= 1 (count humans)) (first humans))))
    
    (defn bottom
      "The side at the foot of the board: the visitor's, or White's when two play or Chessmate
      plays itself, unless the board is turned round."
      [game]
      (let [base (or (human-side game) :white)]
        (if (:flipped? game) (other base) base)))
    
    ;; The clock: the time controls of bullet, blitz, rapid and classical chess
    
    (defn clock-running?
      "Whether the side to move's clock runs: in a game with a clock, not yet over and not paused,
      once each side has moved, as clocks start in play online, where each side's first move is
      free."
      [game]
      (boolean (and (:clock game) (nil? (:result game)) (not (:paused? game)) (>= (count (:moves game)) 2))))
    
    (defn time-left
      "What side has left on its clock at now, in ms, or nil without a clock."
      [game side now]
      (when-let [t (get (:times game) side)]
        (if-let [began (and (= side (to-move game)) (clock-running? game) (:turn-began game))]
          (- t (- now began))
          t)))
    
    (defn start-turn
      "The game with the side to move's clock started at now, if it runs; else stopped."
      [game now]
      (assoc game :turn-began (when (and now (clock-running? game)) now)))
    
    (defn pause
      "The game with the running clock stopped at now, the time it ran taken off."
      [game now]
      (if-let [began (:turn-began game)]
        (-> game (update-in [:times (to-move game)] - (- now began)) (assoc :turn-began nil))
        game))
    
    (defn- pieces-of
      "The kinds of side's pieces other than its king, on board b."
      [b side]
      (for [sq (range 80)
            :let [piece (aget b sq)]
            :when (and (board/on-board? sq) (not= 0 piece) (not= board/king (kind piece))
                       (= side (if (pos? piece) :white :black)))]
        (kind piece)))
    
    (defn- cannot-mate?
      "Whether side cannot checkmate however the game goes on: it has only its king, or its king
      and one bishop or knight against a lone king. With anything else the other king could, at
      worst, be hemmed in by its own pieces."
      [b side]
      (let [mine (pieces-of b side)]
        (or (empty? mine)
            (and (= 1 (count mine)) (contains? #{board/bishop board/knight} (first mine)) (empty? (pieces-of b (other side)))))))
    
    (defn flag
      "The game when side's time has run out: lost, or drawn if the other side cannot checkmate
      however it goes on (FIDE Laws 6.9)."
      [game side]
      (let [winner (other side)
            b (:board (position game))]
        (assoc game
          :times (assoc (:times game) side 0)
          :turn-began nil
          :result (if (cannot-mate? b winner)
                    {:score "1/2-1/2" :reason :time-no-mate :flagged side}
                    {:score (if (= winner :white) "1-0" "0-1") :winner winner :reason :time}))))
    
    (defn play
      "The game after move m, which must be legal in its position: the move recorded in UCI and
      SAN, the new position, and the result if the game has ended. Made at now, on a clock, it
      takes the time the mover used off the mover's clock and adds the increment, and starts the
      other side's clock."
      ([game m] (play game m nil))
      ([game m now]
       (let [mover (to-move game)
             game (if-let [began (and now (clock-running? game) (:turn-began game))]
                    (update-in game [:times mover] #(+ (- % (- now began)) (get-in game [:clock :inc])))
                    game)
             p (position game)
             san (notation/san p m)
             _ (board/make! (:board p) (:state p) m)
             fens (conj (:fens game) (notation/fen p))]
         (-> game
             (assoc :fens fens
                    :moves (conj (:moves game) (board/uci m))
                    :sans (conj (:sans game) san)
                    :result (ending fens))
             (start-turn now)))))
    
    (defn resign
      "The game, lost by side's resignation."
      [game side]
      (let [winner (other side)]
        (assoc game :turn-began nil :result {:score (if (= winner :white) "1-0" "0-1") :winner winner :reason :resignation})))
    
    (defn- take-back [game n]
      (let [keep (- (count (:moves game)) n)]
        (assoc game
          :fens (subvec (:fens game) 0 (inc keep))
          :moves (subvec (:moves game) 0 keep)
          :sans (subvec (:sans game) 0 keep)
          :result nil)))
    
    (defn undo
      "The game without the last move a human made, and any moves Chessmate made since, so that it
      is that human's turn again; and the SAN of the moves taken back, newest first. A game with no
      human move to take back, or lost on time, is returned as it is, with no moves. The clocks
      keep the time they have."
      [game]
      (let [played (count (:moves game))
            white-first? (board/white-to-move? (:state (notation/read-fen (:start-fen game))))
            mover #(if (= white-first? (even? %)) :white :black) ; the side that made move i
            last-human (last (filter #(= :human (player game (mover %))) (range played)))]
        (if (or (nil? last-human) (#{:time :time-no-mate} (:reason (:result game))))
          [game []]
          (let [n (- played last-human)]
            [(assoc (take-back game n) :turn-began nil) (vec (reverse (take-last n (:sans game))))]))))
    
    (defn move-text
      "The last move with its number, as \"1. e4\" or \"1... e5\", and the result if it ended the
      game, as in \"23. Qh7# 1-0\"."
      [game]
      (let [ply (dec (count (:moves game)))
            start (:state (notation/read-fen (:start-fen game)))
            white-first? (board/white-to-move? start)
            white? (= white-first? (even? ply))
            number (+ (board/fullmove-number start) (quot (+ ply (if white-first? 0 1)) 2))]
        (str number (if white? ". " "... ") (peek (:sans game)) (when-let [r (:result game)] (str " " (:score r))))))
    
    (defn clock-name
      "A time control as players write it, minutes and the increment in seconds: \"5+3\"."
      [{:keys [base inc]}]
      (str (/ base 60000) "+" (/ inc 1000)))
    
    (defn pgn
      "The game as PGN, a human named Visitor and Chessmate as itself, on date, such as
      \"2026.09.25\", with its time control if it had a clock."
      [game date]
      (notation/pgn {:event "Casual game"
                     :site "https://petrustheron.com/chessmate"
                     :date date
                     :round "-"
                     :white (if (= :human (player game :white)) "Visitor" "Chessmate")
                     :black (if (= :human (player game :black)) "Visitor" "Chessmate")
                     :result (or (:score (:result game)) "*")
                     :time-control (when-let [{:keys [base inc]} (:clock game)] (str (/ base 1000) "+" (/ inc 1000)))
                     :termination (when (#{:time :time-no-mate} (:reason (:result game))) "time forfeit")
                     :start-fen (:start-fen game)
                     :sans (:sans game)}))
    
    ;; Storage (D42): JSON of chess data only, validated by replaying it, and never evaluated
    
    (defn- side-data
      "A setting kept for each side, as data for storage: {\"white\" … \"black\" …}."
      [game k]
      {"white" (setting game k :white) "black" (setting game k :black)})
    
    (defn- data
      "The game as plain data for storage: where it began, its moves in UCI, who plays each side,
      the level, the board's orientation, whether Chessmate plays from its book, and the clock and
      each side's time left, as they stood at the last move or when the game was left; and a
      result the moves cannot show, a resignation or a flag."
      [game]
      (let [{:keys [reason] :as result} (:result game)]
        (cond-> {"v" 1
                 "startFen" (:start-fen game)
                 "moves" (:moves game)
                 "players" {"white" (name (player game :white)) "black" (name (player game :black))}
                 "level" (side-data game :level)
                 "flipped" (:flipped? game)
                 "book" {"white" (book-on? game :white) "black" (book-on? game :black)}}
          (some #(setting game :think-ms %) sides) (assoc "think" (side-data game :think-ms))
          (some #(setting game :max-depth %) sides) (assoc "maxDepth" (side-data game :max-depth))
          (some #(setting game :max-ply %) sides) (assoc "maxPly" (side-data game :max-ply))
          (:paused? game) (assoc "paused" true)
          (:clock game) (assoc "clock" {"base" (get-in game [:clock :base]) "inc" (get-in game [:clock :inc])}
                               "times" {"white" (js/Math.round (get-in game [:times :white]))
                                        "black" (js/Math.round (get-in game [:times :black]))})
          (#{:resignation :time :time-no-mate} reason)
          (assoc "result" (cond-> {"score" (:score result) "reason" (name reason)}
                            (:winner result) (assoc "winner" (name (:winner result)))
                            (:flagged result) (assoc "flagged" (name (:flagged result))))))))
    
    (defn encode
      "The game as JSON for browser storage, as data describes it, with the game before it, for
      undo."
      [game]
      (js/JSON.stringify (clj->js (cond-> (data game)
                                    (:previous game) (assoc "previous" (data (:previous game)))))))
    
    (defn- stored-players
      "The players in stored data: its players, or, in a game saved before either side could be
      anyone's, the visitor's side against Chessmate; nil if neither is there and valid."
      [^js data]
      (let [ps (unchecked-get data "players")
            kind {"human" :human "chessmate" :chessmate}]
        (if (some? ps)
          (let [w (kind (unchecked-get ps "white"))
                b (kind (unchecked-get ps "black"))]
            (when (and w b) {:white w :black b}))
          (some-> (get {"white" :white "black" :black} (unchecked-get data "human")) playing))))
    
    (defn- stored-clock
      "The time control and the times left in stored data: [clock times], or [nil nil] with no
      clock; nil if they are not valid."
      [^js data]
      (let [c (unchecked-get data "clock")]
        (if (nil? c)
          [nil nil]
          (let [base (unchecked-get c "base")
                inc (unchecked-get c "inc")
                t (unchecked-get data "times")
                w (some-> t (unchecked-get "white"))
                b (some-> t (unchecked-get "black"))
                ms? #(and (number? %) (js/isFinite %) (<= 0 % 36000000))]
            (when (and (integer? base) (<= 1000 base 10800000) (integer? inc) (<= 0 inc 60000) (ms? w) (ms? b))
              [{:base base :inc inc} {:white w :black b}])))))
    
    (defn- stored-result
      "A result stored data gives, which the moves cannot show: a resignation or a flag; nil if
      there is none, and :bad if it is not valid."
      [^js data]
      (let [r (unchecked-get data "result")
            side {"white" :white "black" :black}]
        (cond (nil? r) nil
              (and (#{"1-0" "0-1" "1/2-1/2"} (unchecked-get r "score"))
                   (#{"resignation" "time" "time-no-mate"} (unchecked-get r "reason")))
              (cond-> {:score (unchecked-get r "score") :reason (keyword (unchecked-get r "reason"))}
                (side (unchecked-get r "winner")) (assoc :winner (side (unchecked-get r "winner")))
                (side (unchecked-get r "flagged")) (assoc :flagged (side (unchecked-get r "flagged"))))
              :else :bad)))
    
    (defn- stored-sides
      "A setting kept for each side in stored data, v: one value for both, as a game saved before
      the sides had their own holds, or {\"white\" … \"black\" …}, each value valid by ok?; [value],
      or [nil] with none, or nil if it is not valid."
      [v ok?]
      (cond (nil? v) [nil]
            (object? v) (let [w (unchecked-get v "white")
                              b (unchecked-get v "black")]
                          (when (and (ok? w) (ok? b)) [{:white w :black b}]))
            (ok? v) [v]
            :else nil))
    
    (defn- from-data
      "The game stored data holds, or nil if it does not hold one: if a field is missing or out of
      range, or if the FEN or any move is not legal. A finished game is one only if finished? says
      so, as the game before may be."
      [^js data finished?]
      (let [players (stored-players data)
            [clock times :as clocked] (stored-clock data)
            [level :as level?] (stored-sides (unchecked-get data "level") #(and (integer? %) (<= 0 % 64)))
            flipped (unchecked-get data "flipped")
            [book :as book?] (stored-sides (unchecked-get data "book") boolean?)
            [think :as think?] (stored-sides (unchecked-get data "think") #(or (nil? %) (and (integer? %) (<= 10 % 300000))))
            [max-depth :as max-depth?] (stored-sides (unchecked-get data "maxDepth") #(or (nil? %) (and (integer? %) (<= 1 % 64))))
            [max-ply :as max-ply?] (stored-sides (unchecked-get data "maxPly") #(or (nil? %) (and (integer? %) (<= 1 % 95))))
            paused (unchecked-get data "paused")
            result (stored-result data)
            moves (unchecked-get data "moves")]
        (when (and (object? data)
                   (== 1 (unchecked-get data "v"))
                   (string? (unchecked-get data "startFen"))
                   players
                   clocked
                   level? (some? level)
                   (boolean? flipped)
                   book? think? max-depth? max-ply?
                   (or (nil? paused) (boolean? paused))
                   (not= result :bad)
                   (array? moves))
          (let [game (reduce (fn [game uci]
                               (let [m (when (and (string? uci) (nil? (:result game)))
                                         (notation/parse-uci (position game) uci))]
                                 (if m (play game m) (reduced nil))))
                             (new-game {:start-fen (unchecked-get data "startFen") :players players :level level
                                        :think-ms think :max-depth max-depth :max-ply max-ply :flipped? flipped :book book :clock clock})
                             moves)
                game (cond-> game (and game result (nil? (:result game))) (assoc :result result))]
            (when (and game (or finished? (nil? (:result game))))
              (cond-> (assoc game :paused? (boolean paused))
                clock (assoc :times times)))))))
    
    (defn decode
      "The unfinished game that encode wrote as text, with the game before it, or nil if text is
      not one: if it is not JSON, if a field is missing or out of range, if the FEN or any move is
      not legal, or if the game has ended. It is read as data, and each move is replayed by the
      rules. A game saved before the book could be turned off plays from the book, and one saved
      before either side could be anyone's has the visitor's side, human, against Chessmate."
      [text]
      (try
        (let [^js d (js/JSON.parse text)]
          (when-let [game (from-data d false)]
            (if-let [before (some-> (unchecked-get d "previous") (from-data true))]
              (assoc game :previous before)
              game)))
        (catch :default _ nil)))