;; type for commands

Chessmate 2005

Chessmate;; warming the interpreter…

You

Moves

    ;; ≈ 1,210 Elo · the new engine:

    Play As
    Difficulty
    Chessmate (Black)Opening Book
    Game Mode
    Theme
    Settings

    Settings

    The 2005 engine

    ;; the Java original: theronic/chessmate · the unfinished 2019 port: theronic/cv/src/cv/chessmate.cljs · the post: · this port: · the new 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

    ;; 10 namespaces, 2799 lines, as this machine runs them

    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 · 337 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")))
    
    (def ^:private results #{"1-0" "0-1" "1/2-1/2" "½-½" "*"})
    
    (def ^:private tag-pair #"\[\s*([A-Za-z0-9_]+)\s+\"((?:[^\"\\]|\\.)*)\"\s*\]")
    
    (defn- movetext
      "The words of a game's moves: PGN without its tags, comments, variations, annotation glyphs
      and move numbers, or a plain list of moves. Results stay, for read-pgn to see."
      [text]
      (let [text (-> text
                     (str/replace tag-pair " ")
                     (str/replace #"\{[^}]*\}" " ")
                     (str/replace #";[^\n]*" " ")
                     (str/replace #"\$\d+" " "))
            ;; variations nest: the innermost go first, until none is left
            text (loop [t text]
                   (let [u (str/replace t #"\([^()]*\)" " ")]
                     (if (= u t) t (recur u))))]
        (->> (str/split text #"\s+")
             (map #(str/replace % #"^\d+\.+" "")) ; 12. and 12... and 12.e4
             (remove str/blank?))))
    
    (defn read-pgn
      "A game from text: PGN, or a list of moves in SAN or in coordinates, such as \"1. e4 e5 2.
      Nf3\" or \"e2e4 e7e5 g1f3\". Returns :start-fen, from its FEN tag or the standard start;
      :moves, in UCI; :tags, its PGN tags by name; and :result, when it gives one. A word that is
      not a legal move throws, naming it by its number."
      [text]
      (let [text (str text)
            tags (into {} (map (fn [[_ k v]] [k (str/replace v #"\\(.)" "$1")])) (re-seq tag-pair text))
            start-fen (fen (read-fen (get tags "FEN" standard-start)))
            {b :board state :state :as position} (read-fen start-fen)
            white-first? (board/white-to-move? state)
            first-number (board/fullmove-number state)
            words (movetext text)]
        (loop [[word & more] (remove results words)
               i 0
               ucis []]
          (if (nil? word)
            {:start-fen start-fen :moves ucis :tags tags :result (or (get tags "Result") (last (filter results words)))}
            (let [number (str (+ first-number (quot (+ i (if white-first? 0 1)) 2)) (if (= white-first? (even? i)) ". " "... ") word)
                  m (try (if (re-matches #"[a-h][1-8][a-h][1-8][nbrqNBRQ]?" word) (parse-uci position word) (parse-san position word))
                         (catch :default e (throw (ex-info (str number ": " (ex-message e)) {:word word :ply i}))))]
              (when-not m (throw (ex-info (str number " is not a legal move here") {:word word :ply i})))
              (board/make! b state m)
              (recur more (inc i) (conj ucis (board/uci m))))))))
    chessmate.eval · 70 lines
    (ns chessmate.eval
      "Chessmate's 2005 heuristic (positionEvaluation in Chess.java), applied to the position it is
      asked about (F1). The Java computed it in 32-bit floats, and so does this port, rounding at
      each step the Java did, so its scores are the Java's exactly."
      (:require
       [chessmate.board :as board]
       [chessmate.moves :as moves]))
    
    (defn- f32
      "x rounded to the nearest 32-bit float, as Java's float arithmetic rounds each result."
      [x]
      (.fround js/Math x))
    
    (def value
      "value, verbatim: \"Arbitrary piece values. Note how the king is indispensable - important
      for checks.\" By piece code: pawn 1, knight 3, bishop 3.2, rook 5, queen 9, king 300."
      (js/Float64Array. #js [0 1 3 (f32 3.2) 5 9 300]))
    
    (def ^:private centre [33 34 43 44])
    
    (defn- sums
      "positionEvaluation's two sums, in the Java's order: material, and control, the second
      before and after the centre squares are counted again. Control comes from calcPieceMoves'
      counts for this position, both sides' pieces as they stand."
      [b]
      (moves/control! b)
      (loop [sq 0
             material 0
             control 0]
        (if (< sq 80)
          (let [piece (aget b sq)]
            (if (or (== piece 0) (== piece board/off-board))
              (recur (inc sq) material control)
              (let [white (aget moves/white-control sq)
                    black (aget moves/black-control sq)
                    control (f32 (- (f32 (+ control white)) black))
                    control (cond (and (neg? piece) (> white black)) (f32 (+ control (aget value (- piece))))
                                  (and (pos? piece) (< white black)) (f32 (- control (aget value piece)))
                                  :else control)
                    worth (f32 (* 5 (aget value (if (neg? piece) (- piece) piece))))]
                (recur (inc sq) (f32 (+ material (if (neg? piece) (- worth) worth))) control))))
          #js [material
               control
               (reduce (fn [c sq] (f32 (+ c (- (aget moves/white-control sq) (aget moves/black-control sq)))))
                       control
                       centre)])))
    
    (defn evaluate
      "positionEvaluation: the score of the board for White, or for Black when white? is false.
      Material is 5 times each piece's value (pawn 1, knight 3, bishop 3.2, rook 5, queen 9, king
      300), White's counted up and Black's down. Control is each side's reach, from calcPieceMoves:
      12 for a pawn attacking a piece, 1 for every square another piece can move to. For every
      piece, White's control of its square is added and Black's taken away; a piece the other side
      controls more also counts its value against its owner. The centre squares d4, e4, d5 and e5
      count once more. The score is material + 0.333 x control."
      [b white?]
      (let [[material _ control] (sums b)
            score (f32 (+ material (f32 (* control 0.333))))]
        (if white? score (- score))))
    
    (defn explain
      "The terms of a position's score: :material, :control of the occupied squares with the
      values of pieces the other side controls more, the :centre counted again, and the :score
      for the side to move, which is :material + 0.333 x (:control + :centre)."
      [{b :board state :state}]
      (let [[material control with-centre] (sums b)]
        {:material material
         :control control
         :centre (- with-centre control)
         :score (evaluate b (board/white-to-move? state))}))
    chessmate.search · 194 lines
    (ns chessmate.search
      "The 2005 search (alphaBeta and alphaBetaHelper in Chess.java), ported step for step, with
      its bugs fixed (D30, D33). Each fix is marked FIX, with its number from the design."
      (:require
       [chessmate.board :as board]
       [chessmate.eval :as eval]
       [chessmate.moves :as moves]))
    
    ;; The search's state. The Java kept it in statics, and one search runs at a time here too.
    (def ^:private node-count (volatile! 0)) ; nodeCount
    (def ^:private reached-depth (volatile! 0)) ; reachedDepth
    (def ^:private best-move (volatile! nil)) ; bestMove
    (def ^:private lines
      "principalVariation, as whole lines: at each depth, the line from that node (F7)."
      (make-array 16))
    (def ^:private now (volatile! nil)) ; the clock, in milliseconds
    (def ^:private deadline (volatile! nil)) ; when to stop, once the first iteration is complete
    (def ^:private stopped? (volatile! false)) ; bThinking, cleared
    
    (defn- count-node!
      "++nodeCount, and every 1,024 nodes a look at the clock."
      []
      (let [n (vswap! node-count inc)]
        (when (and (zero? (bit-and n 1023)) @deadline (> (@now) @deadline))
          (vreset! stopped? true))))
    
    (defn- mate-score
      "The score of a side that is mated at depth, reached with extension: the 2005 scale of
      remaining depth, doubled next to the root, seen from the mated side."
      [max-depth depth extension]
      (- (* (max 1 (+ (- max-depth depth) extension)) (if (== depth 1) 2000 1000))))
    
    (declare alpha-beta-helper)
    
    (defn- try-move
      "One turn of alphaBetaHelper's loop: play m and, if it is legal, count the node, search the
      reply, and take the move back. Returns m's score for the side that played it, or nil if m
      is illegal."
      [b state max-depth complete? depth white? alpha beta m]
      (let [pawn (if white? board/pawn (- board/pawn))
            ;; FIX (F4): the extension belongs to this move's subtree; 2005 kept one global
            extension (if (or (not= 0 (aget b (board/to-sq m))) (== pawn (aget b (board/from-sq m)))) 2 0)
            undo (board/make! b state m)]
        ;; FIX (F2): a move that leaves the mover's king attacked is illegal, and skipped
        (if (board/king-attacked? b white?)
          (do (board/unmake! b state m undo)
              nil)
          (do (count-node!)
              (let [value (- (alpha-beta-helper b state max-depth complete? (inc depth) (not white?) (- beta) (- alpha)
                                                extension))]
                (board/unmake! b state m undo)
                value)))))
    
    (defn- new-best!
      "Record m as the best move so far at depth, with the line it leads to (F7)."
      [depth m]
      (aset lines depth (into [m] (aget lines (inc depth))))
      (when (zero? depth) (vreset! best-move m)))
    
    (defn- no-move-score
      "The score of a side with no legal move: mated if it is in check, and a draw if not."
      [b white? max-depth depth extension]
      ;; FIX (F3): 2005 returned the window's bound, which made stalemating the best result there was
      (if (board/king-attacked? b white?) (mate-score max-depth depth extension) 0))
    
    (defn alpha-beta-helper
      "alphaBetaHelper: the score of the position on b and state for the side to move, white?,
      searched to max-depth plies from the root. This node is depth plies from the root; extension
      is 2 when the move that reached it was a capture or a pawn move, which lets it look two plies
      further. Fail-hard, in the 2005 naming: alpha is the upper bound, beta the best score so far.
      A score at or above alpha returns alpha, and only a strictly better score replaces beta.
      complete? false plays by the 2005 rules, without castling and en passant."
      [b state max-depth complete? depth white? alpha beta extension]
      (when (> depth @reached-depth) (vreset! reached-depth depth))
      (aset lines depth [])
      (if (or (>= (- depth extension) max-depth) @stopped?)
        ;; FIX (F1): the evaluation computes the control data of this position
        (eval/evaluate b white?)
        (let [moves (moves/possible-moves b state complete?)]
          (loop [i 0
                 beta beta
                 legal 0]
            (if (< i (alength moves))
              (let [m (aget moves i)
                    value (try-move b state max-depth complete? depth white? alpha beta m)]
                (cond (nil? value) (recur (inc i) beta legal)
                      (>= value alpha) alpha
                      (> value beta) (do (new-best! depth m)
                                         (recur (inc i) value (inc legal)))
                      :else (recur (inc i) beta (inc legal))))
              (if (pos? legal) beta (no-move-score b white? max-depth depth extension)))))))
    
    (defn- named-line
      "The moves of a line from position, each promotion naming its queen."
      [position line]
      (let [{b :board state :state} (board/copy position)]
        (mapv (fn [m]
                (let [piece (aget b (board/from-sq m))
                      promotes? (and (== board/pawn (if (neg? piece) (- piece) piece))
                                     (== (board/rank-of (board/to-sq m)) (if (pos? piece) 7 0))
                                     (zero? (board/promotion m)))
                      m (if promotes? (board/move (board/from-sq m) (board/to-sq m) board/queen) m)]
                  (board/make! b state m)
                  m))
              line)))
    
    (defn- report
      "What an iteration found: its move and score for the side to move, the moves to mate when
      the score is a mate's, the depths, the thinking line, and the effort."
      [position depth score start]
      (let [line (named-line position (aget lines 0))
            ms (- (@now) start)
            nodes @node-count]
        {:move (first line)
         :score score
         :mate (when (>= (if (neg? score) (- score) score) 1000)
                 (if (pos? score) (quot (inc (count line)) 2) (- (quot (count line) 2))))
         :depth depth
         :reached @reached-depth
         :line line
         :nodes nodes
         :ms ms
         :nps (if (pos? ms) (.round js/Math (/ (* 1000 nodes) ms)) 0)}))
    
    (defn- start!
      "Reset the search's state for a new search, timed by clock."
      [clock]
      (vreset! node-count 0)
      (vreset! reached-depth 0)
      (vreset! best-move nil)
      (vreset! now clock)
      (vreset! deadline nil)
      (vreset! stopped? false)
      (aset lines 0 []))
    
    (defn search
      "alphaBeta: Chessmate's move for a position, and how it was found. Deeper than 3, the
      search deepens from 3 to the level, each iteration with the whole window (F5); otherwise it
      searches once at the level. Options:
        :level         the maximum depth, 1 to 6
        :rules         :2005 for no castling or en passant, as the reference; else the complete rules
        :limit-ms      once an iteration is complete, stop after this long (60,000 ms)
        :now           the clock, in milliseconds
        :on-iteration  called with each completed iteration's report
      Returns the report of the deepest completed iteration (F6): :move, :score for the side to
      move, :mate, :depth, :reached, :line, :nodes, :ms and :nps. The position is unchanged."
      [position {:keys [level rules limit-ms on-iteration] :as options}]
      (let [{b :board state :state} (board/copy position)
            white? (board/white-to-move? state)
            complete? (not= rules :2005)
            clock (or (:now options) #(.now js/performance))
            start (clock)]
        (start! clock)
        (reduce (fn [completed max-depth]
                  (let [score (alpha-beta-helper b state max-depth complete? 0 white? 100000 -100000 0)]
                    ;; FIX (F6): a stopped iteration is discarded, and the deepest completed one kept
                    (if @stopped?
                      (reduced completed)
                      (let [done (report position max-depth score start)]
                        (vreset! deadline (+ start (or limit-ms 60000)))
                        (when on-iteration (on-iteration done))
                        done))))
                nil
                (if (> level 3) (range 3 (inc level)) [level]))))
    
    (defn sliced
      "The search at the root one move at a time, for the page when the worker is unavailable
      (D33). The level must be 2 or less, which needs one iteration. Returns a function to call
      once per task: each call searches the next root move, carrying beta to the next, and returns
      nil, until the call after the last move, which returns search's report. It plays what search
      plays: this is alphaBetaHelper's loop at the root, where alpha is 100000 and never reached."
      [position {:keys [level rules now]}]
      (let [{b :board state :state} (board/copy position)
            white? (board/white-to-move? state)
            complete? (not= rules :2005)
            clock (or now #(.now js/performance))
            start (clock)
            moves (moves/possible-moves b state complete?)
            i (volatile! 0)
            beta (volatile! -100000)
            legal (volatile! 0)]
        (start! clock)
        (fn step []
          (if (< @i (alength moves))
            (let [m (aget moves @i)
                  value (try-move b state level complete? 0 white? 100000 @beta m)]
              (vswap! i inc)
              (when (some? value)
                (vswap! legal inc)
                (when (> value @beta)
                  (vreset! beta value)
                  (new-best! 0 m)))
              nil)
            (report position level (if (pos? @legal) @beta (no-move-score b white? level 0 0)) start)))))
    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 · 622 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
        :later       the moves taken back, as [uci san], the next first, while there are any: the
                     line the game had gone on to, which the move list shows after its moves, to
                     play again, until another move is played"
      (: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. A new game waits for Play, as a reset board
      does."
      [game]
      (if (:result 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))
    
    (def ^:private engine-settings
      "How Chessmate plays a side, each kept for each side: the 2005 engine's depth, Chessmate
      2026's thinking time, Depth and Max Depth, and the opening book."
      [:level :think-ms :max-depth :max-ply :book])
    
    (defn- lone-chessmate
      "The side Chessmate plays against a person, or nil when it plays both sides or neither."
      [players]
      (let [sides (filter #(= :chessmate (get players %)) sides)]
        (when (= 1 (count sides)) (first sides))))
    
    (defn with-players
      "g, a game or a new game's settings, with players moving for its sides: when the visitor
      changes sides, Chessmate's settings go with it to its new side (owner, 2026-09-28:
      \"Switching \"Play As\" from White -> Black, or Black -> White should retain the computer
      settings\"). Otherwise each side keeps its own, as Chessmate's side does when it is set to
      play both (\"the computer player side (White or Black) should retain its AI settings\")."
      [g players]
      (let [from (lone-chessmate (:players g))
            to (lone-chessmate players)]
        (cond-> (assoc g :players players)
          (and from to (not= from to))
          (as-> g (reduce (fn [g k] (let [v (get g k)] (if (map? v) (assoc-in g [k to] (get v from)) g)))
                          g
                          engine-settings)))))
    
    (defn set-players
      "The game with players moving for its sides from now on, {:white :human :black :chessmate},
      Chessmate's settings going with it when the visitor changes sides (with-players). A game
      that Chessmate is to play against itself pauses, its clock stopped at now, and waits for
      Play; any other goes on as it was, paused or not."
      [game players now]
      (cond-> (with-players game players)
        (and (not-any? #{:human} (vals players)) (not (:paused? game)) (nil? (:result game)))
        (-> (pause now) (assoc :paused? true))))
    
    (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- with-later
      "The game with later as the moves it has taken back, or with none."
      [game later]
      (if (seq later) (assoc game :later (vec later)) (dissoc game :later)))
    
    (defn- taken-back
      "The moves after the first keep of the game's, as [uci san], and after them the moves it took
      back before: what it keeps once they are taken back."
      [game keep]
      (into (mapv vector (subvec (:moves game) keep) (subvec (:sans game) keep)) (:later game)))
    
    (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. The next of the moves taken back, played again, leaves the rest of them
      to play; any other move leaves their line, and they are forgotten."
      ([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)
             uci (board/uci m)
             later (:later game)
             _ (board/make! (:board p) (:state p) m)
             fens (conj (:fens game) (notation/fen p))]
         (-> game
             (assoc :fens fens
                    :moves (conj (:moves game) uci)
                    :sans (conj (:sans game) san)
                    :result (ending fens))
             (with-later (when (= uci (first (first later))) (subvec later 1)))
             (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- lost-on-time? [game] (contains? #{:time :time-no-mate} (:reason (:result game))))
    
    (def ^:private settings
      "What the visitor sets for a game, which Take Back keeps as it is: who plays each side, how
      Chessmate plays it, the clock, and which way the board faces."
      [:players :level :think-ms :max-depth :max-ply :book :clock :flipped?])
    
    (defn take-back
      "Take Back (owner, 2026-09-28: \"Take Back should *only* take back moves\"): the game with its
      last move taken back, and the SAN of the moves taken back, newest first. When a person plays,
      Chessmate's moves since that person's last are taken back with it, so that it is their move
      again: two people take back one move, a person playing Chessmate their move and its reply,
      and Chessmate playing itself its last move. A game with no move, as a reset board is, gives
      way to the game before it, which a new game keeps (owner: \"in case they accidentally pressed
      Reset Board but wanted to continue the game\"), with :previous-game for the moves: that game's
      moves and result, in the settings as they are now, its times kept if its clock is the one
      now. Take Back never changes a setting. A game that Chessmate is then to move in is paused,
      so that it waits for Play rather than moving again at once. The clock stops at now, with the
      time it ran taken off, and the clocks keep the time they have. The moves taken back are kept,
      in :later, for the move list to play again. A game lost on time, or with nothing to take
      back, is returned as it is, with no moves."
      [game now]
      (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)))
            n (cond last-human (- played last-human) (pos? played) 1 :else 0)
            before (:previous game)
            clock (:clock game)
            waits #(cond-> % (and (nil? (:result %)) (not (humans-turn? %))) (assoc :paused? true))]
        (cond (lost-on-time? game) [game []]
              (pos? n) (let [keep (- played n)
                             g (pause game now)]
                         [(waits (-> g
                                     (assoc :fens (subvec (:fens g) 0 (inc keep))
                                            :moves (subvec (:moves g) 0 keep)
                                            :sans (subvec (:sans g) 0 keep)
                                            :result nil)
                                     (with-later (taken-back g keep))))
                          (vec (reverse (take-last n (:sans game))))])
              before [(waits (-> (merge before (select-keys game settings))
                                 (assoc :times (if (= clock (:clock before))
                                                 (:times before)
                                                 (when clock {:white (:base clock) :black (:base clock)}))
                                        :turn-began nil)))
                      :previous-game]
              :else [game []])))
    
    (defn can-take-back?
      "Whether Take Back has something to take back: a move, unless the game was lost on time; or,
      with no move, the game before it."
      [game]
      (boolean (if (seq (:moves game)) (not (lost-on-time? game)) (:previous game))))
    
    (defn go-to
      "The game as it was after its first ply moves, 0 being its start (owner, 2026-09-28: \"clicking
      on a move (for White or black), reverts the game state to that move, so that Player can
      resume from that point (possibly with different game settings)\"): back to one of its moves,
      the moves after it taken back and kept in :later; or on to one of the moves taken back, those
      up to it played again. It then waits, paused, for Play or a person's move, unless it is over.
      No setting changes. The clock stops at now, with the time it ran taken off, and the clocks
      keep the time they have, as Take Back leaves them. The game as it is, for the move it has
      reached; nil when it has no such move, or was lost on time, when Take Back takes nothing back
      either."
      [game ply now]
      (let [played (count (:moves game))
            later (:later game)]
        (cond (or (lost-on-time? game) (not (integer? ply)) (not (<= 0 ply (+ played (count later))))) nil
              (= ply played) game
              :else (let [g (pause game now)
                          g (if (< ply played)
                              (-> g
                                  (assoc :fens (subvec (:fens g) 0 (inc ply))
                                         :moves (subvec (:moves g) 0 ply)
                                         :sans (subvec (:sans g) 0 ply)
                                         :result nil)
                                  (with-later (taken-back g ply)))
                              (reduce (fn [g [uci]] (play g (notation/parse-uci (position g) uci)))
                                      g
                                      (subvec later 0 (- ply played))))]
                      (assoc g :paused? (nil? (:result g)))))))
    
    (defn can-go-to?
      "Whether the game can go back to its moves, and on to the moves it took back: unless it was
      lost on time."
      [game]
      (not (lost-on-time? game)))
    
    (defn ply-after
      "How many moves the game has made once side, :white or :black, has made its move numbered n,
      as the game numbers them from where it began: 1 after 1. e4, and 2 after 1... e5."
      [game n side]
      (let [start (:state (notation/read-fen (:start-fen game)))]
        (+ (* 2 (- n (board/fullmove-number start)))
           (if (= side :white) 1 2)
           (if (board/white-to-move? start) 0 -1))))
    
    (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 today
      "The date as PGN writes it, as \"2026.09.25\"."
      []
      (let [d (js/Date.)
            pad #(.padStart (str %) 2 "0")]
        (str (.getFullYear d) "." (pad (inc (.getMonth d))) "." (pad (.getDate d)))))
    
    (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; a result
      the moves cannot show, a resignation or a flag; and the moves taken back, in UCI."
      [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)))))
          (seq (:later game)) (assoc "later" (mapv first (:later game))))))
    
    (defn encode
      "The game as JSON for browser storage, as data describes it, with the game before it, for
      Take Back."
      [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- stored-later
      "The moves taken back that stored data gives, ucis, as [uci san] from the game's position on:
      nil when there are none, or when one is not a legal move where it is played, or follows the
      end of their line. They are only there to be played again, so a bad one loses them, not the
      game."
      [game ucis]
      (when (and (array? ucis) (pos? (alength ucis)))
        (let [played (count (:moves game))
              g (reduce (fn [g uci]
                          (if-let [m (when (and (string? uci) (nil? (:result g))) (notation/parse-uci (position g) uci))]
                            (play g m)
                            (reduced nil)))
                        (dissoc game :result :later)
                        ucis)]
          (when g (mapv vector (subvec (:moves g) played) (subvec (:sans g) played))))))
    
    (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. The moves taken back come with it while they are legal."
      [^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-> (with-later (assoc game :paused? (boolean paused)) (stored-later game (unchecked-get data "later")))
                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)))
    chessmate.worker · 72 lines
    (ns chessmate.worker
      "The background search: what the compiled worker does with each message. Evaluating this
      namespace only defines functions; chessmate.worker-main starts it."
      (:require
       [chessmate.board :as board]
       [chessmate.eval :as eval]
       [chessmate.moves :as moves]
       [chessmate.search :as search]))
    
    (defn request
      "A message for the worker about a position (D41): its type, which is \"search\", or
      \"perft\" or \"evaluate\" for the tests; its id; the position's board and state; and
      more fields from extra, a map with string keys."
      [type id {b :board state :state} extra]
      (let [msg #js {"type" type
                     "id" id
                     "board" (.from js/Array b)
                     "white" (board/white-to-move? state)
                     "castling" (board/castling state)
                     "ep" (board/en-passant state)}]
        (doseq [[k v] extra] (unchecked-set msg k v))
        msg))
    
    (defn- position-of
      "The position a request describes."
      [^js msg]
      (board/position (js/Int8Array. (unchecked-get msg "board")) (unchecked-get msg "white")
                      (unchecked-get msg "castling") (unchecked-get msg "ep") 0 1))
    
    (defn- reply
      "An info or best message from a search report: moves in UCI, the score for the side to move."
      [type id {:keys [move score mate depth reached line nodes nps ms]}]
      #js {"type" type
           "id" id
           "move" (when move (board/uci move))
           "score" score
           "mate" mate
           "depth" depth
           "reached" reached
           "pv" (to-array (map board/uci line))
           "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 (D41), so the compiled worker never loses a
      field to renaming."
      [^js msg post]
      (let [kind (unchecked-get msg "type")
            id (unchecked-get msg "id")]
        (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
                       (post (reply "best" id (search/search (position-of msg)
                                                {:level (unchecked-get msg "maxDepth")
                                                 :rules (if (= "2005" (unchecked-get msg "rules")) :2005 :complete)
                                                 :limit-ms (unchecked-get msg "capMs")
                                                 :on-iteration #(post (reply "info" id %))}))))
          "evaluate" (let [{b :board state :state} (position-of msg)]
                       (post #js {"type" "evaluate" "id" id "score" (eval/evaluate b (board/white-to-move? state))}))
          "perft" (let [start (.now js/performance)
                        nodes (moves/perft (position-of msg) (unchecked-get msg "depth"))]
                    (post #js {"type" "perft" "id" id "nodes" nodes "ms" (- (.now js/performance) start)}))
          (post #js {"type" "error" "id" id "message" (str "unknown message type " (pr-str kind))}))))
    
    (defn start!
      "Answer the page's messages. Only the compiled worker calls this, from
      chessmate.worker-main."
      []
      (set! (.-onmessage js/self)
            (fn [^js e] (handle (.-data e) (fn [reply] (.postMessage js/self reply))))))
    chessmate.core · 873 lines
    (ns chessmate.core
      "Play Chessmate 2005: the commands the Listener reaches as chessmate-2005/..., and the game on
      /chessmate-2005.
      Every move, the visitor's and Chessmate's, is a (chessmate-2005/move! !game ...) form evaluated through the
      Listener (D38), so the transcript is the whole game."
      (:require
       [chessmate.board :as board]
       [chessmate.book :as book]
       [chessmate.eval :as eval]
       [chessmate.game :as game]
       [chessmate.moves :as moves]
       [chessmate.notation :as notation]
       [chessmate.search :as search]
       [chessmate.worker :as worker]
       [clojure.string :as str]
       [pt.host :as host]))
    
    (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 players' bars and (chessmate-2005/game)
    (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-2005 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.chess")
    (def ^:private fallback-level 1) ; the page's own search keeps each task short at level 1 (D33)
    (def ^:private deepest 64) ; how deep a search with no limit may go: it stops when its time is up, a minute without a clock
    
    (def difficulties
      "The page's Difficulty (owner, 2026-09-28), each level's depth, as the game keeps it, :level,
      0 for no limit, and whether it plays its first moves from its book, which Novice and Easy do
      not (\"Novice: 10ms, No Opening Book. Easy: 250ms, No Opening Book.\"): the 2005 search has
      no Max Depth, and no thinking time of its own. With no limit, or a deep one, it deepens until
      its time is up, a minute a move without a clock. Easy, its depth of 4 without a book, as the
      2005 game played, is the default."
      (array-map :novice {:level 2 :book false}
                 :easy {:level 4 :book false}
                 :medium {:level 6 :book true}
                 :hard {:level 8 :book true}
                 :chessmate {:level 0 :book true}))
    
    (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- white-score
      "The 2005 heuristic's score for the position, from White's point of view."
      [p]
      (let [score (eval/evaluate (:board p) (board/white-to-move? (:state p)))]
        (if (board/white-to-move? (:state p)) score (- score))))
    
    (defn- winning
      "The side a score for White favours: :white, :black, or :neither when it is 0."
      [score]
      (cond (pos? score) :white
            (neg? score) :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 for the side to move, as the page
      and (chessmate-2005/game) show it: the score from White's point of view, the line in SAN."
      [p {:keys [depth reached score mate pv nodes nps ms]}]
      (let [white? (board/white-to-move? (:state p))
            white-score (if white? score (- score))]
        {:depth depth
         :reached (max (or reached 0) (or depth 0))
         :pawns (/ white-score 5) ; the 2005 heuristic's pawn is worth 5
         :mate (when mate (if (= (pos? mate) white?) (js/Math.abs mate) (- (js/Math.abs mate)))) ; White's mate, or Black's, negative
         :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
              (game/waiting? g) (if (game/human-side g) "Your move" (str (side-name (game/to-move g)) " to move")) ; a first move starts it
              (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 @!game, the depth in the player's bar
              (= 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- 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 2005
      heuristic, in its units, a pawn being worth 5: in words, and as White's score in pawns and a
      mate found, White's positive and Black's negative, for the board's bar; and that search's
      depth, nodes, speed and time. A search's figures are forgotten when the position changes."
      [g p {:keys [depth reached score ahead nodes nps ms pawns mate]}]
      (let [[ahead score pawns] (if depth [ahead score pawns] (let [w (white-score p)] [(winning w) (signed w) (/ w 5)]))]
        (cond-> {:verdict (verdict g ahead score) :pawns pawns}
          (and depth mate) (assoc :mate mate)
          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
      "Moves of game g, in SAN, 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 sans]
      (let [start (:state (notation/read-fen (:start-fen g)))
            sans (cond->> sans (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- difficulty-of
      "The Difficulty that side's depth and book make in game g, or nil when they make none."
      [g side]
      (some (fn [[level {:keys [book] :as settings}]]
              (when (and (= (:level settings) (game/setting g :level side)) (= book (game/book-on? g side))) level))
            difficulties))
    
    (defonce ^:private !pgn (atom [nil nil])) ; the last game written as PGN, by what PGN says of it, and its PGN
    
    (defn- pgn-text
      "Game g as PGN, for the move list's Copy: written again only when its moves, result, players
      or clock change, as the page redraws while Chessmate thinks."
      [g]
      (let [k [(:start-fen g) (:sans g) (:result g) (:players g) (:clock g)]
            [seen text] @!pgn]
        (if (= k seen)
          text
          (let [text (game/pgn g (game/today))]
            (reset! !pgn [k text])
            text))))
    
    (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))
                         :flipped (boolean (:flipped? g)) ; the board turned round from the visitor's side: Flip Board
                         :human (some-> (game/human-side g) name) ; the visitor's side, when one side is theirs
                         ;; the Difficulty Chessmate plays the visitor at, when it plays one side against them
                         :difficulty (some->> (game/human-side g) game/other (difficulty-of g) name)
                         :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))
                         :engine (name (:status e)) ; idle, book, thinking or fallback: what the side to move's bar says
                         :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)})
                         :take-back (game/can-take-back? g) ; for the Take Back button
                         :level (:level g)
                         :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 p e)
                         :rows (move-rows g (:sans g))
                         ;; the move list's: the moves, then the moves taken back, which a click plays again
                         :line (move-rows g (into (:sans g) (map second) (:later g)))
                         ;; whether a click on a move goes to it
                         :go-to (game/can-go-to? g)
                         ;; the game as PGN, which the move list's Copy copies (owner, 2026-09-28)
                         :pgn (pgn-text g)}))))
    
    (defn- draw!
      "Show the game on /chessmate-2005: the board, the players, the controls and the moves (D39)."
      []
      (when @!mounted
        (draw-now!)))
    
    (defn- announce! [text] (when @!mounted (.announce (host/chess-host) text)))
    
    ;; Storage (D42): 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- restore []
      (try (game/decode (.getItem js/localStorage storage-key)) (catch :default _ nil)))
    
    ;; Chessmate's replies (D38): each a (chessmate-2005/move! !game ...) form, built by code from a legal move
    
    (defn- move-form [uci]
      (str "(chessmate-2005/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)
                   (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 2005 · 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- fallback!
      "Search on the page, one root move per task (D33), when the worker cannot run."
      [id ply g]
      (let [p (game/position g)
            step (search/sliced p {:level fallback-level})]
        (swap! !engine assoc :status :fallback)
        (draw!)
        ((fn next-step []
           (when (= id @!search-id)
             (if-let [r (step)]
               (finish-search! id ply p (assoc r :move (board/uci (:move r)) :pv (map board/uci (:line r))))
               (js/setTimeout next-step 0)))))))
    
    (defn- think-ms
      "On a clock, the time Chessmate may think, in ms, after which it plays the deepest search it
      has finished: a twentieth of the time it has left and the increment, and never more than a
      quarter of what is left. Without a clock, nil: it searches to its depth, as the 2005 game
      did."
      [g]
      (when-let [left (and (game/clock-running? g) (:turn-began g) (game/time-left g (game/to-move g) (clock-now)))]
        (js/Math.round (max 10 (min (+ (/ left 20) (get-in g [:clock :inc])) (/ left 4))))))
    
    (defn- search!
      "Search in the background worker, at the side's depth (D41), or with none, deepening until
      its time is up; on a clock stopping when its time says. Each completed iteration redraws the
      status; the deepest completed iteration's move is played. If the worker cannot run, the page
      searches itself."
      [id ply g]
      (let [p (game/position g)
            cap (think-ms g)
            level (game/setting g :level (game/to-move g))]
        (swap! !engine assoc :status :thinking)
        (.search (host/chess-host)
                 (worker/request "search" id p (cond-> {"maxDepth" (if (pos? level) level deepest)} cap (assoc "capMs" cap)))
                 (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- think!
      "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 2005 · 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 the worker."
      []
      (swap! !search-id inc)
      (.stop (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 until the next search."
      []
      (save!)
      (if (and @!mounted (chessmates-turn? @!game) (not (:paused? @!game)))
        (think!)
        (do (swap! !engine assoc :status :idle) (draw!)))
      (arm-flag!))
    
    (defn- start-game! [g]
      (stop-thinking!)
      (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 chessmate-2005/...
    
    (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 "(chessmate-2005/play! :white) or (chessmate-2005/play! :black) starts a new game"}))
    
    (defn- parse-move
      "The legal move that the arguments of chessmate-2005/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 'chessmate-2005/move! '!game args)) " is not a move")
                                          {:pt/hint "give two squares, as (chessmate-2005/move! !game :e2 :e4), or a move in a string, as (chessmate-2005/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"
                                              "(chessmate-2005/moves) lists the legal moves")})))
              :else (try (notation/parse-san p text)
                         (catch :default e
                           (throw (ex-info (ex-message e) {:pt/hint "(chessmate-2005/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 "(chessmate-2005/" cmd " …) takes the game first, as (chessmate-2005/" 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 (chessmate-2005/move! !game :e2 :e4),
      with the piece for a promotion, as (chessmate-2005/move! !game :e7 :e8 :n); or in a string, in coordinates,
      as (chessmate-2005/move! !game \"e2e4\"), or in SAN, as (chessmate-2005/move! !game \"Nf3\") or (chessmate-2005/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 "(chessmate-2005/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. What it leaves out is as the game before, and Chessmate's settings go with it when
      the visitor changes sides."
      [arg]
      (let [g @!game
            before {:players (:players g (game/playing :white)) :clock (:clock g) :level (:level g game/default-level) :book (:book g)}]
        (cond (#{:white :black} arg) (game/with-players before (game/playing arg))
              (map? arg) (let [ps (merge (:players before) (select-keys arg [:white :black]))
                               players {:white (:white ps) :black (:black ps)}]
                           (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-> (game/with-players before players)
                             (contains? arg :clock) (assoc :clock (parse-clock (:clock arg)))))
              :else (throw (ex-info (str "There is no side " (pr-str arg))
                                    {:pt/hint "(chessmate-2005/play! :white) or (chessmate-2005/play! :black), or a map, as (chessmate-2005/play! {:clock \"5+3\"})"})))))
    
    (defn- kept
      "The game to keep as the one before a new game, for Take Back: 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; or as the last game was played, with each side's settings. It waits for Play,
      (chessmate-2005/resume! !game), or a person's first move. The game in progress is kept:
      (chessmate-2005/take-back! !game), with no move to take back, goes back to it, in the
      settings as they are then. Returns the game, !game."
      ([] (new-game! {}))
      ([arg]
       (start-game! (assoc (game/new-game (game-options arg)) :paused? true :previous (kept @!game))) ; it waits for Play
       !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. Chessmate set to play itself waits for Play. The board turns
      to the visitor's side. When the visitor changes sides, Chessmate takes its settings to its
      new side: its depth and book. 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
            g (swap! !game game/set-players {:white white :black black} (clock-now))
            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 (chessmate-2005/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 take-back!
      "Take back the last move, and stop any thinking (owner, 2026-09-28): with a person playing,
      Chessmate's moves since that person's last with it, so that it is their move again; when
      Chessmate plays itself, its last move, the game paused. With no move to take back, as after
      Reset Board, go back to the game before this one, in the settings as they are. It never
      changes a setting, and a game that Chessmate is then to move in waits for Play. The moves
      taken back stay in the move list, to play again with (chessmate-2005/go-to! !game n side).
      Returns the moves taken back, newest first, or :previous-game."
      [a]
      (this-game a "take-back!")
      (let [was @!game
            [g taken] (game/take-back was (clock-now))
            paused (when (and (:paused? g) (not (:paused? was))) ", paused")]
        (cond (= taken :previous-game) (do (stop-thinking!)
                                         (.stop (host/chess-host)) ; its search learnt nothing of the game before
                                         (reset! !game (game/start-turn g (clock-now)))
                                         (announce! (str "Back to the game before" paused))
                                         (changed!)
                                         :previous-game)
              (seq taken) (do (stop-thinking!)
                              (reset! !game (game/start-turn g (clock-now)))
                              (announce! (str "Took back " (str/join " and " taken) paused))
                              (changed!)
                              taken)
              :else (do (note "nothing to take back") nil))))
    
    (def ^{:doc "The same as take-back!, by its first name."} undo! take-back!)
    
    (defn go-to!
      "Go back to the game as it was after the move numbered n of side, :white or :black, as
      (chessmate-2005/go-to! !game 12 :black), the moves after it taken back; or on to one of the
      moves taken back, which the move list shows after the game's, playing it again with those
      before it (owner, 2026-09-28: \"clicking on a move (for White or black), reverts the game
      state to that move, so that Player can resume from that point (possibly with different game
      settings)\"). A click on a move in the list does this. It stops any thinking, and the game
      waits, paused, for Play or a person's move, so that the settings can change first; no setting
      changes, and the clocks keep the time they have. Returns the move, as \"12... Nc6\"."
      [a n side]
      (this-game a "go-to!")
      (when-not (and (integer? n) (#{:white :black} side))
        (throw (ex-info (str (pr-str (list 'chessmate-2005/go-to! '!game n side)) " names no move")
                        {:pt/hint "a move's number and side, as (chessmate-2005/go-to! !game 12 :black) for Black's 12th"})))
      (let [was @!game
            played (count (:moves was))
            ply (game/ply-after was n side)]
        (when-not (<= 1 ply (+ played (count (:later was))))
          (throw (ex-info (str "There is no move " n " for " (side-name side) " in this game")
                          {:pt/hint "the moves in the move list, and those taken back after them, can be gone to"})))
        (if-let [g (when-not (= ply played) (game/go-to was ply (clock-now)))]
          (do (stop-thinking!)
              (reset! !game (game/start-turn g (clock-now)))
              (announce! (str (if (> ply played) "On to " "Back to ") (side-name side) "'s move " n ", "
                              (str/replace (peek (:sans g)) #"[+#]$" "") (when (:paused? g) ", paused")))
              (changed!)
              (game/move-text g))
          (if (= ply played)
            (game/move-text was) ; the game is there already
            (do (note "the game was lost on time, and its moves stay as they are") 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!)
          (think!))))
    
    (defn- set-side!
      "Set k, a setting kept for each side, to v: for side, or for both sides when side is nil.
      Chessmate choosing that side's move starts again, with it."
      [k side v]
      (swap! !game (fn [g] (reduce #(assoc-in %1 [k %2] v)
                                   (update g k #(if (map? %) % {:white % :black %}))
                                   (if side [side] game/sides))))
      (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 (chessmate-2005/" cmd " !game :black …)")}))))
    
    (defn level!
      "Set Chessmate's search depth, the original's \"AI Search Depth\", from 1 to 64, or no limit,
      with 0 or nil: for both sides, or for side, :white or :black. The 2005 game offered 1 to 6.
      With no limit, or a deep one, it deepens until its time is up: a minute a move without a
      clock, and what the clock allows with one. Returns the depth, 0 for no limit."
      ([a n] (level! a nil n))
      ([a side n]
       (this-game a "level!")
       (when side (side-arg "level!" side))
       (when-not (or (nil? n) (and (integer? n) (<= 0 n 64)))
         (throw (ex-info (str "There is no level " (pr-str n)) {:pt/hint "a depth from 1 to 64, as (chessmate-2005/level! !game 4), or 0 for no limit"})))
       (let [n (or n 0)]
         (set-side! :level side n)
         n)))
    
    (defn difficulty!
      "Set how hard Chessmate plays, the page's Difficulty (owner, 2026-09-28): :novice, a depth of
      2 and no book; :easy, 4 and no book, as it plays unless told otherwise; :medium, 6; :hard, 8;
      or :chessmate, no limit, deepening for up to a minute a move without a clock; the last three
      from its book. For the side Chessmate plays against you, or for both sides when it plays both
      or neither, or for side, :white or :black. Returns the difficulty."
      ([a level] (difficulty! a nil level))
      ([a side level]
       (this-game a "difficulty!")
       (when side (side-arg "difficulty!" side))
       (let [settings (get difficulties level)]
         (when-not settings
           (throw (ex-info (str "There is no difficulty " (pr-str level))
                           {:pt/hint (str "a difficulty, " (str/join ", " (map str (keys difficulties))) ", as (chessmate-2005/difficulty! !game :easy)")})))
         (let [side (or side (some-> (game/human-side @!game) game/other))
               per-side #(if (map? %) % {:white % :black %})]
           (swap! !game (fn [g] (reduce (fn [g s] (-> g (assoc-in [:level s] (:level settings)) (assoc-in [:book s] (:book settings))))
                                        (-> g (update :level per-side) (update :book per-side))
                                        (if side [side] game/sides))))
           (save!)
           (draw!)
           (rethink! side))
         level)))
    
    (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 (chessmate-2005/book! !game false), or for one, as (chessmate-2005/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 "(chessmate-2005/book! !game :white false) or (chessmate-2005/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 "(chessmate-2005/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) :book (:book g) :clock (:clock g)})
                     (catch :default e
                       (throw (ex-info (ex-message e) {:pt/hint "(chessmate-2005/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] (println (game/pgn (value g) (game/today)))))
    
    (defn evaluate
      "The 2005 heuristic's verdict on the position: the side it favours, :white, :black or
      :neither, and its score from White's point of view, as the page shows scores, a pawn being
      worth 5. Then its terms, from White's point of view too, which add up to the score as
      material + 0.333 x (control + centre); and the side to move. Of the page's game, or of g, a
      game or its atom."
      ([] (evaluate !game))
      ([g]
       (let [g (value g)
             p (game/position g)
             {:keys [material control centre]} (eval/explain p)
             score (white-score p)
             shown #(/ (js/Math.round (* % 1000)) 1000)] ; to 3 decimals: the exact floats print as 1.3320000171661377
         {:winning (winning score)
          :score (shown score)
          :material (shown material)
          :control (shown control)
          :centre (shown centre)
          :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-2005
    
    (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!)
      (.unmount (host/chess-host)))
    
    (defn start!
      "Show the game on /chessmate-2005: 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 {:book false})))) ; Easy, the Difficulty it opens at
      (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))) (think!)))))
    chessmate.worker-main · 6 lines
    (ns chessmate.worker-main
      "The compiled worker's entry point. Never evaluated on the page."
      (:require
       [chessmate.worker :as worker]))
    
    (worker/start!)