Chessmate 2026

Chessmatewarming the interpreter…

You

Moves

    2,690–2,760 Elo on CCRL's Blitz scale · how it was rated

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

    The same engine in ClojureScript: · the 2005 engine:

    Settings

    What's New in Chessmate 2026

    The post: · this engine:

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

    In WebAssembly

    How It Was Rated

    Chessmate 2026 is rated 2,724 on the scale of CCRL's Blitz list, with a 95% range of 2,689 to 2,756. That comes from 330 games against 11 engines on that list, played by its rules. CCRL rates engines, not people, so its scale is not FIDE's.

    The games were played by its ClojureScript build, now /chessmate-cljs, before the port. On one core this build plays the same moves from the same number of positions searched, about twice as fast, and on every core it searches more still, so on the same clock it looks further ahead. It has not been rated on its own.

    EngineByCCRL BlitzResultScorePerformance
    Floyd 0.9 · CMarcel van Kervinck2,583 ± 13+16 =9 −568%2,717 ± 109
    Lozza 2.5 · JSColin Jenkins2,668 ± 13+10 =14 −657%2,715 ± 92
    Stash 21.0 · CMorgan Houppin2,713 ± 14+6 =15 −945%2,678 ± 89
    K2 0.99 · C++Sergey Meus2,750 ± 10+10 =11 −952%2,762 ± 99
    Octochess r5506 · C++Tim Kosse2,804 ± 10+12 =9 −955%2,839 ± 105
    Clovis III · C++Jonathan McDermid2,829 ± 10+3 =15 −1235%2,721 ± 88
    OliThink 5.11.9 · COliver Brausch2,902 ± 14+2 =6 −2217%2,622 ± 134
    Stash 25.0 · CMorgan Houppin2,933 ± 18+3 =7 −2022%2,710 ± 124
    tomitankChess 5.1 · JSTamás Kuzmics2,943 ± 12+4 =11 −1532%2,809 ± 102
    Stash 27.0 · CMorgan Houppin3,049 ± 16+1 =4 −2510%2,667 ± 165
    Monolith 2.01 · C++Jonas Mayr3,061 ± 9+0 =3 −275%2,549 ± 197

    Each engine's language after its name · CCRL Blitz: its rating on the list, with its 95% error, read on 26 September 2026 · result and score: Chessmate 2026's · performance: the rating Chessmate's score against it implies

    How the Games Were Played

    How the Rating Was Found

    Before This

    These are replaced. The first rested on four versions of one engine, from openings in Chessmate's own book. Stockfish's limited levels were fitted to CCRL's list in January 2023, and they rate Chessmate higher than the engines on the list itself do.

    Source

    The engine: 6 files of Rust, 3654 lines, then the worker that runs it and the crate's manifest · the game, in ClojureScript: /chessmate-cljs

    src/lib.rs · 294 lines
    //! Chessmate 2026 in WebAssembly: chessmate.v2's board, evaluation and search, ported from ClojureScript to Rust, which
    //! /chessmate's background worker runs (worker.js). The search is the ClojureScript's step for step, and plays
    //! node for node as it does; it is only faster.
    //!
    //! The module's interface is plain numbers and one buffer of bytes, `io`, for text both ways: a position in FEN and
    //! moves in UCI go in, and reports come out as JSON. It needs two functions of its host: `now`, the clock in
    //! milliseconds, which the search reads every 256 nodes, and `on_iteration`, which is handed each completed iteration's
    //! report while the search goes on.
    //!
    //! It is built twice from this source (scripts/chess/build.mjs): for one thread, chessmate.wasm, and with atomics and
    //! shared memory, chessmate-threads.wasm, which searches on every core (threads.rs). That build imports its memory,
    //! which the worker shares with the helpers' workers, and a third function of its host, `wake`; and it exports what the
    //! helpers need: `stack`, `control`, `helpers` and `help`, and each helper's stack pointer.
    
    mod board;
    mod eval;
    mod pesto;
    mod search;
    mod threads;
    
    use std::cell::UnsafeCell;
    use std::fmt::Write;
    
    use board::uci;
    use search::{Engine, Options, Report};
    
    extern "C" {
        /// The clock, in milliseconds.
        fn now() -> f64;
        /// A completed iteration's report, as JSON, len bytes of io.
        fn on_iteration(len: usize);
    }
    
    fn clock() -> f64 {
        // Safety: the host's function takes no arguments and returns a number
        unsafe { now() }
    }
    
    /// The size of io, which holds the longest line a search reports many times over.
    const IO_SIZE: usize = 65536;
    
    struct Machine {
        engine: Engine,
        io: Box<[u8; IO_SIZE]>,
    }
    
    struct Global(UnsafeCell<Option<Machine>>);
    
    // Safety: only the worker's own thread, the main thread, reaches the machine, and the host enters the module there once
    // at a time: on_iteration's host only reads memory, and calls nothing back. Helper threads enter only `help`, which
    // keeps to their own engines (threads.rs).
    unsafe impl Sync for Global {}
    
    static MACHINE: Global = Global(UnsafeCell::new(None));
    
    fn machine() -> &'static mut Machine {
        // Safety: see Global
        unsafe {
            (*MACHINE.0.get()).get_or_insert_with(|| Machine {
                engine: Engine::new(clock),
                io: vec![0; IO_SIZE].into_boxed_slice().try_into().unwrap(),
            })
        }
    }
    
    /// Write text into io, cut to its size, and return its length.
    fn put(io: &mut [u8; IO_SIZE], text: &str) -> usize {
        let n = text.len().min(IO_SIZE);
        io[..n].copy_from_slice(&text.as_bytes()[..n]);
        n
    }
    
    /// The text the host wrote into io.
    fn text(io: &[u8; IO_SIZE], len: usize) -> String {
        String::from_utf8_lossy(&io[..len.min(IO_SIZE)]).into_owned()
    }
    
    /// A search's report, as JSON with the fields of chessmate.v2.worker's replies but their type and id.
    fn json(r: &Report) -> String {
        let mut s = String::with_capacity(256);
        let opt = |v: Option<i32>| v.map_or("null".to_string(), |v| v.to_string());
        let mv = r.mv.map_or("null".to_string(), |m| format!("\"{}\"", uci(m)));
        let pv: Vec<String> = r.pv.iter().map(|&m| format!("\"{}\"", uci(m))).collect();
        let _ = write!(
            s,
            "{{\"move\":{},\"score\":{},\"draw\":{},\"mate\":{},\"depth\":{},\"reached\":{},\"pv\":[{}],\"nodes\":{},\"nps\":{},\"ms\":{},\"budget\":{}}}",
            mv,
            r.score,
            opt(r.draw),
            opt(r.mate),
            r.depth,
            r.seldepth,
            pv.join(","),
            r.nodes,
            r.nps as i64,
            r.ms as i64,
            r.budget.map_or("null".to_string(), |b| (b as i64).to_string())
        );
        s
    }
    
    /// Where io is: the host writes text there before a call that reads it, and reads what a call wrote.
    #[no_mangle]
    pub extern "C" fn io() -> *mut u8 {
        machine().io.as_mut_ptr()
    }
    
    /// Set the board to the position in FEN, len bytes of io. Returns 0, or when the FEN is refused, minus the length of
    /// the reason, written into io.
    #[no_mangle]
    pub extern "C" fn set_position(len: usize) -> i32 {
        let Machine { engine, io } = machine();
        match engine.b.set_position(&text(io, len)) {
            Ok(()) => 0,
            Err(why) => -(put(io, &why) as i32),
        }
    }
    
    /// Play the move in UCI, len bytes of io. Returns 0, or when it is not a legal move, minus the length of the reason,
    /// written into io.
    #[no_mangle]
    pub extern "C" fn play(len: usize) -> i32 {
        let Machine { engine, io } = machine();
        match engine.b.play_uci(&text(io, len)) {
            Ok(()) => 0,
            Err(why) => -(put(io, &why) as i32),
        }
    }
    
    /// Chessmate's move for the position on the board, as chessmate.v2.search/think finds it, with its options, NaN for an
    /// option not given. Each completed iteration's report goes to on_iteration; the last report is written into io as
    /// JSON, and its length returned.
    #[no_mangle]
    #[allow(clippy::too_many_arguments)]
    pub extern "C" fn think(
        ms: f64,
        max_ms: f64,
        min_ms: f64,
        depth: f64,
        ply_limit: f64,
        nodes: f64,
        contempt: f64,
        trades: f64,
        self_play: i32,
    ) -> usize {
        let given = |x: f64| if x.is_nan() { None } else { Some(x) };
        let options = Options {
            ms,
            max_ms: given(max_ms),
            min_ms: given(min_ms),
            depth: depth as i32,
            ply_limit: given(ply_limit).map(|p| p as i32),
            nodes: given(nodes),
            contempt: contempt as i32,
            trades: trades as i32,
            self_play: self_play != 0,
        };
        let Machine { engine, io } = machine();
        let r = engine.think(&options, &mut |r: &Report| {
            let len = put(io, &json(r));
            // Safety: the host reads len bytes of io, and calls nothing back
            unsafe { on_iteration(len) }
        });
        put(io, &json(&r))
    }
    
    /// The evaluation of the position on the board, for the side to move, in centipawns.
    #[no_mangle]
    pub extern "C" fn evaluate() -> i32 {
        let Machine { engine, .. } = machine();
        engine.e.evaluate(&engine.b)
    }
    
    /// The number of move sequences of depth moves from the position on the board.
    #[no_mangle]
    pub extern "C" fn perft(depth: i32) -> f64 {
        machine().engine.b.perft(depth) as f64
    }
    
    /// Forget everything learnt: the table, the history, the killers and the countermoves, the helpers' too.
    #[no_mangle]
    pub extern "C" fn clear() {
        machine().engine.s.clear();
        threads::clear();
    }
    
    /// Give the transposition table 2^bits entries of 16 bytes each: at most 1 GB, or 128 MB when threaded, whose memory
    /// can grow to 256 MB only (build.mjs).
    #[no_mangle]
    pub extern "C" fn resize(bits: i32) {
        let most = if cfg!(feature = "threads") { 23 } else { 26 };
        machine().engine.s.resize(bits.clamp(1, most));
    }
    
    /// The position on the board in FEN, written into io; returns its length.
    #[no_mangle]
    pub extern "C" fn fen() -> usize {
        let Machine { engine, io } = machine();
        put(io, &engine.b.fen())
    }
    
    /// The legal moves on the board in UCI, in generation order, separated by spaces, written into io; returns the length.
    #[no_mangle]
    pub extern "C" fn legal() -> usize {
        let Machine { engine, io } = machine();
        let moves: Vec<String> = engine.b.legal_moves().into_iter().map(uci).collect();
        put(io, &moves.join(" "))
    }
    
    /// The late move reductions' table, 64 by 64, for the tests, which compare it with JavaScript's.
    #[no_mangle]
    pub extern "C" fn lmr() -> *const i32 {
        machine().engine.s.lmr().as_ptr()
    }
    
    /// What only the threaded build has: the helpers' side of the interface, and an allocator for many threads.
    #[cfg(feature = "threads")]
    mod threaded {
        use std::alloc::{GlobalAlloc, Layout, System};
        use std::hint::spin_loop;
        use std::sync::atomic::{
            AtomicBool,
            Ordering::{Acquire, Relaxed, Release},
        };
    
        use crate::{clock, threads};
    
        /// The worker has started n helpers, threads 1 to n, which join each search from now on.
        #[no_mangle]
        pub extern "C" fn helpers(n: i32) {
            threads::set_helpers(n);
        }
    
        /// A new stack of bytes for a helper, which its worker points its stack pointer at: the address of its top, where
        /// a stack starts, 16-byte aligned as WebAssembly's C ABI wants it. Kept for the module's life.
        #[no_mangle]
        pub extern "C" fn stack(bytes: usize) -> usize {
            let layout = Layout::from_size_align(bytes, 16).unwrap();
            // Safety: the layout's size is not 0
            let bottom = unsafe { std::alloc::alloc(layout) };
            if bottom.is_null() {
                std::alloc::handle_alloc_error(layout);
            }
            bottom as usize + bytes
        }
    
        /// Where the words the workers and the page reach are: threads.rs's CONTROL, SEQ, PULSE, SEARCH and ABANDONED, four
        /// 32-bit words in that order.
        #[no_mangle]
        pub extern "C" fn control() -> usize {
            threads::control() as usize
        }
    
        /// On helper thread t: its part in the search under way, if any. Returns when the search has finished.
        #[no_mangle]
        pub extern "C" fn help(t: usize) {
            threads::help(t, clock);
        }
    
        /// Rust's allocator for WebAssembly, dlmalloc, taken in turns under a spin lock: the standard library is prebuilt for
        /// one thread, so its allocator has no lock of its own. The search allocates only between iterations, and a helper
        /// only as it first joins, so the lock is rarely waited for.
        struct Locked;
    
        static LOCK: AtomicBool = AtomicBool::new(false);
    
        fn locked<T>(f: impl FnOnce() -> T) -> T {
            while LOCK.compare_exchange_weak(false, true, Acquire, Relaxed).is_err() {
                spin_loop();
            }
            let r = f();
            LOCK.store(false, Release);
            r
        }
    
        // Safety: System's own contracts, one thread at a time
        unsafe impl GlobalAlloc for Locked {
            unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
                locked(|| System.alloc(layout))
            }
            unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
                locked(|| System.alloc_zeroed(layout))
            }
            unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
                locked(|| System.dealloc(p, layout))
            }
            unsafe fn realloc(&self, p: *mut u8, layout: Layout, size: usize) -> *mut u8 {
                locked(|| System.realloc(p, layout, size))
            }
        }
    
        #[global_allocator]
        static ALLOCATOR: Locked = Locked;
    }
    src/search.rs · 1265 lines
    //! Chessmate 2026's search, ported from chessmate.v2.search: negamax, as the 2005 search was, each side maximising the
    //! negation of the other's score, with alpha-beta cutoffs, and the techniques chess programs have found since, each
    //! named where it is used:
    //!
    //!   principal variation search   the first move with the full window, the rest with a null window, searched again
    //!                                only if they beat it
    //!   transposition table          positions met before, by Zobrist hash, with their scores, depths and best moves
    //!   iterative deepening          depth 1, 2, 3, ... until the time is spent, each iteration ordering the next,
    //!                                within an aspiration window
    //!   quiescence search            at the horizon, captures until the position is quiet, so that no exchange is cut
    //!                                off half way
    //!   move ordering                the table's move, good captures by most valuable victim and least valuable
    //!                                attacker, killer moves, the countermove, and the history of quiet moves that caused
    //!                                cutoffs
    //!   pruning and reductions       null move, reverse futility, futility, late move pruning, static exchange pruning
    //!                                and late move reductions
    //!   extensions                   a check is searched a ply deeper
    //!   draws                        repetition, the fifty-move rule and insufficient material
    //!
    //! Scores are in centipawns for the side to move. A mate in n plies is MATE - n.
    //!
    //! Every step is the ClojureScript's, in its order, so that the two search node for node alike: the same moves, scores
    //! and node counts at a fixed depth or node count, and the same table and history from one search to the next.
    //!
    //! Where the page may share memory between workers, the search also runs on every other core, as helper threads that
    //! search the same position into the same table (threads.rs, and `help` below). Alone, it is the ClojureScript's.
    
    use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
    use std::sync::Arc;
    
    use crate::board::{move_from, move_to, promotion, uci, Board, EN_PASSANT_FLAG, KING, PAWN, QUEEN, SEE_VALUES};
    use crate::eval::Eval;
    use crate::threads;
    
    pub const INFINITY: i32 = 32000;
    pub const MATE: i32 = 31000;
    /// Any score beyond this is a mate.
    pub const MATE_BOUND: i32 = 30000;
    pub const MAX_PLY: i32 = 96;
    const MAX_MOVES: i32 = 256;
    /// What each side's last search leaves its next before there is one: see Search's previous.
    const FRESH: [f64; 6] = [
        INFINITY as f64,
        INFINITY as f64,
        0.85,
        INFINITY as f64,
        INFINITY as f64,
        0.85,
    ];
    
    // Bounds, as the transposition table records them
    /// The score is at most this: every move failed low.
    const UPPER: i32 = 1;
    /// At least this: a move failed high.
    const LOWER: i32 = 2;
    const EXACT: i32 = 3;
    
    /// The transposition table's words, four per entry (see Search's tt), which every thread of a search reads and writes
    /// at once. Each word is read and written whole, and the first is kept XOR'd with the other three, so that an entry
    /// read while another thread was writing it, part old and part new, no longer matches its position and counts as a
    /// miss, not as another position's score (Hyatt's lockless hashing). A thread alone finds each entry as it left it.
    pub struct Table {
        words: Box<[AtomicI32]>,
    }
    
    impl Table {
        /// A table of n words, all 0.
        fn new(n: usize) -> Table {
            let words = vec![0i32; n].into_boxed_slice(); // zeroed as allocated, not word by word
            let len = words.len();
            // Safety: AtomicI32 has the size, alignment and bit validity of i32
            let words = unsafe {
                Box::from_raw(std::ptr::slice_from_raw_parts_mut(
                    Box::into_raw(words) as *mut AtomicI32,
                    len,
                ))
            };
            Table { words }
        }
    
        #[inline(always)]
        fn get(&self, i: usize) -> i32 {
            self.words[i].load(Relaxed)
        }
    
        #[inline(always)]
        fn set(&self, i: usize, v: i32) {
            self.words[i].store(v, Relaxed)
        }
    
        /// Entry e's words: the position's key (the first word unXOR'd), the move, the score and static evaluation, and the
        /// depth, bound and generation.
        #[inline(always)]
        fn entry(&self, e: usize) -> (i32, i32, i32, i32) {
            let (w, m, packed, info) = (self.get(e), self.get(e + 1), self.get(e + 2), self.get(e + 3));
            (w ^ m ^ packed ^ info, m, packed, info)
        }
    }
    
    /// What a helper thread takes from the main thread's search as each search starts (threads.rs): the table, and the
    /// settings that decide what the table's entries mean, so that the threads' entries agree.
    #[cfg_attr(not(feature = "threads"), allow(dead_code))] // only the threaded build has helpers
    #[derive(Clone)]
    pub struct Settings {
        tt: Arc<Table>,
        mask: i32,
        generation: i32,
        contempt: i32,
        trades: i32,
        key_lo: i32,
        key_hi: i32,
        ply_limit: i32,
        /// The deepest iteration.
        depth: i32,
    }
    
    /// Which iterations helper thread t skips, so that the helpers spread across the depths, ahead of the main thread and
    /// behind it, rather than all search the same iteration: Stockfish 9's pattern, which repeats every 20 helpers.
    #[cfg_attr(not(feature = "threads"), allow(dead_code))] // only the threaded build has helpers
    fn skips(t: usize, depth: i32) -> bool {
        const SIZE: [i32; 20] = [1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4];
        const PHASE: [i32; 20] = [0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 7];
        let i = (t - 1) % 20;
        (depth + PHASE[i]) / SIZE[i] % 2 != 0
    }
    
    /// The search's state, as chessmate.v2.search keeps it in typed arrays.
    pub struct Search {
        /// Each ply's moves, from ply * MAX_MOVES.
        moves: Vec<i32>,
        /// The moves' ordering scores.
        scores: Vec<i32>,
        /// Each ply's quiet moves tried, from ply * 64, for the history's malus.
        quiets: Vec<i32>,
        /// Two quiet moves per ply that caused cutoffs there.
        killers: Vec<i32>,
        /// Quiet moves' success, by side * 16384 + from * 128 + to.
        history: Vec<i32>,
        /// The quiet move that refuted each move, by its piece * 128 + its square.
        counters: Vec<i32>,
        /// The move made at each ply, 0 for a pass.
        played: Vec<i32>,
        /// The static evaluation at each ply, for improving.
        evals: Vec<i32>,
        /// The principal variation from each ply, from ply * MAX_PLY.
        pv: Vec<i32>,
        /// Where each ply's variation ends.
        pv_length: Vec<i32>,
        stopped: bool,
        seldepth: i32,
        generation: i32,
        mask: i32,
        contempt: i32,
        /// The best root move of the iteration under way, once one is known, and its score.
        root_move: i32,
        root_score: i32,
        /// The deepest ply any line may reach, captures and checks included.
        ply_limit: i32,
        /// What trading down costs the side to move at the root: think's trades.
        trades: i32,
        /// XOR'd into the hash's halves for the table: see think's self_play.
        key_lo: i32,
        key_hi: i32,
        nodes: u64,
        /// The clock's reading at which the search stops.
        hard: f64,
        max_nodes: f64,
        /// How often the root's best move has changed in the iteration under way.
        changes: i32,
        /// The root's legal moves, as legal_moves gives them; the nodes each one's searches have taken in this search,
        /// Stockfish's effort; and each one's score averaged over this search's iterations, each weighted by the nodes it
        /// took, Stockfish's averageScore, -INFINITY before its first.
        root_moves: Vec<i32>,
        effort: Vec<u64>,
        average: Vec<i32>,
        /// What each side's last search leaves its next, as Stockfish keeps it between moves: three numbers a side,
        /// White's first. The score of the move played, and its average score, INFINITY before any; and how settled its
        /// best move was, the time reduction, 0.85 before any.
        previous: [f64; 6],
        /// The transposition table, allocated by the first search that needs it. Four numbers per entry, indexed by the
        /// hash's low half: the hash's high half, to tell positions apart; the best move; the score (low 16 bits) and the
        /// static evaluation (high 16); and the depth + 1 (8 bits), the bound (2 bits) and the search's generation. When one
        /// table serves both sides of a game, Black's searches read and write it by the hash's halves XOR'd with a constant
        /// each, so that neither side meets the other's entries. The helper threads share it (Table).
        tt: Arc<Table>,
        /// Late move reductions, by depth * 64 + the move's number: more for later moves and deeper searches.
        lmr: Vec<i32>,
    }
    
    /// What `think` is asked, as chessmate.v2.search/think's options.
    pub struct Options {
        /// The time to aim for, in milliseconds.
        pub ms: f64,
        /// The time never to exceed: the search stops then, even part way through an iteration. None is ms.
        pub max_ms: Option<f64>,
        /// The time to think at least: until then only the depth ends the search. None is none.
        pub min_ms: Option<f64>,
        /// The deepest iteration.
        pub depth: i32,
        /// The deepest ply any line may reach, the captures and checks followed beyond the iteration's depth included: 95,
        /// the most the search's arrays hold, when None. An iteration goes no deeper than it.
        pub ply_limit: Option<i32>,
        /// The most nodes to search, for tests that must not depend on the clock.
        pub nodes: Option<f64>,
        /// What a draw costs the side to move, in centipawns.
        pub contempt: i32,
        /// What trading down costs it, in centipawns for all the pieces.
        pub trades: i32,
        /// True when the table serves both sides of one game, each searching in turn, as when Chessmate plays itself.
        pub self_play: bool,
    }
    
    /// A search's report, as think returns it: move None when there is no legal move, draw None then too.
    #[derive(Clone)]
    pub struct Report {
        pub mv: Option<i32>,
        pub score: i32,
        pub draw: Option<i32>,
        pub mate: Option<i32>,
        pub depth: i32,
        pub seldepth: i32,
        pub pv: Vec<i32>,
        pub nodes: u64,
        pub ms: f64,
        pub nps: f64,
        /// The time the search gave itself by the iteration reported, in ms, or None before one completes.
        pub budget: Option<f64>,
    }
    
    /// JavaScript's Math.round, for the times the report gives: to the nearer whole number, halves up.
    fn js_round(x: f64) -> f64 {
        (x + 0.5).floor()
    }
    
    /// A score for the table: a mate counted from this position, not from the root.
    fn to_table(score: i32, ply: i32) -> i32 {
        if score >= MATE_BOUND {
            score + ply
        } else if score <= -MATE_BOUND {
            score - ply
        } else {
            score
        }
    }
    
    fn from_table(score: i32, ply: i32) -> i32 {
        if score >= MATE_BOUND {
            score - ply
        } else if score <= -MATE_BOUND {
            score + ply
        } else {
            score
        }
    }
    
    /// Moves to mate for a mate score, negative when the side to move is mated; else None.
    fn mate_in(score: i32) -> Option<i32> {
        if score >= MATE_BOUND {
            Some((MATE - score + 1) / 2)
        } else if score <= -MATE_BOUND {
            Some(-((MATE + score) / 2))
        } else {
            None
        }
    }
    
    /// Stockfish's score units in a centipawn: its pawn is worth 208 of them (PawnValue), and Chessmate's a hundred
    /// centipawns, so that its constants read Chessmate's scores as its own.
    const SF_UNITS: f64 = 2.08;
    
    /// Stockfish's interpolate: the straight line through (x0, y0) and (x1, y1), at x.
    fn interpolate(x: f64, x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
        y0 + (y1 - y0) * (x - x0) / (x1 - x0)
    }
    
    impl Search {
        pub fn new() -> Search {
            let (plies, moves) = (MAX_PLY as usize, MAX_MOVES as usize);
            let mut lmr = vec![0; 4096];
            for d in 1..64 {
                for n in 1..64 {
                    lmr[d * 64 + n] = (0.75 + (d as f64).ln() * (n as f64).ln() / 2.25).floor() as i32;
                }
            }
            Search {
                moves: vec![0; plies * moves],
                scores: vec![0; plies * moves],
                quiets: vec![0; plies * 64],
                killers: vec![0; plies * 2],
                history: vec![0; 32768],
                counters: vec![0; 2048],
                played: vec![0; plies],
                evals: vec![0; plies],
                pv: vec![0; plies * plies],
                pv_length: vec![0; plies],
                stopped: false,
                seldepth: 0,
                generation: 0,
                mask: 0,
                contempt: 0,
                root_move: 0,
                root_score: 0,
                ply_limit: 0,
                trades: 0,
                key_lo: 0,
                key_hi: 0,
                nodes: 0,
                hard: 0.0,
                max_nodes: 0.0,
                changes: 0,
                root_moves: Vec::with_capacity(moves),
                effort: vec![0; moves],
                average: vec![0; moves],
                previous: FRESH,
                tt: Arc::new(Table::new(0)),
                lmr,
            }
        }
    
        /// Give the transposition table 2^bits entries of 16 bytes each: 20 bits is 16 MB.
        pub fn resize(&mut self, bits: i32) {
            self.tt = Arc::new(Table::new(4 << bits));
            self.mask = (1 << bits) - 1;
        }
    
        /// Forget everything learnt: the table, the history, the killers and the countermoves, and what each side's last
        /// search left its next.
        pub fn clear(&mut self) {
            for w in self.tt.words.iter() {
                w.store(0, Relaxed);
            }
            self.history.fill(0);
            self.killers.fill(0);
            self.counters.fill(0);
            self.previous = FRESH;
        }
    
        /// The late move reductions, for the tests, which compare them with JavaScript's.
        pub fn lmr(&self) -> &[i32] {
            &self.lmr
        }
    }
    
    /// The board, the evaluation and the search, which the search's functions all reach, as chessmate.v2's namespaces reach
    /// each other's state; and the clock.
    pub struct Engine {
        pub b: Board,
        pub e: Eval,
        pub s: Search,
        /// The clock, in milliseconds.
        pub clock: fn() -> f64,
        /// Which thread searches with this engine: 0 the main thread, which keeps the time and reports, 1 and up a helper.
        pub thread: usize,
        /// A helper's last sight of its main thread's heartbeat: the beat, and when it changed (threads.rs).
        pulse: (i32, f64),
    }
    
    impl Engine {
        pub fn new(clock: fn() -> f64) -> Engine {
            Engine {
                b: Board::new(),
                e: Eval::new(),
                s: Search::new(),
                clock,
                thread: 0,
                pulse: (0, 0.0),
            }
        }
    
        /// The engine of helper thread t.
        #[cfg_attr(not(feature = "threads"), allow(dead_code))] // only the threaded build has helpers
        pub fn helper(clock: fn() -> f64, t: usize) -> Engine {
            Engine {
                thread: t,
                ..Engine::new(clock)
            }
        }
    
        /// Record a search's result for the position on the board. A deeper result for another position is kept unless it
        /// is from an earlier search.
        fn store(&mut self, depth: i32, bound: i32, score: i32, static_eval: i32, m: i32, ply: i32) {
            let s = &self.s;
            let hi = self.b.hash_hi ^ s.key_hi;
            let e = 4 * ((self.b.hash_lo ^ s.key_lo) & s.mask) as usize;
            let (key, old, _, info) = s.tt.entry(e);
            let generation = s.generation;
            let same = key == hi;
            if info == 0 || info >> 10 != generation || bound == EXACT || depth + 3 >= (info & 255) - 1 {
                let m = if !same || m != 0 { m } else { old };
                let packed = (to_table(score, ply) & 0xFFFF) | (static_eval << 16);
                let info = (depth + 1) | (bound << 8) | (generation << 10);
                s.tt.set(e + 1, m);
                s.tt.set(e + 2, packed);
                s.tt.set(e + 3, info);
                s.tt.set(e, hi ^ m ^ packed ^ info);
            }
        }
    
        // Counting nodes, and the clock
    
        /// One more node; every 256 a look at the clock and the node limit.
        #[inline(always)]
        fn count_node(&mut self) {
            self.s.nodes += 1;
            let n = self.s.nodes;
            if n & 255 == 0 && self.must_stop(n) {
                self.s.stopped = true;
            }
        }
    
        /// Whether the search must stop, n nodes in: the main thread's once the page has given it up, or past its node
        /// limit or its time, a heartbeat given first; a helper's once the main thread has finished, or the page has given the
        /// search up, or the main thread has stopped beating (its worker ended, every 16,384 nodes a look), its count left
        /// for the main thread's reports first.
        fn must_stop(&mut self, n: u64) -> bool {
            if self.thread == 0 {
                threads::beat();
                threads::abandoned() || n as f64 >= self.s.max_nodes || (self.clock)() > self.s.hard
            } else {
                threads::counted(self.thread, n);
                threads::stopping() || (n & 16383 == 0 && !threads::beating(&mut self.pulse, (self.clock)()))
            }
        }
    
        /// A draw, as the side to move at ply sees it: the side to move at the root values it at -contempt, so that it
        /// plays on in level positions.
        fn draw_score(&self, ply: i32) -> i32 {
            if ply & 1 == 0 {
                -self.s.contempt
            } else {
                self.s.contempt
            }
        }
    
        /// The evaluation at ply, for the side to move; and when trading down costs the side to move at the root (think's
        /// trades), that much more for it for the pieces on the board, by their phase, in which a knight or bishop counts
        /// 1, a rook 2 and a queen 4 of 24. So a trade costs it its share, and a draw, which has no pieces to count, all of
        /// it. It depends only on the position, so the table's entries hold from one search to the next.
        fn static_eval(&mut self, ply: i32) -> i32 {
            let v = self.e.evaluate(&self.b);
            let trades = self.s.trades;
            if trades == 0 {
                v
            } else {
                let pieces = trades * 24.min(self.b.phase) / 24;
                if ply & 1 == 0 {
                    v + pieces
                } else {
                    v - pieces
                }
            }
        }
    
        // Move ordering
    
        /// The move that last refuted the move that led to ply, or 0.
        fn counter_move(&self, ply: i32) -> i32 {
            if ply == 0 {
                return 0;
            }
            let prev = self.s.played[(ply - 1) as usize];
            if prev == 0 {
                0
            } else {
                let to = move_to(prev);
                self.s.counters[(self.b.squares[to as usize] * 128 + to) as usize]
            }
        }
    
        /// How promising m looks, for ordering: the table's move first, then captures that do not lose material, by most
        /// valuable victim and least valuable attacker, then queen promotions, killer moves and the countermove, then quiet
        /// moves by their history, then captures that lose material, and under-promotions last.
        fn move_score(&mut self, m: i32, tt_move: i32, k1: i32, k2: i32, counter: i32, us: i32) -> i32 {
            let from = m & 127;
            let to = (m >> 7) & 127;
            let victim = self.b.squares[to as usize];
            let promo = promotion(m);
            if m == tt_move {
                30000000
            } else if victim != 0 || m & EN_PASSANT_FLAG != 0 {
                let v = if victim == 0 { PAWN } else { victim & 7 };
                let a = self.b.squares[from as usize] & 7;
                let order = v * 16 + (8 - a) + if promo == QUEEN { 64 } else { 0 };
                if SEE_VALUES[a as usize] <= SEE_VALUES[v as usize] || self.b.see(m) >= 0 {
                    20000000 + order
                } else {
                    -1000000 + order
                }
            } else if promo == QUEEN {
                19000000
            } else if promo > 0 {
                -2000000
            } else if m == k1 {
                15000000
            } else if m == k2 {
                14000000
            } else if m == counter {
                13000000
            } else {
                self.s.history[(us * 16384 + from * 128 + to) as usize]
            }
        }
    
        fn score_moves(&mut self, base: usize, end: usize, tt_move: i32, ply: i32) {
            let us = self.b.side;
            let k1 = self.s.killers[(ply * 2) as usize];
            let k2 = self.s.killers[(ply * 2 + 1) as usize];
            let counter = self.counter_move(ply);
            for i in base..end {
                let m = self.s.moves[i];
                self.s.scores[i] = self.move_score(m, tt_move, k1, k2, counter, us);
            }
        }
    
        /// Order captures by most valuable victim and least valuable attacker, for the quiescence search.
        fn score_captures(&mut self, base: usize, end: usize) {
            for i in base..end {
                let m = self.s.moves[i];
                let victim = self.b.squares[move_to(m) as usize];
                self.s.scores[i] = 16 * (if victim == 0 { PAWN } else { victim & 7 })
                    + (8 - (self.b.squares[move_from(m) as usize] & 7))
                    + if promotion(m) == QUEEN { 64 } else { 0 };
            }
        }
    
        /// The best-scored move from place i to end, swapped into place i.
        #[inline(always)]
        fn pick(&mut self, i: usize, end: usize) -> i32 {
            let scores = &mut self.s.scores;
            let mut best = i;
            for j in i + 1..end {
                if scores[j] > scores[best] {
                    best = j;
                }
            }
            scores.swap(best, i);
            self.s.moves.swap(best, i);
            self.s.moves[i]
        }
    
        /// A quiet move m caused a cutoff at ply: make it a killer and the countermove, raise its history and lower that of
        /// the quiet moves tried before it. The history saturates at +-16,384.
        fn reward(&mut self, m: i32, ply: i32, depth: i32, n_quiets: i32) {
            let us = self.b.side;
            let bonus = 1600.min(16 * depth * depth);
            let s = &mut self.s;
            let k = (ply * 2) as usize;
            if m != s.killers[k] {
                s.killers[k + 1] = s.killers[k];
                s.killers[k] = m;
            }
            if ply > 0 {
                let prev = s.played[(ply - 1) as usize];
                if prev != 0 {
                    let to = move_to(prev);
                    s.counters[(self.b.squares[to as usize] * 128 + to) as usize] = m;
                }
            }
            let i = (us * 16384 + move_from(m) * 128 + move_to(m)) as usize;
            let h = s.history[i];
            s.history[i] = h + (bonus - h * bonus / 16384);
            for j in 0..n_quiets {
                let q = s.quiets[(ply * 64 + j) as usize];
                if q != m {
                    let i = (us * 16384 + move_from(q) * 128 + move_to(q)) as usize;
                    let h = s.history[i];
                    s.history[i] = h - bonus - h * bonus / 16384;
                }
            }
        }
    
        /// m is the best move at ply so far: the variation from ply is m, then the child's.
        fn new_pv(&mut self, m: i32, ply: i32) {
            let s = &mut self.s;
            let row = (ply * MAX_PLY) as usize;
            let child = ((ply + 1) * MAX_PLY) as usize;
            let end = s.pv_length[(ply + 1) as usize];
            s.pv[row + ply as usize] = m;
            for i in (ply + 1)..end {
                s.pv[row + i as usize] = s.pv[child + i as usize];
            }
            s.pv_length[ply as usize] = end.max(ply + 1);
        }
    
        // The quiescence search
    
        /// The score of the position once the captures are played out: the side to move may stand on its static
        /// evaluation, or capture. Captures that lose material by the static exchange, or that could not lift the score to
        /// alpha even if the piece were free, are skipped. In check, every move is tried, as standing is no option.
        fn quiesce(&mut self, alpha: i32, beta: i32, ply: i32) -> i32 {
            self.s.pv_length[ply as usize] = ply;
            self.count_node();
            if ply > self.s.seldepth {
                self.s.seldepth = ply;
            }
            if ply >= self.s.ply_limit {
                return self.static_eval(ply);
            }
            let in_check = self.b.in_check();
            let stand = if in_check { -INFINITY } else { self.static_eval(ply) };
            if stand >= beta {
                return stand;
            }
            let base = (ply * MAX_MOVES) as usize;
            let end = self.b.generate(&mut self.s.moves, base, !in_check);
            if in_check {
                self.score_moves(base, end, 0, ply);
            } else {
                self.score_captures(base, end);
            }
            let mut alpha = alpha.max(stand);
            let mut best = stand;
            let mut legal = 0;
            for i in base..end {
                let m = self.pick(i, end);
                let victim = if m & EN_PASSANT_FLAG == 0 {
                    SEE_VALUES[self.b.squares[move_to(m) as usize] as usize]
                } else {
                    100
                };
                if !in_check
                    && promotion(m) != QUEEN
                    && (stand + victim + 200 < alpha
                        || (SEE_VALUES[self.b.squares[move_from(m) as usize] as usize] > victim && self.b.see(m) < 0))
                {
                    continue;
                }
                self.b.make(m);
                if !self.b.legal_after(m, in_check) {
                    self.b.unmake();
                    continue;
                }
                let score = -self.quiesce(-beta, -alpha, ply + 1);
                self.b.unmake();
                if self.s.stopped {
                    return 0;
                }
                if score >= beta {
                    return score;
                }
                if score > best {
                    if score > alpha {
                        self.new_pv(m, ply);
                    }
                    alpha = alpha.max(score);
                    best = score;
                }
                legal += 1;
            }
            if in_check && legal == 0 {
                -MATE + ply
            } else {
                best
            }
        }
    
        // The main search
    
        /// Search m's reply, m made: the first move with the full window; later ones with a null window, reduced when late
        /// and quiet, and searched again at full depth and then with the full window only if they beat alpha (principal
        /// variation search).
        #[allow(clippy::too_many_arguments)]
        fn child(
            &mut self,
            depth: i32,
            alpha: i32,
            beta: i32,
            ply: i32,
            pv: bool,
            legal: i32,
            quiet: bool,
            checks: bool,
            improving: bool,
            killer: bool,
            h: i32,
        ) -> i32 {
            let new_depth = depth - 1;
            if legal == 1 {
                return -self.search(new_depth, -beta, -alpha, ply + 1, pv, true);
            }
            let r = if quiet && depth >= 3 {
                let mut r = self.s.lmr[(depth.min(63) * 64 + legal.min(63)) as usize];
                if pv {
                    r -= 1;
                }
                if !improving {
                    r += 1;
                }
                if killer {
                    r -= 1;
                }
                if checks {
                    r -= 1;
                }
                r -= h / 5000;
                0.max(r.min(depth - 2))
            } else {
                0
            };
            let mut score = -self.search(new_depth - r, -(alpha + 1), -alpha, ply + 1, false, true);
            if score > alpha && r > 0 {
                score = -self.search(new_depth, -(alpha + 1), -alpha, ply + 1, false, true);
            }
            if pv && score > alpha && score < beta {
                -self.search(new_depth, -beta, -alpha, ply + 1, true, true)
            } else {
                score
            }
        }
    
        /// The null move: if passing still leaves the side to move above beta after a reduced search, a real move would
        /// too, and the node fails high without searching one.
        fn null_search(&mut self, depth: i32, beta: i32, ply: i32, static_eval: i32) -> i32 {
            let r = 3 + depth / 3 + 3.min((static_eval - beta) / 200);
            self.s.played[ply as usize] = 0;
            self.b.make_null();
            let score = -self.search(depth - 1 - r, -beta, 1 - beta, ply + 1, false, false);
            self.b.unmake_null();
            score
        }
    
        /// Has the side to move a piece besides pawns and its king? Without one, zugzwang is common and the null move
        /// unsound.
        fn non_pawn_material(&self) -> bool {
            let us = self.b.side;
            let base = (us * 16) as usize;
            (0..self.b.counts[us as usize] as usize).any(|i| {
                let k = self.b.squares[self.b.lists[base + i] as usize] & 7;
                k > PAWN && k < KING
            })
        }
    
        /// At the root, remember the best move so far and its score: a search stopped part way through an iteration may
        /// play it.
        fn root_move(&mut self, ply: i32, m: i32, score: i32) {
            if ply == 0 {
                self.s.root_move = m;
                self.s.root_score = score;
            }
        }
    
        /// After a search of root move m, as Stockfish's search keeps them: the nodes it took added to its effort, and its
        /// score to its average, weighted by those nodes against the ones before; and one more change of the best move,
        /// when a move after the first beats alpha.
        fn root_searched(&mut self, m: i32, score: i32, nodes: u64, changed: bool) {
            let s = &mut self.s;
            if let Some(i) = s.root_moves.iter().position(|&x| x == m) {
                let e = s.effort[i] + nodes;
                let before = (e - nodes).max(1);
                let w = ((64 * nodes) / (2 * nodes + 3 * before)).clamp(12, 24) as i64; // in 32nds
                let a = s.average[i];
                s.effort[i] = e;
                s.average[i] = if a == -INFINITY {
                    score
                } else {
                    (score as i64 * w + a as i64 * (32 - w)).div_euclid(32) as i32
                };
            }
            if changed {
                s.changes += 1;
            }
        }
    
        /// Try the moves in order, pruning what cannot matter, and return the best score. Stores the result in the table.
        #[allow(clippy::too_many_arguments)]
        fn moves_loop(
            &mut self,
            depth: i32,
            alpha: i32,
            beta: i32,
            ply: i32,
            pv: bool,
            in_check: bool,
            static_eval: i32,
            improving: bool,
            tt_move: i32,
        ) -> i32 {
            let base = (ply * MAX_MOVES) as usize;
            let end = self.b.generate(&mut self.s.moves, base, false);
            let us = self.b.side;
            let k1 = self.s.killers[(ply * 2) as usize];
            let k2 = self.s.killers[(ply * 2 + 1) as usize];
            let original_alpha = alpha;
            self.score_moves(base, end, tt_move, ply);
            let mut alpha = alpha;
            let mut best = -INFINITY;
            let mut best_move = 0;
            let mut legal = 0;
            let mut n_quiets = 0; // quiet moves seen, pruned or searched, for late-move pruning
            let mut n_tried = 0; // quiet moves searched, which the history's malus may lower
            for i in base..end {
                let m = self.pick(i, end);
                let to = move_to(m);
                let capture = self.b.squares[to as usize] != 0 || m & EN_PASSANT_FLAG != 0;
                let quiet = !capture && promotion(m) == 0;
                let prunable = !pv && !in_check && best > -MATE_BOUND;
                if prunable && capture && depth <= 6 && self.b.see(m) < -100 * depth {
                    continue; // a capture that loses too much
                }
                self.b.make(m);
                if !self.b.legal_after(m, in_check) {
                    self.b.unmake();
                    continue;
                }
                let checks = self.b.in_check();
                legal += 1;
                if prunable
                    && quiet
                    && !checks
                    && ((depth <= 8 && n_quiets >= (3 + depth * depth) / (if improving { 1 } else { 2 })) // late moves
                        || (depth <= 6 && static_eval + 100 + 100 * depth <= alpha))
                // futility
                {
                    self.b.unmake();
                    n_quiets += 1;
                    continue;
                }
                self.s.played[ply as usize] = m;
                let h = if quiet {
                    self.s.history[(us * 16384 + move_from(m) * 128 + to) as usize]
                } else {
                    0
                };
                let before = if ply == 0 { self.s.nodes } else { 0 };
                let score = self.child(
                    depth,
                    alpha,
                    beta,
                    ply,
                    pv,
                    legal,
                    quiet,
                    checks,
                    improving,
                    m == k1 || m == k2,
                    h,
                );
                self.b.unmake();
                if quiet && n_tried < 64 {
                    self.s.quiets[(ply * 64 + n_tried) as usize] = m;
                }
                if ply == 0 && !self.s.stopped {
                    let nodes = self.s.nodes - before;
                    self.root_searched(m, score, nodes, legal > 1 && score > alpha);
                }
                if self.s.stopped {
                    return 0;
                }
                if score >= beta {
                    if quiet {
                        self.reward(m, ply, depth, 64.min(n_tried + 1));
                    }
                    self.store(depth, LOWER, score, static_eval, m, ply);
                    self.root_move(ply, m, score);
                    return score;
                }
                if score > alpha {
                    self.new_pv(m, ply);
                    self.root_move(ply, m, score);
                    alpha = score;
                    best = score;
                    best_move = m;
                } else {
                    best = best.max(score);
                }
                if quiet {
                    n_quiets += 1;
                    n_tried += 1;
                }
            }
            if legal > 0 {
                self.store(
                    depth,
                    if alpha > original_alpha { EXACT } else { UPPER },
                    best,
                    static_eval,
                    best_move,
                    ply,
                );
                best
            } else if in_check {
                -MATE + ply // checkmated
            } else {
                self.draw_score(ply) // stalemate
            }
        }
    
        /// The score of the position on the board for the side to move, searched depth plies deep (plus extensions, less
        /// reductions), within the window alpha to beta, at ply plies from the root. pv is true on the principal variation,
        /// where no pruning is allowed; null_ok false forbids a second pass in a row.
        fn search(&mut self, depth: i32, alpha: i32, beta: i32, ply: i32, pv: bool, null_ok: bool) -> i32 {
            self.s.pv_length[ply as usize] = ply;
            if self.s.stopped {
                return 0; // the caller discards it
            }
            if ply > 0 && (self.b.repetition() || self.b.insufficient_material()) {
                return self.draw_score(ply);
            }
            // the fifty-move rule, unless the last move mated: as in the game, checkmate comes first
            if ply > 0 && self.b.halfmove >= 100 {
                return if self.b.in_check() && !self.b.has_legal_move() {
                    -MATE + ply
                } else {
                    self.draw_score(ply)
                };
            }
            if ply >= self.s.ply_limit {
                return self.static_eval(ply); // as deep as a line may go
            }
            // mate distance pruning: nothing here beats mating now
            let alpha = if ply > 0 { alpha.max(-MATE + ply) } else { alpha };
            let beta = if ply > 0 { beta.min(MATE - ply - 1) } else { beta };
            let in_check = self.b.in_check();
            let depth = if in_check { depth + 1 } else { depth }; // the check extension
            if alpha >= beta {
                return alpha;
            }
            if depth <= 0 {
                return self.quiesce(alpha, beta, ply);
            }
            self.count_node();
            let e = 4 * ((self.b.hash_lo ^ self.s.key_lo) & self.s.mask) as usize;
            let (key, entry_move, packed, info) = self.s.tt.entry(e);
            let hit = key == (self.b.hash_hi ^ self.s.key_hi) && info != 0;
            let tt_score = from_table((packed << 16) >> 16, ply);
            let tt_bound = (info >> 8) & 3;
            if hit
                && !pv
                && (info & 255) - 1 >= depth
                && (tt_bound == EXACT
                    || (tt_bound == LOWER && tt_score >= beta)
                    || (tt_bound == UPPER && tt_score <= alpha))
            {
                return tt_score; // the table knows
            }
            let static_eval = if in_check {
                -INFINITY
            } else if hit {
                packed >> 16
            } else {
                self.static_eval(ply)
            };
            let improving = !in_check && ply >= 2 && static_eval > self.s.evals[(ply - 2) as usize];
            let tt_move = if hit { entry_move } else { 0 };
            self.s.evals[ply as usize] = static_eval;
            // reverse futility: so far above beta that a quiet move would not bring it back
            if !pv
                && !in_check
                && depth <= 7
                && beta.abs() < MATE_BOUND
                && static_eval - depth * (if improving { 60 } else { 80 }) >= beta
            {
                return static_eval;
            }
            // the null move
            if !pv
                && !in_check
                && null_ok
                && depth >= 3
                && static_eval >= beta
                && beta.abs() < MATE_BOUND
                && self.non_pawn_material()
            {
                let score = self.null_search(depth, beta, ply, static_eval);
                if !self.s.stopped && score >= beta {
                    return beta;
                }
            }
            // internal iterative reduction: with no move from the table, search less deep
            let depth = if depth >= 4 && tt_move == 0 { depth - 1 } else { depth };
            self.moves_loop(depth, alpha, beta, ply, pv, in_check, static_eval, improving, tt_move)
        }
    
        // Iterative deepening
    
        /// One iteration, depth plies deep, from the root: within an aspiration window around the last iteration's score,
        /// prev, from depth 5, widened each time the score falls outside it. Returns the score, or 0 when stopped.
        fn aspire(&mut self, depth: i32, prev: i32) -> i32 {
            self.s.seldepth = 0;
            self.s.root_move = 0;
            let mut delta = 25;
            let mut alpha = if depth >= 5 {
                (-INFINITY).max(prev - delta)
            } else {
                -INFINITY
            };
            let mut beta = if depth >= 5 {
                INFINITY.min(prev + delta)
            } else {
                INFINITY
            };
            loop {
                let s = self.search(depth, alpha, beta, 0, true, false);
                if self.s.stopped {
                    return s;
                } else if s <= alpha {
                    let lower = (-INFINITY).max(s - 2 * delta);
                    beta = (alpha + beta) / 2;
                    alpha = lower;
                    delta *= 2;
                } else if s >= beta {
                    beta = INFINITY.min(s + 2 * delta);
                    delta *= 2;
                } else {
                    return s;
                }
            }
        }
    
        /// What the helper threads need of this search to join it, the deepest iteration depth: its table and settings.
        pub fn settings(&self, depth: i32) -> Settings {
            let s = &self.s;
            Settings {
                tt: s.tt.clone(),
                mask: s.mask,
                generation: s.generation,
                contempt: s.contempt,
                trades: s.trades,
                key_lo: s.key_lo,
                key_hi: s.key_hi,
                ply_limit: s.ply_limit,
                depth,
            }
        }
    
        /// A helper thread's part in a search (threads.rs): board's position, searched as the main thread searches it, with
        /// its settings and into its table, iteration after iteration from depth 1, each within its aspiration window, less
        /// the iterations this thread skips (skips), until the main thread has finished. It reports nothing: what it finds
        /// reaches the main thread through the table, where the main thread meets positions already searched, often deeper
        /// than it has yet looked. It keeps its own history, killers and countermoves, as the main thread does, and its own
        /// evaluation cache.
        #[cfg_attr(not(feature = "threads"), allow(dead_code))] // only the threaded build has helpers
        pub fn help(&mut self, board: &Board, settings: &Settings) {
            self.b.copy_from(board);
            let s = &mut self.s;
            s.tt = settings.tt.clone();
            s.mask = settings.mask;
            s.generation = settings.generation;
            s.contempt = settings.contempt;
            s.trades = settings.trades;
            s.key_lo = settings.key_lo;
            s.key_hi = settings.key_hi;
            s.ply_limit = settings.ply_limit;
            s.nodes = 0;
            s.max_nodes = f64::INFINITY;
            s.hard = f64::INFINITY;
            s.stopped = false;
            s.killers.fill(0);
            for h in s.history.iter_mut() {
                *h /= 2;
            }
            s.root_moves.clear(); // no time to keep, so no root moves' effort
            self.pulse = (-1, (self.clock)()); // the main thread beats from now
            let mut prev = 0;
            for d in 1..=settings.depth {
                if skips(self.thread, d) {
                    continue;
                }
                let score = self.aspire(d, prev);
                if self.s.stopped || threads::stopping() {
                    break;
                }
                prev = score;
            }
            threads::counted(self.thread, self.s.nodes);
        }
    
        /// What side's next search needs of this one, whose report is r, as Stockfish keeps it between moves: r's score, its
        /// move's average score, and how settled its best move was. Returns r, once any helper threads have stopped.
        fn finish(&mut self, r: Report, side: usize, level: i32, reduction: f64) -> Report {
            threads::stop();
            let s = &mut self.s;
            let a = s
                .root_moves
                .iter()
                .position(|&m| Some(m) == r.mv)
                .map_or(-INFINITY, |i| s.average[i]);
            s.previous[side] = r.score as f64;
            s.previous[side + 1] = if a == -INFINITY || mate_in(a).is_some() {
                a
            } else {
                a - level
            } as f64;
            s.previous[side + 2] = reduction;
            r
        }
    
        /// Chessmate 2026's move for the position on the board, which is unchanged afterwards, as chessmate.v2.search/think
        /// finds it. on_iteration is called with each completed iteration's report. After each iteration the search stops as
        /// Stockfish's does on a clock: once past the time to aim for, scaled up while the score falls and the best move keeps
        /// changing, and down while the best move holds and takes most of the search; at a forced move, after 500 ms at most;
        /// and once it mates within three plies, or is mated in two; but not before the least time, unless at its depth.
        /// Returns the report of the last completed iteration, or of the iteration under way if it had already found a
        /// better move: the move, the score in centipawns for the side to move, the mate, the depth, the deepest ply reached,
        /// the line, the nodes, the time, the speed and the time it gave itself; and the score a draw gets, which a line
        /// ending in one scores. Scores count what trading down has cost since the root, not the pieces the root has. With no
        /// legal move, the move is None and the mate 0 when the side to move is mated.
        pub fn think(&mut self, o: &Options, on_iteration: &mut dyn FnMut(&Report)) -> Report {
            let limit = match o.ply_limit {
                Some(p) => 1.max(p.min(MAX_PLY - 1)),
                None => MAX_PLY - 1,
            };
            let depth = o.depth.min(limit);
            let start = (self.clock)();
            let most = o.max_ms.unwrap_or(o.ms);
            let hard = start + most;
            let least = o.min_ms.unwrap_or(0.0).min(most);
            let legal = self.b.legal_moves();
            let level = o.trades * 24.min(self.b.phase) / 24; // the root's pieces, as trades counts them
            if legal.is_empty() {
                let check = self.b.in_check();
                return Report {
                    mv: None,
                    score: if check { -MATE } else { 0 },
                    draw: None,
                    mate: if check { Some(0) } else { None },
                    depth: 0,
                    seldepth: 0,
                    pv: Vec::new(),
                    nodes: 0,
                    ms: 0.0,
                    nps: 0.0,
                    budget: None,
                };
            }
            if self.s.tt.words.is_empty() {
                self.s.resize(20);
            }
            self.s.nodes = 0;
            self.s.hard = hard;
            self.s.max_nodes = o.nodes.unwrap_or(f64::INFINITY);
            self.s.stopped = false;
            self.s.contempt = o.contempt;
            self.s.trades = o.trades;
            let black = o.self_play && self.b.side == 1;
            self.s.key_lo = if black { 0x2c1b3c6d } else { 0 };
            self.s.key_hi = if black { 0x297a2d39 } else { 0 };
            self.s.ply_limit = limit;
            self.s.generation = (self.s.generation + 1) & 1023;
            self.s.killers.fill(0);
            for h in self.s.history.iter_mut() {
                *h /= 2;
            }
            self.s.root_moves.clear();
            self.s.root_moves.extend_from_slice(&legal);
            for i in 0..legal.len() {
                self.s.effort[i] = 0;
                self.s.average[i] = -INFINITY;
            }
            self.s.changes = 0;
            let side = 3 * self.b.side as usize; // this side's numbers in previous
            let shown = |score: i32| if mate_in(score).is_some() { score } else { score - level }; // as the report gives it
                                                                                                   // the last four iterations' scores, the last search's at first: Stockfish's iterValue
            let mut values = match self.s.previous[side] {
                v if v == INFINITY as f64 => [0.0; 4],
                v => [v; 4],
            };
            let mut d = 1;
            let mut done: Option<Report> = None;
            let mut prev = 0; // the last iteration's score, as the search counts it
            let mut last_best = 0; // the last iteration's best move
            let mut settled_from = 0; // the iteration it became the best move at: Stockfish's lastBestMoveDepth
            let mut changes = 0.0; // the changes of the best move, each iteration's count halved at the next
            let mut oldest = 0; // the oldest of values: Stockfish's iterIdx
            let mut reduction = 1.0; // how settled the best move is: Stockfish's timeReduction
            threads::start(self, depth); // any helper threads join, until finish stops them
            loop {
                let score = self.aspire(d, prev);
                let elapsed = (self.clock)() - start;
                let own = self.s.nodes; // this thread's, of which the best move's share decides the time
                let n = own + threads::helper_nodes(); // every thread's, as the reports give them
                let partial = self.s.stopped;
                changes = changes / 2.0 + self.s.changes as f64;
                self.s.changes = 0;
                let report = |this: &Engine, m: i32, score: i32, budget: Option<f64>| {
                    let mate = mate_in(score);
                    let length = this.s.pv_length[0];
                    Report {
                        mv: Some(m),
                        score: shown(score),
                        draw: Some(-o.contempt - level),
                        mate,
                        depth: if partial { d - 1 } else { d },
                        seldepth: this.s.seldepth,
                        // the root's variation, when it is m's; a stopped iteration may have found m without writing its line
                        pv: if length > 0 && this.s.pv[0] == m {
                            this.s.pv[..length as usize].to_vec()
                        } else {
                            vec![m]
                        },
                        nodes: n,
                        ms: js_round(elapsed),
                        nps: if elapsed > 0.0 {
                            js_round(1000.0 * n as f64 / elapsed)
                        } else {
                            0.0
                        },
                        budget,
                    }
                };
                if partial {
                    // stopped: the iteration under way counts only if it found a new best move
                    let m = self.s.root_move;
                    let r = match done {
                        None if m == 0 => report(self, legal[0], 0, None),
                        Some(ref r) if m == 0 || r.mv.map(uci) == Some(uci(m)) => {
                            let mut r = r.clone();
                            r.nodes = n;
                            r.ms = js_round(elapsed);
                            r
                        }
                        _ => report(self, m, self.s.root_score, done.as_ref().and_then(|r| r.budget)),
                    };
                    return self.finish(r, side, level, reduction);
                }
                let best = self.s.pv[0];
                let value = shown(score);
                if best != last_best {
                    settled_from = d;
                }
                // the share of the search the best move took, in 100,000ths
                let effort = self
                    .s
                    .root_moves
                    .iter()
                    .position(|&m| m == best)
                    .map_or(0, |i| self.s.effort[i]);
                let nodes_effort = effort * 100000 / own.max(1);
                // more time while the score falls, against the last search's and four iterations ago
                let falling = ((11.48
                    + 2.30 * (SF_UNITS * (self.s.previous[side + 1] - value as f64))
                    + 1.1 * (SF_UNITS * (values[oldest] - value as f64)))
                    / 100.0)
                    .clamp(0.576, 1.728);
                // less while the best move holds, and the more its last search's did
                reduction = interpolate((d - settled_from) as f64, 4.96, 18.79, 0.639, 1.712).clamp(0.629, 1.544);
                let mut total = o.ms
                    * falling
                    * ((1.468 + self.s.previous[side + 2]) / (2.284 * reduction))
                    * (1.077 + 2.229 * changes) // more while the best move keeps changing
                    * interpolate(nodes_effort as f64, 75800.0, 104510.0, 0.969, 0.714).clamp(0.693, 0.838); // less when it takes most of the search
                if legal.len() == 1 {
                    total = 500f64.min(total); // a forced move
                }
                let budget = total.min(most);
                let r = report(self, best, score, Some(js_round(budget)));
                values[oldest] = value as f64;
                on_iteration(&r);
                if d >= depth
                    || (elapsed >= least
                        && (elapsed > budget
                            || score >= MATE - 3 // it mates within three plies
                            || score == -MATE + 2))
                // or is mated in two
                {
                    return self.finish(r, side, level, reduction);
                }
                d += 1;
                done = Some(r);
                prev = score;
                last_best = best;
                oldest = (oldest + 1) & 3;
            }
        }
    }
    src/threads.rs · 212 lines
    //! Chessmate's search on every core at once, where the page may share memory between workers (worker.js): Lazy SMP,
    //! as Stockfish and most engines since search on many threads. The worker's own thread is the main thread, which
    //! searches, keeps the time and reports, as it does alone. Each other thread is a helper, a worker of its own running
    //! this same module on the same memory, which searches the same position with its own board, evaluation and history,
    //! into the main thread's transposition table. They share nothing else. The helpers' iterations run ahead of the main
    //! thread's and beside it (search.rs, skips), so that the main thread finds positions in the table that a helper has
    //! searched already, often deeper than it has itself, and gets deeper sooner. The move is the main thread's.
    //!
    //! The threads meet through a few words of memory, each read and written whole (atomics). Four of them, CONTROL, the
    //! workers and the page reach too, at the address `control` gives:
    //!
    //!   SEQ        bumped as each search starts: an idle helper waits in worker.js for it to change, then joins
    //!   PULSE      the main thread's heartbeat: one more every 256 nodes it searches, and from its worker every 250 ms
    //!              while it waits for the page
    //!   SEARCH     the number of the page's search under way, which worker.js writes before it starts it; 0 for none
    //!   ABANDONED  the number of the last search the page has given up (chess-engine.ts): a search stops once its number
    //!              is no more than that
    //!
    //! and the others only the threads:
    //!
    //!   STOP       1 while no search is under way or the one under way is ending, and helpers stop when they see it
    //!   ACTIVE     how many helpers are inside a search
    //!   NODES      each helper's nodes in the search under way, which the main thread's reports add to its own
    //!
    //! The main thread writes the root, its position and settings, only while STOP is 1 and no helper is ACTIVE; a helper
    //! counts itself ACTIVE before it looks at STOP, and reads the root only when STOP is 0. So no helper reads a root
    //! half written, and a helper that wakes late, after its search has ended, finds STOP 1 and waits again. As a search
    //! ends, the main thread sets STOP and waits for every helper to leave before it returns its move.
    //!
    //! The page gives a search up through ABANDONED, when Chessmate is to think again or not at all: every thread sees it
    //! within 256 nodes, and the worker, its table and its helpers are ready for the next search at once. Ending the worker
    //! would not stop the search: WebKit ran on a worker it had ended to the end of its search, on every thread, and
    //! Chromium ended its busy helpers only after about ten seconds. So a helper also stops once the main thread's heartbeat
    //! has been silent for 250 ms, as when its worker has ended in a search, and its own worker, finding it silent for good,
    //! ends itself.
    //!
    //! Without atomics, where the module is built for one thread (chessmate.wasm), no helper ever starts, and start and
    //! stop do nothing: the search is the ClojureScript's, node for node.
    
    #![cfg_attr(not(feature = "threads"), allow(dead_code))] // built for one thread, no helper starts
    
    use std::cell::UnsafeCell;
    use std::hint::spin_loop;
    use std::sync::atomic::{
        AtomicI32, AtomicU64,
        Ordering::{Relaxed, SeqCst},
    };
    
    use crate::board::Board;
    use crate::search::{Engine, Settings};
    
    /// The most threads a search runs on, the main thread among them.
    pub const MAX_THREADS: usize = 32;
    
    /// SEQ, PULSE, SEARCH and ABANDONED, in that order: see above.
    static CONTROL: [AtomicI32; 4] = [const { AtomicI32::new(0) }; 4];
    const SEQ: usize = 0;
    const PULSE: usize = 1;
    const SEARCH: usize = 2;
    const ABANDONED: usize = 3;
    static STOP: AtomicI32 = AtomicI32::new(1);
    static ACTIVE: AtomicI32 = AtomicI32::new(0);
    /// How many helpers the worker has started.
    static HELPERS: AtomicI32 = AtomicI32::new(0);
    static NODES: [AtomicU64; MAX_THREADS] = [const { AtomicU64::new(0) }; MAX_THREADS];
    
    /// How long a helper waits for its main thread's next heartbeat before it stops, in ms.
    const SILENCE_MS: f64 = 250.0;
    
    /// Memory every thread can reach, whose threads take turns by the protocol above rather than by a lock.
    struct Shared<T>(UnsafeCell<T>);
    
    // Safety: see the protocol above; each helper's engine is only ever touched by its own thread, and by the main thread
    // between searches (clear)
    unsafe impl<T> Sync for Shared<T> {}
    
    /// The search's root, as the main thread leaves it for the helpers: the position, with the moves that led to it, and
    /// the settings.
    struct Root {
        board: Board,
        settings: Settings,
    }
    
    // Boxed, so that the statics are null pointers until used, not an engine's worth of bytes each in the module
    static ROOT: Shared<Option<Box<Root>>> = Shared(UnsafeCell::new(None));
    /// Each helper's engine, by its thread, made at its first search.
    static ENGINES: [Shared<Option<Box<Engine>>>; MAX_THREADS] = [const { Shared(UnsafeCell::new(None)) }; MAX_THREADS];
    
    #[cfg(feature = "threads")]
    extern "C" {
        /// Wake the helpers waiting for SEQ, at at, to change: worker.js's Atomics.notify.
        fn wake(at: *const i32);
    }
    
    /// Built for one thread, nothing waits.
    #[cfg(not(feature = "threads"))]
    unsafe fn wake(_: *const i32) {}
    
    /// The worker has started n helpers, threads 1 to n.
    pub fn set_helpers(n: i32) {
        HELPERS.store(n.clamp(0, MAX_THREADS as i32 - 1), SeqCst);
    }
    
    /// Where CONTROL is.
    pub fn control() -> *const AtomicI32 {
        CONTROL.as_ptr()
    }
    
    /// The main thread's heartbeat, every 256 nodes it searches.
    #[inline]
    pub fn beat() {
        CONTROL[PULSE].fetch_add(1, Relaxed);
    }
    
    /// Whether a helper's main thread still beats, at now: seen is the beat the helper saw last and when it saw it change.
    pub fn beating(seen: &mut (i32, f64), now: f64) -> bool {
        let p = CONTROL[PULSE].load(Relaxed);
        if p != seen.0 {
            *seen = (p, now);
        }
        now - seen.1 < SILENCE_MS
    }
    
    /// Whether the page has given up the search under way: never where it cannot share memory with the worker, whose
    /// searches are then never numbered.
    #[inline]
    pub fn abandoned() -> bool {
        let s = CONTROL[SEARCH].load(Relaxed);
        s > 0 && s <= CONTROL[ABANDONED].load(Relaxed)
    }
    
    /// As main's search starts, before its first iteration: leave the root and the settings for the helpers, and wake them.
    /// Nothing without helpers.
    pub fn start(main: &Engine, depth: i32) {
        if HELPERS.load(SeqCst) == 0 {
            return;
        }
        while ACTIVE.load(SeqCst) != 0 {
            spin_loop(); // a helper that woke late is leaving
        }
        // Safety: STOP is 1 and no helper is ACTIVE, so none reads the root
        let root = unsafe { &mut *ROOT.0.get() };
        let settings = main.settings(depth);
        match root {
            Some(r) => {
                r.board.copy_from(&main.b);
                r.settings = settings;
            }
            None => {
                let mut board = Board::new();
                board.copy_from(&main.b);
                *root = Some(Box::new(Root { board, settings }));
            }
        }
        for n in NODES.iter() {
            n.store(0, SeqCst);
        }
        STOP.store(0, SeqCst);
        CONTROL[SEQ].fetch_add(1, SeqCst);
        // Safety: the host's function only notifies the address's waiters
        unsafe { wake(CONTROL[SEQ].as_ptr()) }
    }
    
    /// As main's search ends: stop the helpers, and wait until every one has left the search.
    pub fn stop() {
        if HELPERS.load(SeqCst) == 0 {
            return;
        }
        STOP.store(1, SeqCst);
        while ACTIVE.load(SeqCst) != 0 {
            spin_loop(); // each looks at STOP every 256 nodes
        }
    }
    
    /// Whether the search under way is ending, for a helper: the main thread has finished, or the page has given it up.
    #[inline]
    pub fn stopping() -> bool {
        STOP.load(Relaxed) != 0 || abandoned()
    }
    
    /// Helper thread t has searched n nodes in the search under way.
    pub fn counted(t: usize, n: u64) {
        NODES[t % MAX_THREADS].store(n, Relaxed);
    }
    
    /// The nodes every helper has searched in the search under way, as each last counted them: 0 without helpers.
    pub fn helper_nodes() -> u64 {
        NODES.iter().map(|n| n.load(Relaxed)).sum()
    }
    
    /// Helper thread t's part in the search under way, on its own thread, clock its clock: see the protocol above. Returns
    /// once the main thread has finished its search, or at once when none is under way.
    pub fn help(t: usize, clock: fn() -> f64) {
        ACTIVE.fetch_add(1, SeqCst);
        if STOP.load(SeqCst) == 0 && (1..MAX_THREADS).contains(&t) {
            // Safety: while STOP is 0 the main thread leaves the root alone, and only this thread touches its engine
            if let Some(root) = unsafe { (*ROOT.0.get()).as_ref() } {
                let engine = unsafe { (*ENGINES[t].0.get()).get_or_insert_with(|| Box::new(Engine::helper(clock, t))) };
                engine.help(&root.board, &root.settings);
            }
        }
        ACTIVE.fetch_sub(1, SeqCst);
    }
    
    /// Forget what the helpers have learnt, as the main thread forgets its own: their histories, killers and countermoves,
    /// and their evaluation caches. Their engines are made afresh at their next search. Only between searches.
    pub fn clear() {
        for e in ENGINES.iter() {
            // Safety: between searches STOP is 1, so no helper touches its engine
            unsafe { *e.0.get() = None }
        }
    }
    src/eval.rs · 428 lines
    //! Chessmate 2026's evaluation, ported from chessmate.v2.eval: what a position is worth to the side to move, in
    //! centipawns.
    //!
    //! Its base is PeSTO (pesto.rs): each piece's value and square, blended between a middlegame and an endgame table by
    //! how much material is left, which the board keeps up to date move by move. On top of that it looks at what the
    //! tables cannot see:
    //!
    //!   mobility       the squares each piece reaches that no enemy pawn guards
    //!   king safety    enemy pieces bearing on the squares around each king, and the pawns before it
    //!   pawns          passed pawns, most of all in the endgame, and isolated and doubled ones
    //!   pieces         the bishop pair, and rooks on open and half-open files
    //!   endgames       a lone king driven to the edge, and to the right corner against bishop and knight; drawish
    //!                  material scaled towards a draw
    //!   tempo          a little for the side to move
    //!
    //! Every term is a middlegame and an endgame value, blended as the tables are.
    
    use crate::board::{
        bishop_attacks, file_of, index64, on_board, rank_of, rook_attacks, Board, BISHOP, KNIGHT, KNIGHT_ATTACKS, PAWN,
        QUEEN, ROOK,
    };
    
    // The evaluation reads the board's bitboards (board.rs), a1 the lowest bit and h8 the highest: a piece's mobility is
    // the squares it reaches, less its own side's and those an enemy pawn guards, counted at once, where the ClojureScript
    // walks each ray. Every term is the same, and so is every score.
    
    /// Per safe square, by type, less a typical count (MOBILITY_CENTRE).
    const MOBILITY_MG: [i32; 7] = [0, 0, 4, 5, 2, 1, 0];
    const MOBILITY_EG: [i32; 7] = [0, 0, 4, 5, 4, 2, 0];
    const MOBILITY_CENTRE: [i32; 7] = [0, 0, 4, 6, 6, 12, 0];
    
    /// How much a piece's attack on the king's zone counts, by type.
    const ATTACK_WEIGHT: [i32; 7] = [0, 0, 2, 2, 3, 5, 0];
    
    /// Each file's squares, a to h.
    const FILES: [u64; 8] = {
        let mut out = [0; 8];
        let mut f = 0;
        while f < 8 {
            out[f] = 0x0101010101010101 << f;
            f += 1;
        }
        out
    };
    
    /// The files beside each file.
    const BESIDE: [u64; 8] = {
        let mut out = [0; 8];
        let mut f = 0;
        while f < 8 {
            if f > 0 {
                out[f] |= FILES[f - 1];
            }
            if f < 7 {
                out[f] |= FILES[f + 1];
            }
            f += 1;
        }
        out
    };
    
    /// The light squares, whose file and rank add up to an odd number.
    const LIGHT: u64 = 0x55AA55AA55AA55AA;
    
    /// The squares around each side's king that an attacker aims at: the king's and its neighbours', and the three two
    /// ranks ahead of it. By side, then the king's index64.
    static KING_ZONE: [[u64; 64]; 2] = {
        let mut z = [[0; 64]; 2];
        let mut side = 0;
        while side < 2 {
            let ranks = [-1, 0, 1, if side == 0 { 2 } else { -2 }];
            let mut king = 0;
            while king < 128 {
                if on_board(king) {
                    let mut df = -1;
                    while df <= 1 {
                        let mut r = 0;
                        while r < 4 {
                            let sq = king + ranks[r] * 16 + df;
                            if on_board(sq) {
                                z[side][index64(king)] |= 1 << index64(sq);
                            }
                            r += 1;
                        }
                        df += 1;
                    }
                }
                king += 1;
            }
            side += 1;
        }
        z
    };
    
    /// The squares ahead of a pawn of each side on each square, on its file and the files beside it: where an enemy pawn
    /// stops it being passed. By side, then index64.
    static FRONT_SPAN: [[u64; 64]; 2] = {
        let mut out = [[0; 64]; 2];
        let mut s = 0;
        while s < 64 {
            let (f, r) = (s % 8, s / 8);
            let span = FILES[f] | BESIDE[f];
            let mut rank = 0;
            while rank < 8 {
                let row = 0xFFu64 << (8 * rank);
                if rank > r {
                    out[0][s] |= span & row;
                }
                if rank < r {
                    out[1][s] |= span & row;
                }
                rank += 1;
            }
            s += 1;
        }
        out
    };
    
    /// The squares a side's pawns attack.
    fn pawn_guards(pawns: u64, side: i32) -> u64 {
        let (not_a, not_h) = (!FILES[0], !FILES[7]);
        if side == 0 {
            ((pawns << 7) & not_h) | ((pawns << 9) & not_a)
        } else {
            ((pawns >> 9) & not_h) | ((pawns >> 7) & not_a)
        }
    }
    
    /// A bitboard's squares, lowest first, as their index64.
    fn squares_of(mut bits: u64) -> impl Iterator<Item = usize> {
        std::iter::from_fn(move || {
            if bits == 0 {
                None
            } else {
                let s = bits.trailing_zeros() as usize;
                bits &= bits - 1;
                Some(s)
            }
        })
    }
    
    /// The 0x88 square of an index64.
    fn square_of(s: usize) -> i32 {
        ((s / 8) * 16 + s % 8) as i32
    }
    
    /// By rank from the pawn's own side.
    const PASSED_MG: [i32; 8] = [0, 0, 5, 10, 20, 35, 60, 0];
    const PASSED_EG: [i32; 8] = [0, 10, 15, 25, 45, 75, 120, 0];
    
    /// Evaluations already made, by the hash's low 16 bits: the high half of the hash, with its lowest bit set so that an
    /// empty entry never matches, and the score.
    const CACHE_ENTRIES: usize = 65536;
    
    pub struct Eval {
        /// The running score while evaluating: middlegame and endgame, White's minus Black's.
        acc: [i32; 2],
        /// By the attacked side.
        attack_units: [i32; 2],
        attackers: [i32; 2],
        /// Material, by side * 6: the pieces' worth (knight and bishop 300, rook 500, queen 900), and the counts of pawns,
        /// minor pieces, bishops, light-squared bishops and queens.
        mat: [i32; 12],
        cache: Box<[i32; 2 * CACHE_ENTRIES]>,
    }
    
    #[inline(always)]
    fn distance(a: i32, b: i32) -> i32 {
        (file_of(a) - file_of(b)).abs().max((rank_of(a) - rank_of(b)).abs())
    }
    
    fn centre_distance(sq: i32) -> i32 {
        (3 - file_of(sq).min(7 - file_of(sq))).max(3 - rank_of(sq).min(7 - rank_of(sq)))
    }
    
    /// How many squares a bitboard holds.
    #[inline(always)]
    fn count(bits: u64) -> i32 {
        bits.count_ones() as i32
    }
    
    impl Eval {
        pub fn new() -> Eval {
            Eval {
                acc: [0; 2],
                attack_units: [0; 2],
                attackers: [0; 2],
                mat: [0; 12],
                cache: vec![0; 2 * CACHE_ENTRIES].into_boxed_slice().try_into().unwrap(),
            }
        }
    
        /// Add a term, for White when sign is 1 and for Black when it is -1.
        #[inline(always)]
        fn add(&mut self, sign: i32, mg: i32, eg: i32) {
            self.acc[0] += sign * mg;
            self.acc[1] += sign * eg;
        }
    
        /// The mobility of a knight, bishop, rook or queen of side that reaches the squares reach, and its attacks on the
        /// enemy king's zone: the squares it reaches that hold no piece of its own and that no enemy pawn guards, less a
        /// typical count; and the squares of the zone it reaches, whatever stands there.
        fn piece_activity(&mut self, kind: usize, reach: u64, side: usize, sign: i32, own: u64, guarded: u64, zone: u64) {
            let m = count(reach & !own & !guarded) - MOBILITY_CENTRE[kind];
            self.add(sign, m * MOBILITY_MG[kind], m * MOBILITY_EG[kind]);
            let hits = count(reach & zone);
            if hits > 0 {
                self.attack_units[side ^ 1] += hits * ATTACK_WEIGHT[kind];
                self.attackers[side ^ 1] += 1;
            }
        }
    
        /// Passed, isolated and doubled pawns of side, whose pawns are own, against the enemy's.
        fn pawn_structure(&mut self, b: &Board, side: usize, sign: i32, own: u64, enemy: u64) {
            for f in 0..8 {
                let c = count(own & FILES[f]);
                if c > 1 {
                    self.add(sign, -10 * (c - 1), -20 * (c - 1));
                }
                if c > 0 && own & BESIDE[f] == 0 {
                    self.add(sign, -10 * c, -12 * c);
                }
            }
            for s in squares_of(own) {
                if enemy & FRONT_SPAN[side][s] == 0 {
                    let sq = square_of(s);
                    let r = rank_of(sq);
                    let rel = (if side == 0 { r } else { 7 - r }) as usize;
                    let ahead = sq + if side == 0 { 16 } else { -16 };
                    let blocked = b.squares[ahead as usize] != 0;
                    // in the endgame, the kings' distances to the square ahead decide races
                    let kings = 5 * distance(b.kings[side ^ 1], ahead) - 2 * distance(b.kings[side], ahead);
                    let mg = if blocked { PASSED_MG[rel] / 2 } else { PASSED_MG[rel] };
                    let eg = if blocked { PASSED_EG[rel] / 2 } else { PASSED_EG[rel] };
                    self.add(sign, mg, eg + kings * 0.max(rel as i32 - 2));
                }
            }
        }
    
        /// The pawns before side's king, when it stands on its back ranks away from the centre files: a pawn a rank ahead
        /// is best, one two ranks ahead is next, and a file with none is a hole.
        fn shelter(&mut self, b: &Board, side: i32, sign: i32, own: u64) {
            let ksq = b.kings[side as usize];
            let kf = file_of(ksq);
            let rel = if side == 0 { rank_of(ksq) } else { 7 - rank_of(ksq) };
            if rel <= 1 && kf != 3 && kf != 4 {
                let forward = if side == 0 { 16 } else { -16 };
                let pawn = PAWN + side * 8;
                for j in 0..3 {
                    let f = kf + j - 1;
                    if (0..=7).contains(&f) {
                        let one = ksq + forward + (f - kf);
                        let two = one + forward;
                        if b.squares[one as usize] == pawn {
                            self.add(sign, 12, 0);
                        } else if on_board(two) && b.squares[two as usize] == pawn {
                            self.add(sign, 6, 0);
                        } else if own & FILES[f as usize] == 0 {
                            self.add(sign, -18, 0);
                        }
                    }
                }
            }
        }
    
        fn count_material(&mut self, b: &Board, side: usize) {
            let own = b.occupied[side];
            let [knights, bishops, rooks, queens] = [KNIGHT, BISHOP, ROOK, QUEEN].map(|k| count(b.kinds[k as usize] & own));
            let m = side * 6;
            self.mat[m] = 300 * (knights + bishops) + 500 * rooks + 900 * queens;
            self.mat[m + 1] = count(b.kinds[PAWN as usize] & own);
            self.mat[m + 2] = knights + bishops;
            self.mat[m + 3] = bishops;
            self.mat[m + 4] = count(b.kinds[BISHOP as usize] & own & LIGHT);
            self.mat[m + 5] = queens;
        }
    
        /// The attack on side's king: the enemy pieces bearing on its zone, counted by their weights and worth more than
        /// in proportion as they gather. One attacker alone is no attack, and without a queen an attack seldom mates.
        fn king_danger(&mut self, side: i32, sign: i32) {
            if self.attackers[side as usize] >= 2 {
                let units = self.attack_units[side as usize];
                // units squared over 8: over 4 lost 23 Elo in 800 games of self-play, and over 16 was no better
                let danger = 600.min(units * units / 8);
                let queen = self.mat[((side ^ 1) * 6 + 5) as usize] > 0;
                self.add(sign, -(if queen { danger } else { danger / 2 }), 0);
            }
        }
    
        /// How much of the endgame score to keep, in sixteenths: none when the side ahead cannot mate (a lone minor piece,
        /// or two knights, without pawns), little when it has no pawns and at most a minor piece more, and half with bishops
        /// of opposite colours and pawns.
        fn endgame_scale(&self, eg: i32) -> i32 {
            let mat = &self.mat;
            let s = if eg > 0 { 0 } else { 6 };
            let w = if eg > 0 { 6 } else { 0 };
            if mat[s + 1] == 0 && mat[s] <= 300 {
                0
            } else if mat[s + 1] == 0 && mat[s] == 600 && mat[s + 2] == 2 && mat[s + 3] == 0 {
                0
            } else if mat[s + 1] == 0 && mat[s] - mat[w] <= 300 {
                2
            } else if mat[0] == 300 && mat[6] == 300 && mat[3] == 1 && mat[9] == 1 && mat[4] != mat[10] {
                8
            } else {
                16
            }
        }
    
        /// Against a lone king, for the strong side: its king to the edge and the strong king to it; with bishop and
        /// knight, to a corner of the bishop's colour, where the mate is.
        fn drive(&self, b: &Board, strong: i32, weak: i32) -> i32 {
            let wk = b.kings[weak as usize];
            let m = (strong * 6) as usize;
            let f = file_of(wk);
            let r = rank_of(wk);
            let mat = &self.mat;
            let corner = if mat[m] == 600 && mat[m + 2] == 2 && mat[m + 3] == 1 && mat[m + 1] == 0 {
                // a bishop and a knight
                80 * (7 - if mat[m + 4] == 1 {
                    (f + (7 - r)).min((7 - f) + r) // light: a8 and h1
                } else {
                    (f + r).min(14 - f - r) // dark: a1 and h8
                })
            } else {
                0
            };
            30 * centre_distance(wk) + 20 * (7 - distance(wk, b.kings[strong as usize])) + corner
        }
    
        /// The drive against a lone king, with neither pieces nor pawns, from White's point of view, when the other side
        /// has the material to mate with. Against pawns, stopping them comes first.
        fn mop_up(&self, b: &Board) -> i32 {
            let mat = &self.mat;
            if mat[6] == 0 && mat[7] == 0 && mat[0] >= 500 {
                self.drive(b, 0, 1)
            } else if mat[0] == 0 && mat[1] == 0 && mat[6] >= 500 {
                -self.drive(b, 1, 0)
            } else {
                0
            }
        }
    
        fn evaluate_now(&mut self, b: &Board) -> i32 {
            if b.insufficient_material() {
                return 0;
            }
            let phase = 24.min(b.phase);
            self.acc = [b.mg, b.eg];
            self.attack_units = [0; 2];
            self.attackers = [0; 2];
            let occupied = b.occupied[0] | b.occupied[1];
            let k = &b.kinds;
            let pawns = [k[PAWN as usize] & b.occupied[0], k[PAWN as usize] & b.occupied[1]];
            let guards = [pawn_guards(pawns[0], 0), pawn_guards(pawns[1], 1)];
            self.count_material(b, 0);
            self.count_material(b, 1);
            for side in 0..2 {
                let sign = if side == 0 { 1 } else { -1 };
                let own = b.occupied[side];
                let guarded = guards[side ^ 1];
                let zone = KING_ZONE[side ^ 1][index64(b.kings[side ^ 1])];
                for s in squares_of(k[KNIGHT as usize] & own) {
                    self.piece_activity(KNIGHT as usize, KNIGHT_ATTACKS[s], side, sign, own, guarded, zone);
                }
                for s in squares_of(k[BISHOP as usize] & own) {
                    self.piece_activity(
                        BISHOP as usize,
                        bishop_attacks(s, occupied),
                        side,
                        sign,
                        own,
                        guarded,
                        zone,
                    );
                }
                for s in squares_of(k[ROOK as usize] & own) {
                    self.piece_activity(ROOK as usize, rook_attacks(s, occupied), side, sign, own, guarded, zone);
                    let file = FILES[s % 8];
                    if pawns[side] & file == 0 {
                        if pawns[side ^ 1] & file == 0 {
                            self.add(sign, 25, 10);
                        } else {
                            self.add(sign, 10, 5);
                        }
                    }
                }
                for s in squares_of(k[QUEEN as usize] & own) {
                    self.piece_activity(
                        QUEEN as usize,
                        bishop_attacks(s, occupied) | rook_attacks(s, occupied),
                        side,
                        sign,
                        own,
                        guarded,
                        zone,
                    );
                }
                if count(k[BISHOP as usize] & own) >= 2 {
                    self.add(sign, 30, 50);
                }
                self.pawn_structure(b, side, sign, pawns[side], pawns[side ^ 1]);
                self.shelter(b, side as i32, sign, pawns[side]);
            }
            self.king_danger(0, 1);
            self.king_danger(1, -1);
            let mg = self.acc[0];
            let eg = self.acc[1] + self.mop_up(b);
            let eg = eg * self.endgame_scale(eg) / 16;
            let score = (mg * phase + eg * (24 - phase)) / 24;
            10 + if b.side == 0 { score } else { -score }
        }
    
        /// The position's score for the side to move, in centipawns: PeSTO's material and squares, and the terms above,
        /// blended by the material left.
        pub fn evaluate(&mut self, b: &Board) -> i32 {
            let i = 2 * (b.hash_lo & 65535) as usize;
            let check = b.hash_hi | 1;
            if self.cache[i] == check {
                self.cache[i + 1]
            } else {
                let score = self.evaluate_now(b);
                self.cache[i] = check;
                self.cache[i + 1] = score;
                score
            }
        }
    }
    src/board.rs · 1308 lines
    //! Chessmate 2026's board, ported from chessmate.v2.board and built for speed as that is. There is one position,
    //! changed in place by `make` and taken back by `unmake`. The game on the page is still judged by the 2005 rules
    //! (chessmate.board and chessmate.moves); this board is the search's, and perft checks it against the published counts.
    //!
    //! Squares are 0x88: rank * 16 + file, counting from 0, so a1 is 0, h1 is 7 and a8 is 112. A number with a bit of 0x88
    //! set is off the board, which is how a step off the edge is seen without a border, and the difference of two squares
    //! names the line between them.
    //!
    //! Pieces are a type, 1 to 6 for pawn, knight, bishop, rook, queen and king, plus 8 for Black's. Sides are 0 for White
    //! and 1 for Black.
    //!
    //! Moves are numbers: from + 128 * to + 16384 * the type a pawn promotes to, plus a flag for an en passant capture, a
    //! castle or a pawn's double step.
    //!
    //! Alongside the pieces, the board keeps what the search asks about most: the position's Zobrist hash, in two 32-bit
    //! halves, and its PeSTO score, both updated move by move.
    
    use crate::pesto;
    
    // Pieces and sides
    
    pub const PAWN: i32 = 1;
    pub const KNIGHT: i32 = 2;
    pub const BISHOP: i32 = 3;
    pub const ROOK: i32 = 4;
    pub const QUEEN: i32 = 5;
    pub const KING: i32 = 6;
    pub const BLACK: i32 = 8; // a piece's side is piece >> 3: 0 for White, 1 for Black
    
    // Moves
    
    pub const EN_PASSANT_FLAG: i32 = 131072;
    pub const CASTLE_FLAG: i32 = 262144;
    pub const DOUBLE_STEP_FLAG: i32 = 524288;
    /// 16384 * 7: the bits of the type a move promotes to.
    const PROMOTION_BITS: i32 = 114688;
    
    #[inline(always)]
    pub fn move_from(m: i32) -> i32 {
        m & 127
    }
    
    #[inline(always)]
    pub fn move_to(m: i32) -> i32 {
        (m >> 7) & 127
    }
    
    /// The type a move promotes to, or 0.
    #[inline(always)]
    pub fn promotion(m: i32) -> i32 {
        (m >> 14) & 7
    }
    
    // Squares
    
    /// The 0x88 square of a file and rank, each from 0.
    #[inline(always)]
    pub const fn square(file: i32, rank: i32) -> i32 {
        rank * 16 + file
    }
    
    #[inline(always)]
    pub fn file_of(sq: i32) -> i32 {
        sq & 7
    }
    
    #[inline(always)]
    pub fn rank_of(sq: i32) -> i32 {
        sq >> 4
    }
    
    #[inline(always)]
    pub const fn on_board(sq: i32) -> bool {
        sq & 0x88 == 0
    }
    
    pub fn square_name(sq: i32) -> String {
        let mut s = String::with_capacity(2);
        s.push((b'a' + file_of(sq) as u8) as char);
        s.push((b'1' + rank_of(sq) as u8) as char);
        s
    }
    
    /// The 0x88 square a name such as "e4" names, or -1.
    pub fn parse_square(s: &str) -> i32 {
        match s.as_bytes() {
            [f @ b'a'..=b'h', r @ b'1'..=b'8'] => square((f - b'a') as i32, (r - b'1') as i32),
            _ => -1,
        }
    }
    
    /// A move in UCI's coordinates, such as e2e4, or e7e8q for a promotion.
    pub fn uci(m: i32) -> String {
        let mut s = square_name(move_from(m));
        s.push_str(&square_name(move_to(m)));
        s.push_str(["", "", "n", "b", "r", "q"][promotion(m) as usize]);
        s
    }
    
    // Zobrist keys: pieces at piece * 128 + square, castling rights at 2048 + rights, the en passant file at 2064 + file,
    // the side at 2072, each a low and a high half. Drawn from the same fixed mulberry32 sequence as chessmate.v2.board's,
    // so that both hash every position alike.
    
    const KEYS: usize = 2080;
    const CASTLING_KEYS: usize = 2048;
    const EP_KEYS: usize = 2064;
    const SIDE_KEY: usize = 2072;
    
    static ZOBRIST: [[i32; 2]; KEYS] = zobrist();
    
    const fn zobrist() -> [[i32; 2]; KEYS] {
        let mut keys = [[0; 2]; KEYS];
        let mut x: u32 = 0x2545F491;
        let mut i = 0;
        while i < KEYS {
            let mut half = 0;
            while half < 2 {
                // mulberry32, as ClojureScript computes it with Math.imul and >>>
                x = x.wrapping_add(0x6D2B79F5);
                let mut t = (x ^ (x >> 15)).wrapping_mul(x | 1);
                t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
                keys[i][half] = (t ^ (t >> 14)) as i32;
                half += 1;
            }
            i += 1;
        }
        keys
    }
    
    // PeSTO's tables per 0x88 square, White's positive and Black's negative, values included, by piece * 128 + square
    
    static PST_MG: [i32; 2048] = psts(&pesto::MG_VALUES, &pesto::MG_TABLES);
    static PST_EG: [i32; 2048] = psts(&pesto::EG_VALUES, &pesto::EG_TABLES);
    
    const fn psts(values: &[i32; 7], tables: &[[i32; 64]; 6]) -> [i32; 2048] {
        let mut out = [0; 2048];
        let mut kind = 1;
        while kind <= 6 {
            let mut i = 0;
            while i < 64 {
                let file = (i & 7) as i32;
                let rank = 7 - (i >> 3) as i32; // the tables start at a8
                let v = values[kind] + tables[kind - 1][i];
                out[kind * 128 + square(file, rank) as usize] = v;
                out[(kind + 8) * 128 + square(file, 7 - rank) as usize] = -v; // Black's pieces read the table upside down
                i += 1;
            }
            kind += 1;
        }
        out
    }
    
    /// By piece.
    static PHASE_WEIGHT: [i32; 16] = {
        let mut w = [0; 16];
        let mut kind = 1;
        while kind <= 6 {
            w[kind] = pesto::PHASE_WEIGHTS[kind];
            w[kind + 8] = pesto::PHASE_WEIGHTS[kind];
            kind += 1;
        }
        w
    };
    
    /// The steps pieces take: a knight's eight, then the diagonals and the lines.
    pub const STEPS: [i32; 16] = [33, 31, 18, 14, -14, -18, -31, -33, 15, 17, -15, -17, 1, -1, 16, -16];
    /// Where each type's steps start in STEPS.
    pub const STEP_START: [usize; 7] = [0, 0, 0, 8, 12, 8, 8];
    /// Where they end.
    pub const STEP_END: [usize; 7] = [0, 0, 8, 12, 16, 16, 16];
    
    /// The castling rights that survive a move from or to each square.
    static CASTLE_MASK: [i32; 128] = {
        let mut a = [15; 128];
        a[4] = 12;
        a[0] = 13;
        a[7] = 14;
        a[116] = 3;
        a[112] = 7;
        a[119] = 11;
        a
    };
    
    /// Values for exchanges, by piece.
    pub const SEE_VALUES: [i32; 16] = [
        0, 100, 320, 330, 500, 900, 20000, 0, 0, 100, 320, 330, 500, 900, 20000, 0,
    ];
    
    // Bitboards: the board as 64 bits, a1 the lowest and h8 the highest, which the board keeps beside its squares so that
    // asking whether a square is attacked takes a few 64-bit operations, which WebAssembly has, rather than a look at
    // every enemy piece. They answer as the squares do: only the time differs.
    
    /// A 0x88 square's bit.
    #[inline(always)]
    pub const fn bit(sq: i32) -> u64 {
        1 << ((sq + (sq & 7)) >> 1)
    }
    
    /// A 0x88 square's number in a bitboard, 0 to 63.
    #[inline(always)]
    pub const fn index64(sq: i32) -> usize {
        ((sq + (sq & 7)) >> 1) as usize
    }
    
    /// The squares a piece standing on each square reaches in one of steps, by index64.
    const fn leaps(steps: &[i32]) -> [u64; 64] {
        let mut out = [0; 64];
        let mut sq = 0;
        while sq < 128 {
            if on_board(sq) {
                let mut i = 0;
                while i < steps.len() {
                    if on_board(sq + steps[i]) {
                        out[index64(sq)] |= bit(sq + steps[i]);
                    }
                    i += 1;
                }
            }
            sq += 1;
        }
        out
    }
    
    pub static KNIGHT_ATTACKS: [u64; 64] = leaps(&[33, 31, 18, 14, -14, -18, -31, -33]);
    pub static KING_ATTACKS: [u64; 64] = leaps(&[15, 17, -15, -17, 1, -1, 16, -16]);
    /// The squares a pawn of each side attacks from each square.
    pub static PAWN_ATTACKS: [[u64; 64]; 2] = [leaps(&[15, 17]), leaps(&[-15, -17])];
    
    /// The eight directions, as 0x88 steps: those that go up the bitboard's bits first.
    const DIRECTIONS: [i32; 8] = [1, 16, 17, 15, -1, -16, -17, -15];
    
    /// Each direction's ray from each square to the edge, the square itself left out, by direction * 64 + index64.
    static RAYS: [u64; 512] = {
        let mut out = [0; 512];
        let mut d = 0;
        while d < 8 {
            let mut sq = 0;
            while sq < 128 {
                if on_board(sq) {
                    let mut to = sq + DIRECTIONS[d];
                    while on_board(to) {
                        out[d * 64 + index64(sq)] |= bit(to);
                        to += DIRECTIONS[d];
                    }
                }
                sq += 1;
            }
            d += 1;
        }
        out
    };
    
    /// What a slider on square s reaches along direction d, with occupied's squares in the way: the ray up to and
    /// including the first of them.
    #[inline(always)]
    fn ray_attacks(d: usize, s: usize, occupied: u64) -> u64 {
        let ray = RAYS[d * 64 + s];
        let blockers = ray & occupied;
        if blockers == 0 {
            ray
        } else {
            let first = if d < 4 {
                blockers.trailing_zeros()
            } else {
                63 - blockers.leading_zeros()
            };
            ray ^ RAYS[d * 64 + first as usize]
        }
    }
    
    /// The squares a bishop on s reaches.
    #[inline(always)]
    pub fn bishop_attacks(s: usize, occupied: u64) -> u64 {
        ray_attacks(2, s, occupied)
            | ray_attacks(3, s, occupied)
            | ray_attacks(6, s, occupied)
            | ray_attacks(7, s, occupied)
    }
    
    /// The squares a rook on s reaches.
    #[inline(always)]
    pub fn rook_attacks(s: usize, occupied: u64) -> u64 {
        ray_attacks(0, s, occupied)
            | ray_attacks(1, s, occupied)
            | ray_attacks(4, s, occupied)
            | ray_attacks(5, s, occupied)
    }
    
    /// The squares a bishop or rook on each square reaches on an empty board.
    static DIAGONALS: [u64; 64] = {
        let mut out = [0; 64];
        let mut s = 0;
        while s < 64 {
            out[s] = RAYS[2 * 64 + s] | RAYS[3 * 64 + s] | RAYS[6 * 64 + s] | RAYS[7 * 64 + s];
            s += 1;
        }
        out
    };
    static LINES: [u64; 64] = {
        let mut out = [0; 64];
        let mut s = 0;
        while s < 64 {
            out[s] = RAYS[s] | RAYS[64 + s] | RAYS[4 * 64 + s] | RAYS[5 * 64 + s];
            s += 1;
        }
        out
    };
    
    /// The history: what each move changed, for `unmake` and for the repetition test.
    pub const MAX_HISTORY: usize = 1024;
    
    #[derive(Clone, Copy, Default)]
    struct Undo {
        m: i32,
        captured: i32,
        place: i32, // the captured piece's place in its side's list
        castling: i32,
        ep: i32,
        halfmove: i32,
        hash_lo: i32,
        hash_hi: i32,
        mg: i32,
        eg: i32,
        phase: i32,
    }
    
    /// The position, and the state beside it.
    pub struct Board {
        /// The piece on each 0x88 square, or 0.
        pub squares: [i32; 128],
        /// Each side's pieces, by square, in no order: White's in the first 16 places, Black's in the next 16.
        pub lists: [i32; 32],
        /// Where the piece on each square stands in its side's list.
        list_index: [i32; 128],
        /// How many pieces each side has.
        pub counts: [i32; 2],
        /// Each side's king's square.
        pub kings: [i32; 2],
        /// Each side's pieces as a bitboard.
        pub occupied: [u64; 2],
        /// Each type's pieces, both sides', by type.
        pub kinds: [u64; 7],
        /// The side to move.
        pub side: i32,
        /// The castling rights: 1 and 2 White's short and long, 4 and 8 Black's.
        pub castling: i32,
        /// The square a pawn has just passed, when an enemy pawn could take on it; else -1.
        pub ep: i32,
        /// Half-moves since the last capture or pawn move.
        pub halfmove: i32,
        /// Moves made since the position was set, which index the history.
        pub ply: i32,
        /// The Zobrist hash, low half.
        pub hash_lo: i32,
        /// And high half.
        pub hash_hi: i32,
        /// The PeSTO middlegame score, White's minus Black's.
        pub mg: i32,
        /// And the endgame score.
        pub eg: i32,
        /// The material left, 24 at the start.
        pub phase: i32,
        pub fullmove: i32,
        undo: Box<[Undo; MAX_HISTORY]>,
        see_gain: [i32; 40],
    }
    
    impl Board {
        pub fn new() -> Board {
            Board {
                squares: [0; 128],
                lists: [0; 32],
                list_index: [0; 128],
                counts: [0; 2],
                kings: [0; 2],
                occupied: [0; 2],
                kinds: [0; 7],
                side: 0,
                castling: 0,
                ep: -1,
                halfmove: 0,
                ply: 0,
                hash_lo: 0,
                hash_hi: 0,
                mg: 0,
                eg: 0,
                phase: 0,
                fullmove: 1,
                undo: Box::new([Undo::default(); MAX_HISTORY]),
                see_gain: [0; 40],
            }
        }
    
        /// Make this board other's position, with the history that leads to it, for a helper thread's search: without
        /// allocating, and naming every field, so that a new one cannot be left out.
        pub fn copy_from(&mut self, other: &Board) {
            let Board {
                squares,
                lists,
                list_index,
                counts,
                kings,
                occupied,
                kinds,
                side,
                castling,
                ep,
                halfmove,
                ply,
                hash_lo,
                hash_hi,
                mg,
                eg,
                phase,
                fullmove,
                undo,
                see_gain: _, // scratch
            } = other;
            self.squares = *squares;
            self.lists = *lists;
            self.list_index = *list_index;
            self.counts = *counts;
            self.kings = *kings;
            self.occupied = *occupied;
            self.kinds = *kinds;
            self.side = *side;
            self.castling = *castling;
            self.ep = *ep;
            self.halfmove = *halfmove;
            self.ply = *ply;
            self.hash_lo = *hash_lo;
            self.hash_hi = *hash_hi;
            self.mg = *mg;
            self.eg = *eg;
            self.phase = *phase;
            self.fullmove = *fullmove;
            let n = *ply as usize;
            self.undo[..n].copy_from_slice(&undo[..n]);
        }
    
        #[inline(always)]
        fn at(&self, sq: i32) -> i32 {
            self.squares[((sq) & 127) as usize]
        }
    
        #[inline(always)]
        fn toggle(&mut self, k: usize) {
            self.hash_lo ^= ZOBRIST[k][0];
            self.hash_hi ^= ZOBRIST[k][1];
        }
    
        /// A piece's key, by piece * 128 + square.
        #[inline(always)]
        fn toggle_piece(&mut self, k: usize) {
            self.toggle(k & 2047);
        }
    
        // Putting pieces down and picking them up, keeping the lists, the hash and the scores
    
        /// Place piece on the empty square sq, last in its side's list.
        fn put(&mut self, piece: i32, sq: i32) {
            let side = piece >> 3;
            let i = self.counts[side as usize];
            let k = (piece * 128 + sq) as usize;
            self.squares[((sq) & 127) as usize] = piece;
            self.lists[((side * 16 + i) & 31) as usize] = sq;
            self.list_index[((sq) & 127) as usize] = i;
            self.counts[side as usize] = i + 1;
            if piece & 7 == KING {
                self.kings[side as usize] = sq;
            }
            self.occupied[side as usize] |= bit(sq);
            self.kinds[(piece & 7) as usize] |= bit(sq);
            self.toggle_piece(k);
            self.mg += PST_MG[k & 2047];
            self.eg += PST_EG[k & 2047];
            self.phase += PHASE_WEIGHT[(piece & 15) as usize];
        }
    
        /// Take the piece off sq, moving the last of its side's list into its place. Returns its place in the list, which
        /// `restore` puts it back in.
        fn lift(&mut self, sq: i32) -> i32 {
            let piece = self.at(sq);
            let side = piece >> 3;
            let base = side * 16;
            let i = self.list_index[((sq) & 127) as usize];
            let last = self.counts[side as usize] - 1;
            let moved = self.lists[((base + last) & 31) as usize];
            let k = (piece * 128 + sq) as usize;
            self.lists[((base + i) & 31) as usize] = moved;
            self.list_index[((moved) & 127) as usize] = i;
            self.counts[side as usize] = last;
            self.squares[((sq) & 127) as usize] = 0;
            self.occupied[side as usize] ^= bit(sq);
            self.kinds[(piece & 7) as usize] ^= bit(sq);
            self.toggle_piece(k);
            self.mg -= PST_MG[k & 2047];
            self.eg -= PST_EG[k & 2047];
            self.phase -= PHASE_WEIGHT[(piece & 15) as usize];
            i
        }
    
        /// Move the piece on from to the empty square to.
        fn shift(&mut self, from: i32, to: i32) {
            let piece = self.at(from);
            let side = piece >> 3;
            let a = (piece * 128 + from) as usize;
            let b = (piece * 128 + to) as usize;
            self.squares[((to) & 127) as usize] = piece;
            self.squares[((from) & 127) as usize] = 0;
            let place = self.list_index[((from) & 127) as usize];
            self.lists[((side * 16 + place) & 31) as usize] = to;
            self.list_index[((to) & 127) as usize] = place;
            if piece & 7 == KING {
                self.kings[side as usize] = to;
            }
            self.occupied[side as usize] ^= bit(from) | bit(to);
            self.kinds[(piece & 7) as usize] ^= bit(from) | bit(to);
            self.toggle_piece(a);
            self.toggle_piece(b);
            self.mg += PST_MG[b & 2047] - PST_MG[a & 2047];
            self.eg += PST_EG[b & 2047] - PST_EG[a & 2047];
        }
    
        /// Turn the pawn on sq into piece.
        fn promote(&mut self, sq: i32, piece: i32) {
            let a = (self.at(sq) * 128 + sq) as usize;
            let b = (piece * 128 + sq) as usize;
            self.squares[((sq) & 127) as usize] = piece;
            self.kinds[PAWN as usize] ^= bit(sq);
            self.kinds[(piece & 7) as usize] ^= bit(sq);
            self.toggle_piece(a);
            self.toggle_piece(b);
            self.mg += PST_MG[b & 2047] - PST_MG[a & 2047];
            self.eg += PST_EG[b & 2047] - PST_EG[a & 2047];
            self.phase += PHASE_WEIGHT[(piece & 15) as usize];
        }
    
        // unmake restores the hash and the scores from the history, so it moves pieces plainly
    
        fn relocate(&mut self, from: i32, to: i32) {
            let piece = self.at(from);
            self.squares[((to) & 127) as usize] = piece;
            self.squares[((from) & 127) as usize] = 0;
            let place = self.list_index[((from) & 127) as usize];
            self.lists[(((piece >> 3) * 16 + place) & 31) as usize] = to;
            self.list_index[((to) & 127) as usize] = place;
            if piece & 7 == KING {
                self.kings[(piece >> 3) as usize] = to;
            }
            self.occupied[(piece >> 3) as usize] ^= bit(from) | bit(to);
            self.kinds[(piece & 7) as usize] ^= bit(from) | bit(to);
        }
    
        /// Put piece back on sq at place i of its side's list, undoing `lift`.
        fn restore(&mut self, piece: i32, sq: i32, i: i32) {
            let side = piece >> 3;
            let base = side * 16;
            let n = self.counts[side as usize];
            let moved = self.lists[((base + i) & 31) as usize];
            self.lists[((base + n) & 31) as usize] = moved;
            self.list_index[((moved) & 127) as usize] = n;
            self.lists[((base + i) & 31) as usize] = sq;
            self.list_index[((sq) & 127) as usize] = i;
            self.counts[side as usize] = n + 1;
            self.squares[((sq) & 127) as usize] = piece;
            self.occupied[side as usize] |= bit(sq);
            self.kinds[(piece & 7) as usize] |= bit(sq);
        }
    
        // Attacks
    
        /// Is sq attacked by side's pieces?
        #[inline]
        pub fn attacked(&self, sq: i32, side: i32) -> bool {
            self.attacked_by(
                sq,
                side,
                self.occupied[0] | self.occupied[1],
                self.occupied[side as usize],
            )
        }
    
        /// Is sq attacked by side's pieces among theirs, with occupied's squares in the way? As chessmate.v2.board/attacked?
        /// asks it of each of side's pieces in turn, here of each type at once.
        #[inline(always)]
        fn attacked_by(&self, sq: i32, side: i32, occupied: u64, theirs: u64) -> bool {
            let s = index64(sq);
            let k = &self.kinds;
            let diagonal = theirs & (k[BISHOP as usize] | k[QUEEN as usize]);
            let straight = theirs & (k[ROOK as usize] | k[QUEEN as usize]);
            KNIGHT_ATTACKS[s] & theirs & k[KNIGHT as usize] != 0
                || PAWN_ATTACKS[(side ^ 1) as usize][s] & theirs & k[PAWN as usize] != 0
                || KING_ATTACKS[s] & theirs & k[KING as usize] != 0
                || (DIAGONALS[s] & diagonal != 0 && bishop_attacks(s, occupied) & diagonal != 0)
                || (LINES[s] & straight != 0 && rook_attacks(s, occupied) & straight != 0)
        }
    
        /// Is the side to move in check?
        #[inline]
        pub fn in_check(&self) -> bool {
            let us = self.side;
            self.attacked(self.kings[us as usize], us ^ 1)
        }
    
        /// After `make`: did the move leave its side's king unattacked?
        #[inline]
        pub fn legal(&self) -> bool {
            let them = self.side;
            !self.attacked(self.kings[(them ^ 1) as usize], them)
        }
    
        /// After `make` of m, as `legal` asks, knowing whether its side was in check before it. A side not in check can
        /// only leave its king attacked by moving the king, by an en passant capture, which empties two squares, or by
        /// uncovering a line to it from the square the piece left; any other move is legal without looking.
        #[inline]
        pub fn legal_after(&self, m: i32, was_in_check: bool) -> bool {
            let king = self.kings[(self.side ^ 1) as usize];
            let s = index64(king);
            if was_in_check
                || king == move_to(m)
                || m & EN_PASSANT_FLAG != 0
                || (DIAGONALS[s] | LINES[s]) & bit(move_from(m)) != 0
            {
                self.legal()
            } else {
                true
            }
        }
    
        // Move generation
    
        fn promotions(out: &mut [i32], mut n: usize, from: i32, to: i32, captures: bool) -> usize {
            let m = from + to * 128;
            out[n] = m + QUEEN * 16384;
            n += 1;
            if !captures {
                out[n] = m + KNIGHT * 16384;
                out[n + 1] = m + ROOK * 16384;
                out[n + 2] = m + BISHOP * 16384;
                n += 3;
            }
            n
        }
    
        #[allow(clippy::too_many_arguments)]
        fn pawn_capture(
            &self,
            out: &mut [i32],
            n: usize,
            from: i32,
            to: i32,
            them: i32,
            last_rank: bool,
            captures: bool,
        ) -> usize {
            if on_board(to) {
                let target = self.at(to);
                if target != 0 && target >> 3 == them {
                    if last_rank {
                        Board::promotions(out, n, from, to, captures)
                    } else {
                        out[n] = from + to * 128;
                        n + 1
                    }
                } else if to == self.ep {
                    out[n] = from + to * 128 + EN_PASSANT_FLAG;
                    n + 1
                } else {
                    n
                }
            } else {
                n
            }
        }
    
        fn pawn_moves(&self, out: &mut [i32], mut n: usize, from: i32, us: i32, captures: bool) -> usize {
            let forward = if us == 0 { 16 } else { -16 };
            let to = from + forward;
            let last_rank = to >> 4 == if us == 0 { 7 } else { 0 };
            let them = us ^ 1;
            if self.at(to) == 0 {
                if last_rank {
                    n = Board::promotions(out, n, from, to, captures);
                } else if !captures {
                    out[n] = from + to * 128;
                    n += 1;
                    if from >> 4 == (if us == 0 { 1 } else { 6 }) && self.at(to + forward) == 0 {
                        out[n] = from + (to + forward) * 128 + DOUBLE_STEP_FLAG;
                        n += 1;
                    }
                }
            }
            n = self.pawn_capture(out, n, from, to - 1, them, last_rank, captures);
            self.pawn_capture(out, n, from, to + 1, them, last_rank, captures)
        }
    
        #[allow(clippy::too_many_arguments)]
        #[inline(always)]
        fn ray(
            &self,
            out: &mut [i32],
            mut n: usize,
            from: i32,
            step: i32,
            them: i32,
            slides: bool,
            captures: bool,
        ) -> usize {
            let mut to = from + step;
            while on_board(to) {
                let target = self.at(to);
                if target == 0 {
                    if !captures {
                        out[n] = from + to * 128;
                        n += 1;
                    }
                    if !slides {
                        return n;
                    }
                    to += step;
                } else {
                    if target >> 3 == them {
                        out[n] = from + to * 128;
                        n += 1;
                    }
                    return n;
                }
            }
            n
        }
    
        fn piece_moves(&self, out: &mut [i32], mut n: usize, from: i32, piece: i32, them: i32, captures: bool) -> usize {
            let k = (piece & 7) as usize;
            let slides = k >= BISHOP as usize && k <= QUEEN as usize;
            for &step in &STEPS[STEP_START[k]..STEP_END[k]] {
                n = self.ray(out, n, from, step, them, slides, captures);
            }
            n
        }
    
        #[allow(clippy::too_many_arguments)]
        fn castle(
            &self,
            out: &mut [i32],
            n: usize,
            right: i32,
            king_sq: i32,
            rook_sq: i32,
            between: &[i32],
            safe: [i32; 3],
        ) -> usize {
            let colour = if king_sq > 7 { BLACK } else { 0 };
            let enemy = if king_sq > 7 { 0 } else { 1 };
            if self.castling & right != 0
                && self.at(king_sq) == KING + colour
                && self.at(rook_sq) == ROOK + colour
                && between.iter().all(|&sq| self.at(sq) == 0)
                && !safe.iter().any(|&sq| self.attacked(sq, enemy))
            {
                out[n] = king_sq + safe[1] * 128 + CASTLE_FLAG;
                n + 1
            } else {
                n
            }
        }
    
        fn castles(&self, out: &mut [i32], n: usize, us: i32) -> usize {
            if us == 0 {
                let n = self.castle(out, n, 1, 4, 7, &[5, 6], [4, 6, 5]);
                self.castle(out, n, 2, 4, 0, &[3, 2, 1], [4, 2, 3])
            } else {
                let n = self.castle(out, n, 4, 116, 119, &[117, 118], [116, 118, 117]);
                self.castle(out, n, 8, 116, 112, &[115, 114, 113], [116, 114, 115])
            }
        }
    
        /// Write the side to move's moves into out from index start, and return the index after the last. With captures
        /// true, only captures and promotions to a queen, for the quiescence search. The moves are pseudo-legal: after
        /// `make`, `legal` says whether the mover's king is safe. Castling is generated only when legal, as the probe cannot
        /// see a king cross an attacked square.
        pub fn generate(&self, out: &mut [i32], start: usize, captures: bool) -> usize {
            let us = self.side;
            let them = us ^ 1;
            let base = (us * 16) as usize;
            let n = self.counts[us as usize] as usize;
            let mut end = start;
            for i in 0..n {
                let from = self.lists[base + i];
                let piece = self.at(from);
                end = if piece & 7 == PAWN {
                    self.pawn_moves(out, end, from, us, captures)
                } else {
                    self.piece_moves(out, end, from, piece, them, captures)
                };
            }
            if captures || self.castling == 0 {
                end
            } else {
                self.castles(out, end, us)
            }
        }
    
        // Making and unmaking moves
    
        /// Could side's pawn on sq take the pawn on victim en passant, onto ep, without leaving its king attacked? The
        /// capture is tried on the bitboards alone: the pawns' squares emptied and ep filled.
        fn ep_legal(&self, sq: i32, victim: i32, ep: i32, side: i32) -> bool {
            if on_board(sq) && self.at(sq) == PAWN + side * 8 {
                let occupied = ((self.occupied[0] | self.occupied[1]) & !bit(sq) & !bit(victim)) | bit(ep);
                let theirs = self.occupied[(side ^ 1) as usize] & !bit(victim) & !bit(ep);
                !self.attacked_by(self.kings[side as usize], side ^ 1, occupied, theirs)
            } else {
                false
            }
        }
    
        /// Could side's pawns take en passant on ep, the square just passed? Only then is ep part of the position: two
        /// positions that differ only in an en passant square no capture can use are one, as the rules' EPD counts them. A
        /// capture that would leave its king attacked cannot use it, as when the two pawns are all that stand between the
        /// king and a rook on their rank.
        fn ep_capturable(&self, ep: i32, side: i32) -> bool {
            let victim = ep + if side == 0 { -16 } else { 16 };
            self.ep_legal(victim - 1, victim, ep, side) || self.ep_legal(victim + 1, victim, ep, side)
        }
    
        fn set_ep(&mut self, sq: i32) {
            let old = self.ep;
            if old != -1 {
                self.toggle(EP_KEYS + (old & 7) as usize);
            }
            self.ep = sq;
            if sq != -1 {
                self.toggle(EP_KEYS + (sq & 7) as usize);
            }
        }
    
        fn set_castling(&mut self, rights: i32) {
            let old = self.castling;
            if old != rights {
                self.toggle(CASTLING_KEYS + old as usize);
                self.toggle(CASTLING_KEYS + rights as usize);
                self.castling = rights;
            }
        }
    
        /// Play move m, which `generate` made for this position. The history keeps what `unmake` needs: the move, the piece
        /// it took and that piece's place in its list, the castling rights, the en passant square, the halfmove clock, the
        /// hash and the scores.
        pub fn make(&mut self, m: i32) {
            let from = m & 127;
            let to = (m >> 7) & 127;
            let us = self.side;
            let piece = self.at(from);
            let u = self.ply as usize;
            self.undo[u] = Undo {
                m,
                captured: 0,
                place: 0,
                castling: self.castling,
                ep: self.ep,
                halfmove: self.halfmove,
                hash_lo: self.hash_lo,
                hash_hi: self.hash_hi,
                mg: self.mg,
                eg: self.eg,
                phase: self.phase,
            };
            self.set_ep(-1);
            if m & CASTLE_FLAG == 0 {
                let at = if m & EN_PASSANT_FLAG != 0 {
                    to + if us == 0 { -16 } else { 16 }
                } else {
                    to
                };
                let captured = self.at(at);
                self.undo[u].captured = captured;
                self.undo[u].place = if captured == 0 { 0 } else { self.lift(at) };
                self.shift(from, to);
                if m & PROMOTION_BITS != 0 {
                    self.promote(to, ((m >> 14) & 7) + us * 8);
                }
                self.halfmove = if piece & 7 == PAWN || captured != 0 {
                    0
                } else {
                    self.halfmove + 1
                };
                if m & DOUBLE_STEP_FLAG != 0 && self.ep_capturable((from + to) >> 1, us ^ 1) {
                    self.set_ep((from + to) >> 1);
                }
            } else {
                self.shift(from, to);
                if to > from {
                    self.shift(to + 1, to - 1);
                } else {
                    self.shift(to - 2, to + 1);
                }
                self.halfmove += 1;
            }
            self.set_castling(self.castling & CASTLE_MASK[from as usize] & CASTLE_MASK[to as usize]);
            self.toggle(SIDE_KEY);
            self.side = us ^ 1;
            self.ply += 1;
            if us != 0 {
                self.fullmove += 1;
            }
        }
    
        /// Take back the last move `make` played.
        pub fn unmake(&mut self) {
            let h = self.ply - 1;
            let u = self.undo[h as usize];
            let m = u.m;
            let from = m & 127;
            let to = (m >> 7) & 127;
            let us = self.side ^ 1;
            if m & CASTLE_FLAG == 0 {
                if m & PROMOTION_BITS != 0 {
                    self.kinds[(self.at(to) & 7) as usize] ^= bit(to);
                    self.kinds[PAWN as usize] ^= bit(to);
                    self.squares[((to) & 127) as usize] = PAWN + us * 8;
                }
                self.relocate(to, from);
                if u.captured != 0 {
                    let at = if m & EN_PASSANT_FLAG == 0 {
                        to
                    } else {
                        to + if us == 0 { -16 } else { 16 }
                    };
                    self.restore(u.captured, at, u.place);
                }
            } else {
                self.relocate(to, from);
                if to > from {
                    self.relocate(to - 1, to + 1);
                } else {
                    self.relocate(to + 1, to - 2);
                }
            }
            self.side = us;
            self.ply = h;
            self.castling = u.castling;
            self.ep = u.ep;
            self.halfmove = u.halfmove;
            self.hash_lo = u.hash_lo;
            self.hash_hi = u.hash_hi;
            self.mg = u.mg;
            self.eg = u.eg;
            self.phase = u.phase;
            if us != 0 {
                self.fullmove -= 1;
            }
        }
    
        /// Pass: let the other side move, for the null-move search. The halfmove clock restarts, so the repetition test
        /// never looks back across a pass.
        pub fn make_null(&mut self) {
            let u = &mut self.undo[self.ply as usize];
            u.m = 0;
            u.ep = self.ep;
            u.halfmove = self.halfmove;
            u.hash_lo = self.hash_lo;
            u.hash_hi = self.hash_hi;
            self.set_ep(-1);
            self.toggle(SIDE_KEY);
            self.halfmove = 0;
            self.side ^= 1;
            self.ply += 1;
        }
    
        pub fn unmake_null(&mut self) {
            let h = self.ply - 1;
            let u = self.undo[h as usize];
            self.side ^= 1;
            self.ply = h;
            self.ep = u.ep;
            self.halfmove = u.halfmove;
            self.hash_lo = u.hash_lo;
            self.hash_hi = u.hash_hi;
        }
    
        // Draws the search can see
    
        /// Has the position occurred before, since the last capture or pawn move? Positions before the search's root count
        /// too: the game's history is played onto the board first.
        pub fn repetition(&self) -> bool {
            let h = self.ply;
            let oldest = 0.max(h - self.halfmove);
            let mut i = h - 2;
            while i >= oldest {
                let u = &self.undo[i as usize];
                if u.hash_lo == self.hash_lo && u.hash_hi == self.hash_hi {
                    return true;
                }
                i -= 2;
            }
            false
        }
    
        /// Kings alone, or a king and one knight or bishop against a king: no mate is possible.
        pub fn insufficient_material(&self) -> bool {
            let w = self.counts[0];
            let b = self.counts[1];
            (w == 1 && b == 1)
                || (w + b == 3 && {
                    let sq = if w == 2 {
                        if self.lists[0] == self.kings[0] {
                            self.lists[1]
                        } else {
                            self.lists[0]
                        }
                    } else if self.lists[16] == self.kings[1] {
                        self.lists[17]
                    } else {
                        self.lists[16]
                    };
                    let k = self.at(sq) & 7;
                    k == KNIGHT || k == BISHOP
                })
        }
    
        // Positions from and to FEN
    
        fn clear(&mut self) {
            self.squares = [0; 128];
            self.counts = [0; 2];
            self.occupied = [0; 2];
            self.kinds = [0; 7];
            self.side = 0;
            self.castling = 0;
            self.ep = -1;
            self.halfmove = 0;
            self.ply = 0;
            self.hash_lo = 0;
            self.hash_hi = 0;
            self.mg = 0;
            self.eg = 0;
            self.phase = 0;
            self.fullmove = 0;
        }
    
        /// Set the board to a position in FEN, which must be legal, as chessmate.notation/read-fen checks. The history is
        /// emptied. A FEN this cannot read is refused, where ClojureScript's would set a broken board.
        pub fn set_position(&mut self, fen: &str) -> Result<(), String> {
            let mut fields = fen.split_whitespace();
            let placement = fields.next().unwrap_or("");
            let side = fields.next();
            let castling = fields.next();
            let ep = fields.next();
            let halfmove = fields.next();
            let fullmove = fields.next();
            self.clear();
            let (mut file, mut rank) = (0, 7);
            for c in placement.chars() {
                match c {
                    '/' => {
                        file = 0;
                        rank -= 1;
                    }
                    '1'..='8' => file += c as i32 - '0' as i32,
                    _ => {
                        let kind = "PNBRQK"
                            .find(c.to_ascii_uppercase())
                            .ok_or_else(|| format!("not a FEN: {}", fen))? as i32
                            + 1;
                        let piece = kind + if c.is_ascii_uppercase() { 0 } else { BLACK };
                        if !(0..8).contains(&file) || !(0..8).contains(&rank) {
                            return Err(format!("not a FEN: {}", fen));
                        }
                        if self.counts[(piece >> 3) as usize] == 16 {
                            return Err(format!("more than 16 pieces a side: {}", fen));
                        }
                        self.put(piece, square(file, rank));
                        file += 1;
                    }
                }
            }
            if side == Some("b") {
                self.side = 1;
                self.toggle(SIDE_KEY);
            }
            let rights = castling.unwrap_or("").chars().map(|c| match c {
                'K' => 1,
                'Q' => 2,
                'k' => 4,
                'q' => 8,
                _ => 0,
            });
            self.set_castling(rights.sum());
            let sq = match ep {
                None | Some("-") => -1,
                Some(s) => parse_square(s),
            };
            if sq != -1 && self.ep_capturable(sq, self.side) {
                self.set_ep(sq);
            }
            self.halfmove = halfmove.map_or(0, parse_int);
            self.fullmove = fullmove.map_or(1, parse_int);
            Ok(())
        }
    
        /// The position in FEN. Its en passant square is given only when a pawn could take there.
        pub fn fen(&self) -> String {
            let mut s = String::new();
            for rank in (0..8).rev() {
                let mut empty = 0;
                for file in 0..8 {
                    let p = self.at(square(file, rank));
                    if p == 0 {
                        empty += 1;
                    } else {
                        if empty > 0 {
                            s.push_str(&empty.to_string());
                            empty = 0;
                        }
                        let c = b"PNBRQK"[((p & 7) - 1) as usize] as char;
                        s.push(if p & BLACK == 0 { c } else { c.to_ascii_lowercase() });
                    }
                }
                if empty > 0 {
                    s.push_str(&empty.to_string());
                }
                if rank > 0 {
                    s.push('/');
                }
            }
            s.push_str(if self.side == 0 { " w " } else { " b " });
            if self.castling == 0 {
                s.push('-');
            } else {
                for (bit, letter) in [(1, 'K'), (2, 'Q'), (4, 'k'), (8, 'q')] {
                    if self.castling & bit != 0 {
                        s.push(letter);
                    }
                }
            }
            s.push(' ');
            s.push_str(&if self.ep == -1 {
                "-".to_string()
            } else {
                square_name(self.ep)
            });
            s.push_str(&format!(" {} {}", self.halfmove, self.fullmove));
            s
        }
    
        /// Has the side to move a legal move? Without one, it is mated or stalemated.
        pub fn has_legal_move(&mut self) -> bool {
            let mut scratch = [0; 256];
            let end = self.generate(&mut scratch, 0, false);
            for &m in &scratch[..end] {
                self.make(m);
                let ok = self.legal();
                self.unmake();
                if ok {
                    return true;
                }
            }
            false
        }
    
        /// The side to move's legal moves, in generation order.
        pub fn legal_moves(&mut self) -> Vec<i32> {
            let mut buffer = [0; 256];
            let end = self.generate(&mut buffer, 0, false);
            let mut found = Vec::with_capacity(end);
            for &m in &buffer[..end] {
                self.make(m);
                let ok = self.legal();
                self.unmake();
                if ok {
                    found.push(m);
                }
            }
            found
        }
    
        /// The legal move a string in UCI's coordinates names, such as "e2e4" or "e7e8q", or None.
        pub fn parse_uci(&mut self, s: &str) -> Option<i32> {
            self.legal_moves().into_iter().find(|&m| uci(m) == s)
        }
    
        /// Play a legal move given in UCI, or say it is not one. The history holds the moves since the last capture or pawn
        /// move, and the search's: a game longer than that is refused, where ClojureScript's would lose its history.
        pub fn play_uci(&mut self, s: &str) -> Result<(), String> {
            match self.parse_uci(s) {
                Some(_) if self.ply as usize >= MAX_HISTORY - 128 => Err(format!("too many moves to play: {}", s)),
                Some(m) => {
                    self.make(m);
                    Ok(())
                }
                None => Err(format!("not a legal move here: {}", s)),
            }
        }
    
        fn perft_from(&mut self, buffer: &mut [i32], depth: i32, start: usize) -> u64 {
            let end = self.generate(buffer, start, false);
            let mut nodes = 0;
            for i in start..end {
                let m = buffer[i];
                self.make(m);
                if self.legal() {
                    nodes += if depth == 1 {
                        1
                    } else {
                        self.perft_from(buffer, depth - 1, end)
                    };
                }
                self.unmake();
            }
            nodes
        }
    
        /// How many move sequences of depth moves the position has: the move generator's test.
        pub fn perft(&mut self, depth: i32) -> u64 {
            if depth <= 0 {
                1
            } else {
                let mut buffer = vec![0; 256 * depth as usize];
                self.perft_from(&mut buffer, depth, 0)
            }
        }
    
        // Static exchange evaluation
    
        /// The square of side's least valuable piece that attacks sq, of the pieces on occupied's squares, which also stand
        /// in the way, or -1. Of two such pieces, the first in side's list, as chessmate.v2.board/least-attacker takes it.
        fn least_attacker(&self, sq: i32, side: i32, occupied: u64) -> i32 {
            let s = index64(sq);
            let k = &self.kinds;
            let present = occupied & self.occupied[side as usize];
            let diagonal = bishop_attacks(s, occupied);
            let straight = rook_attacks(s, occupied);
            // by their exchange values: pawn, knight, bishop, rook, queen and king
            let found = [
                PAWN_ATTACKS[(side ^ 1) as usize][s] & k[PAWN as usize],
                KNIGHT_ATTACKS[s] & k[KNIGHT as usize],
                diagonal & k[BISHOP as usize],
                straight & k[ROOK as usize],
                (diagonal | straight) & k[QUEEN as usize],
                KING_ATTACKS[s] & k[KING as usize],
            ]
            .into_iter()
            .map(|attackers| attackers & present)
            .find(|&attackers| attackers != 0);
            match found {
                None => -1,
                Some(attackers) if attackers & (attackers - 1) == 0 => {
                    let i = attackers.trailing_zeros() as i32;
                    (i / 8) * 16 + i % 8
                }
                Some(attackers) => {
                    let base = (side * 16) as usize;
                    let n = self.counts[side as usize] as usize;
                    *self.lists[base..base + n]
                        .iter()
                        .find(|&&from| attackers & bit(from) != 0)
                        .unwrap()
                }
            }
        }
    
        /// The static exchange evaluation of move m, in centipawns: what the mover wins on m's square if both sides then
        /// take there in turn, each with its least valuable piece, and each stops when going on would cost it. The pieces
        /// that have taken leave the bitboard of those still standing, which uncovers any piece behind them.
        pub fn see(&mut self, m: i32) -> i32 {
            let from = m & 127;
            let to = (m >> 7) & 127;
            let piece = self.at(from);
            let target = self.at(to);
            self.see_gain[0] = if m & EN_PASSANT_FLAG == 0 {
                SEE_VALUES[target as usize]
            } else {
                100
            };
            let mut standing = (self.occupied[0] | self.occupied[1]) & !bit(from);
            let mut d = 0;
            let mut side = (piece >> 3) ^ 1;
            let mut on = SEE_VALUES[piece as usize];
            loop {
                let a = self.least_attacker(to, side, standing);
                if a == -1 {
                    for i in (1..=d).rev() {
                        self.see_gain[i - 1] = -(-self.see_gain[i - 1]).max(self.see_gain[i]);
                    }
                    return self.see_gain[0];
                }
                d += 1;
                self.see_gain[d] = on - self.see_gain[d - 1];
                standing &= !bit(a);
                side ^= 1;
                on = SEE_VALUES[self.at(a) as usize];
            }
        }
    }
    
    /// A FEN's count as JavaScript's parseInt reads it: its leading digits, or 0 when it has none, as an Int32Array stores
    /// parseInt's NaN.
    fn parse_int(s: &str) -> i32 {
        let t = s.trim_start();
        let (sign, digits) = match t.as_bytes().first() {
            Some(b'-') => (-1, &t[1..]),
            Some(b'+') => (1, &t[1..]),
            _ => (1, t),
        };
        let n: String = digits.chars().take_while(|c| c.is_ascii_digit()).collect();
        n.parse::<i64>().map_or(0, |v| (sign * v) as i32)
    }
    src/pesto.rs · 147 lines
    //! The piece values and piece-square tables of Chessmate 2026's evaluation, as chessmate.v2.pesto holds them: Ronald
    //! Friederich's PeSTO ("Piece-Square Tables Only"), tuned by Texel's method for his engine RofChade, as the Chess
    //! Programming Wiki publishes them (CC BY-SA 3.0):
    //! https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function
    //!
    //! Each piece has two values and two tables, one for the middlegame and one for the endgame, in centipawns. The board
    //! blends them by how much material is left.
    
    /// The middlegame values, by type: none, pawn, knight, bishop, rook, queen and king.
    pub const MG_VALUES: [i32; 7] = [0, 82, 337, 365, 477, 1025, 0];
    
    /// The endgame values, by type, as MG_VALUES.
    pub const EG_VALUES: [i32; 7] = [0, 94, 281, 297, 512, 936, 0];
    
    /// How much each type counts towards the middlegame, by type: 24 with every piece on the board, and 0 with only kings
    /// and pawns.
    pub const PHASE_WEIGHTS: [i32; 7] = [0, 0, 1, 1, 2, 4, 0];
    
    /// The middlegame tables, by type: pawn, knight, bishop, rook, queen and king, each a8 first, as White sees the board.
    #[rustfmt::skip]
    pub const MG_TABLES: [[i32; 64]; 6] = [
        [ // pawn
              0,    0,    0,    0,    0,    0,    0,    0,
             98,  134,   61,   95,   68,  126,   34,  -11,
             -6,    7,   26,   31,   65,   56,   25,  -20,
            -14,   13,    6,   21,   23,   12,   17,  -23,
            -27,   -2,   -5,   12,   17,    6,   10,  -25,
            -26,   -4,   -4,  -10,    3,    3,   33,  -12,
            -35,   -1,  -20,  -23,  -15,   24,   38,  -22,
              0,    0,    0,    0,    0,    0,    0,    0,
        ],
        [ // knight
           -167,  -89,  -34,  -49,   61,  -97,  -15, -107,
            -73,  -41,   72,   36,   23,   62,    7,  -17,
            -47,   60,   37,   65,   84,  129,   73,   44,
             -9,   17,   19,   53,   37,   69,   18,   22,
            -13,    4,   16,   13,   28,   19,   21,   -8,
            -23,   -9,   12,   10,   19,   17,   25,  -16,
            -29,  -53,  -12,   -3,   -1,   18,  -14,  -19,
           -105,  -21,  -58,  -33,  -17,  -28,  -19,  -23,
        ],
        [ // bishop
            -29,    4,  -82,  -37,  -25,  -42,    7,   -8,
            -26,   16,  -18,  -13,   30,   59,   18,  -47,
            -16,   37,   43,   40,   35,   50,   37,   -2,
             -4,    5,   19,   50,   37,   37,    7,   -2,
             -6,   13,   13,   26,   34,   12,   10,    4,
              0,   15,   15,   15,   14,   27,   18,   10,
              4,   15,   16,    0,    7,   21,   33,    1,
            -33,   -3,  -14,  -21,  -13,  -12,  -39,  -21,
        ],
        [ // rook
             32,   42,   32,   51,   63,    9,   31,   43,
             27,   32,   58,   62,   80,   67,   26,   44,
             -5,   19,   26,   36,   17,   45,   61,   16,
            -24,  -11,    7,   26,   24,   35,   -8,  -20,
            -36,  -26,  -12,   -1,    9,   -7,    6,  -23,
            -45,  -25,  -16,  -17,    3,    0,   -5,  -33,
            -44,  -16,  -20,   -9,   -1,   11,   -6,  -71,
            -19,  -13,    1,   17,   16,    7,  -37,  -26,
        ],
        [ // queen
            -28,    0,   29,   12,   59,   44,   43,   45,
            -24,  -39,   -5,    1,  -16,   57,   28,   54,
            -13,  -17,    7,    8,   29,   56,   47,   57,
            -27,  -27,  -16,  -16,   -1,   17,   -2,    1,
             -9,  -26,   -9,  -10,   -2,   -4,    3,   -3,
            -14,    2,  -11,   -2,   -5,    2,   14,    5,
            -35,   -8,   11,    2,    8,   15,   -3,    1,
             -1,  -18,   -9,   10,  -15,  -25,  -31,  -50,
        ],
        [ // king
            -65,   23,   16,  -15,  -56,  -34,    2,   13,
             29,   -1,  -20,   -7,   -8,   -4,  -38,  -29,
             -9,   24,    2,  -16,  -20,    6,   22,  -22,
            -17,  -20,  -12,  -27,  -30,  -25,  -14,  -36,
            -49,   -1,  -27,  -39,  -46,  -44,  -33,  -51,
            -14,  -14,  -22,  -46,  -44,  -30,  -15,  -27,
              1,    7,   -8,  -64,  -43,  -16,    9,    8,
            -15,   36,   12,  -54,    8,  -28,   24,   14,
        ],
    ];
    
    /// The endgame tables, by type: pawn, knight, bishop, rook, queen and king, each a8 first, as White sees the board.
    #[rustfmt::skip]
    pub const EG_TABLES: [[i32; 64]; 6] = [
        [ // pawn
              0,    0,    0,    0,    0,    0,    0,    0,
            178,  173,  158,  134,  147,  132,  165,  187,
             94,  100,   85,   67,   56,   53,   82,   84,
             32,   24,   13,    5,   -2,    4,   17,   17,
             13,    9,   -3,   -7,   -7,   -8,    3,   -1,
              4,    7,   -6,    1,    0,   -5,   -1,   -8,
             13,    8,    8,   10,   13,    0,    2,   -7,
              0,    0,    0,    0,    0,    0,    0,    0,
        ],
        [ // knight
            -58,  -38,  -13,  -28,  -31,  -27,  -63,  -99,
            -25,   -8,  -25,   -2,   -9,  -25,  -24,  -52,
            -24,  -20,   10,    9,   -1,   -9,  -19,  -41,
            -17,    3,   22,   22,   22,   11,    8,  -18,
            -18,   -6,   16,   25,   16,   17,    4,  -18,
            -23,   -3,   -1,   15,   10,   -3,  -20,  -22,
            -42,  -20,  -10,   -5,   -2,  -20,  -23,  -44,
            -29,  -51,  -23,  -15,  -22,  -18,  -50,  -64,
        ],
        [ // bishop
            -14,  -21,  -11,   -8,   -7,   -9,  -17,  -24,
             -8,   -4,    7,  -12,   -3,  -13,   -4,  -14,
              2,   -8,    0,   -1,   -2,    6,    0,    4,
             -3,    9,   12,    9,   14,   10,    3,    2,
             -6,    3,   13,   19,    7,   10,   -3,   -9,
            -12,   -3,    8,   10,   13,    3,   -7,  -15,
            -14,  -18,   -7,   -1,    4,   -9,  -15,  -27,
            -23,   -9,  -23,   -5,   -9,  -16,   -5,  -17,
        ],
        [ // rook
             13,   10,   18,   15,   12,   12,    8,    5,
             11,   13,   13,   11,   -3,    3,    8,    3,
              7,    7,    7,    5,    4,   -3,   -5,   -3,
              4,    3,   13,    1,    2,    1,   -1,    2,
              3,    5,    8,    4,   -5,   -6,   -8,  -11,
             -4,    0,   -5,   -1,   -7,  -12,   -8,  -16,
             -6,   -6,    0,    2,   -9,   -9,  -11,   -3,
             -9,    2,    3,   -1,   -5,  -13,    4,  -20,
        ],
        [ // queen
             -9,   22,   22,   27,   27,   19,   10,   20,
            -17,   20,   32,   41,   58,   25,   30,    0,
            -20,    6,    9,   49,   47,   35,   19,    9,
              3,   22,   24,   45,   57,   40,   57,   36,
            -18,   28,   19,   47,   31,   34,   39,   23,
            -16,  -27,   15,    6,    9,   17,   10,    5,
            -22,  -23,  -30,  -16,  -16,  -23,  -36,  -32,
            -33,  -28,  -22,  -43,   -5,  -32,  -20,  -41,
        ],
        [ // king
            -74,  -35,  -18,  -18,  -11,   15,    4,  -17,
            -12,   17,   14,   17,   17,   38,   23,   11,
             10,   17,   23,   15,   20,   45,   44,   13,
             -8,   22,   24,   27,   26,   33,   26,    3,
            -18,   -4,   21,   24,   27,   23,    9,  -11,
            -19,   -3,   11,   21,   23,   16,    7,   -9,
            -27,  -11,    4,   13,   14,    4,   -5,  -17,
            -53,  -34,  -21,  -11,  -28,  -14,  -24,  -43,
        ],
    ];
    engine.js · 9 lines
    // Chessmate 2026's engine, for the WebAssembly worker, ../worker.js, which scripts/chess/build.mjs writes after this into
    // public/machine/chess-wasm-worker.js: its name, its two modules, for one thread and for every core, and how a search's
    // request becomes the module's think, with every option chessmate.v2.worker takes, NaN for one not given.
    self.wasmEngine = {
      name: 'chessmate',
      modules: ['chessmate.wasm', 'chessmate-threads.wasm'],
      think: (wasm, msg, given) => wasm.think(msg.ms ?? 2000, given(msg.maxMs), given(msg.minMs), msg.depth ?? 64, given(msg.plyLimit),
        given(msg.nodes), msg.contempt ?? 0, msg.trades ?? 0, msg.selfPlay ? 1 : 0),
    };
    worker.js · 238 lines
    // The background search of the engines in WebAssembly, Chessmate 2026's for /chessmate and Checkers' for /checkers:
    // public/machine/chess-wasm-worker.js and public/machine/checkers-wasm-worker.js, each this file after its engine's
    // engine.js (src/wasm/chessmate, src/wasm/checkers), which names the engine, its two modules, for one thread and for
    // every core, and how a search's request becomes the module's think; scripts/chess/build.mjs compiles each engine's Rust
    // into its modules and writes its worker. It answers the page's messages as the ClojureScript builds' workers answer them
    // (chessmate.v2.worker, checkers.worker; add-chessmate-v2 D41): a search's started, info and best replies, evaluate,
    // perft, clear and ping, and an error for anything else. The engine, the whole search, is the module's; this file only
    // carries messages to it and its replies back.
    //
    // Like the ClojureScript workers, it lives on between the engine's moves, so that its transposition table carries what
    // it learnt thinking about one move into the next.
    //
    // Where the page is cross-origin isolated, so that workers may share memory, it searches on every core the browser
    // reports: it runs the threaded module on a memory it shares with a helper for each other core, a worker of this same
    // file, which searches beside it into the same table (each engine's threads.rs). Elsewhere it runs the module for one
    // thread alone, which plays node for node as the ClojureScript does. There the page gives up a search by number, through
    // a word of the shared
    // memory this worker hands it (a control reply), and the worker is free for the next at once; elsewhere, by ending the
    // worker. Helpers whose worker has ended stop once its heartbeat stops, and then end themselves (threads.rs).
    //
    // The tests run this file under Node, as the body of a function whose parameter is `self`: they hand it the compiled
    // module as self.engineModule, a clock as self.engineClock, and for threads self.engineThreads, the threaded module, the
    // number of threads and a function that starts a helper.
    (() => {
      const ENGINE = self.wasmEngine; // engine.js's: { name, modules: [one thread's, every core's], think(wasm, msg, given) }
      const MAX_THREADS = 32; // threads.rs's
      // The threaded module's memory, in 64 KB pages: 2 MB at first, and at most 256 MB, as build.mjs links it
      const PAGES = 32, MAX_PAGES = 4096;
      const STACK = 1 << 20; // each helper's stack, as large as the main thread's
      const READY_WAIT_MS = 2000; // how long the first search waits for the helpers to start
      const BEAT_MS = 250; // how often the main thread's worker beats while it waits for the page
      const SILENT_WAITS = 2; // how many seconds of silence end a waiting helper
      const clock = self.engineClock || (() => performance.now());
    
      if (/^\w+ helper \d+$/.test(self.name ?? '')) {
        helper();
        return;
      }
    
      const decoder = new TextDecoder();
      const encoder = new TextEncoder();
      let wasm = null; // the module's exports, once it has started
      let memory = null; // its memory
      let helpers = []; // the helpers' workers
      let beating = null; // the heartbeat's timer, while there are helpers
      let control = null; // where memory is shared: a view of its words, and SEARCH's index in it (threads.rs's CONTROL)
      let failed = null; // why it could not start
      let searching = null; // the id of the search under way, whose iterations on_iteration reports
      let waiting = []; // the messages that came while it was starting
    
      const post = (reply) => self.postMessage(reply);
    
      /** The module's buffer, len bytes of it. Its memory grows as the engine needs, which replaces memory.buffer, so the
       * view is made afresh each time, once the buffer's address is known. */
      function buffer(len) {
        const at = wasm.io();
        return new Uint8Array(memory.buffer, at, len);
      }
    
      /** Text the module wrote into its buffer, len bytes; copied out first, since a TextDecoder reads no shared memory. */
      const read = (len) => decoder.decode(buffer(len).slice());
    
      /** Write text into the module's buffer, and return its length in bytes. */
      function write(text) {
        const bytes = encoder.encode(String(text));
        buffer(bytes.length).set(bytes);
        return bytes.length;
      }
    
      /** A search's report as an info or best reply: the ClojureScript worker's fields, from the module's JSON. */
      const reply = (type, id, len) => ({ type, id, ...JSON.parse(read(len)) });
    
      const iteration = (len) => post(reply('info', searching, len));
    
      /** How many threads to search on: as many as the browser reports cores, or as the worker's address asks for
       * (chess-wasm-worker.js?threads=n), at most MAX_THREADS; and one where the page is not cross-origin isolated, since
       * workers may share memory only there. */
      function threadCount() {
        if (self.engineThreads) return Math.min(MAX_THREADS, self.engineThreads.count);
        if (!self.crossOriginIsolated) return 1;
        let asked = 0;
        try { asked = Math.floor(Number(new URL(self.location.href).searchParams.get('threads'))); } catch { /* no address */ }
        return Math.max(1, Math.min(MAX_THREADS, asked >= 1 ? asked : self.navigator?.hardwareConcurrency || 1));
      }
    
      /** A module, compiled: the one the tests gave, or the file beside this one. */
      async function load(file, given) {
        if (given) return given;
        const r = await fetch(new URL(file, self.location.href));
        if (!r.ok) throw new Error(`${file}: ${r.status}`);
        return WebAssembly.compile(await r.arrayBuffer());
      }
    
      /** One thread: the engine's first module, on its own memory. */
      async function startOne() {
        const instance = await WebAssembly.instantiate(await load(ENGINE.modules[0], self.engineModule), { env: { now: clock, on_iteration: iteration } });
        return [instance.exports, instance.exports.memory];
      }
    
      /** n threads: the engine's threaded module, on a memory shared with n - 1 helpers, each handed the module, the memory
       * and a stack of its own. They start before the first search: a worker that is searching cannot start another. */
      async function startThreads(n) {
        const module = await load(ENGINE.modules[1], self.engineThreads?.module);
        const shared = new WebAssembly.Memory({ initial: PAGES, maximum: MAX_PAGES, shared: true });
        const words = new Int32Array(shared.buffer);
        const wake = (at) => Atomics.notify(words, at >>> 2);
        const x = (await WebAssembly.instantiate(module, { env: { memory: shared, now: clock, on_iteration: iteration, wake } })).exports;
        const at = x.control() >>> 2; // SEQ, then PULSE, SEARCH and ABANDONED
        beating = setInterval(() => Atomics.add(words, at + 1, 1), BEAT_MS); // the search beats for itself
        beating.unref?.(); // under Node, the tests', it keeps nothing running
        const spawn = self.engineThreads?.spawn ?? ((name) => new Worker(self.location.href, { name }));
        const started = [];
        for (let t = 1; t < n; t++) {
          const w = spawn(`${ENGINE.name} helper ${t}`);
          helpers.push(w);
          started.push(new Promise((done) => {
            w.onmessage = done; // ready, or failed: either way no longer starting
            w.onerror = (e) => { e?.preventDefault?.(); done(); };
          }));
          w.postMessage({ module, memory: shared, thread: t, stack: x.stack(STACK) });
        }
        x.helpers(helpers.length);
        await Promise.race([Promise.all(started), new Promise((r) => setTimeout(r, READY_WAIT_MS))]);
        control = { words, search: at + 2 };
        post({ type: 'control', abandoned: new Int32Array(shared.buffer, 4 * (at + 3), 1) }); // for the page to give searches up
        return [x, shared];
      }
    
      function endHelpers() {
        clearInterval(beating);
        helpers.forEach((w) => w.terminate());
        helpers = [];
        control = null;
      }
    
      function flush() {
        const queued = waiting;
        waiting = [];
        queued.forEach(handle);
      }
    
      function start() {
        endHelpers();
        wasm = null;
        memory = null;
        const threads = threadCount();
        if (threads === 1 && self.engineModule) { // the tests', at once
          const instance = new WebAssembly.Instance(self.engineModule, { env: { now: clock, on_iteration: iteration } });
          [wasm, memory] = [instance.exports, instance.exports.memory];
          return;
        }
        const one = () => { endHelpers(); return startOne(); };
        (threads > 1 ? startThreads(threads).catch(one) : startOne())
          .then(([x, m]) => { [wasm, memory] = [x, m]; }, (e) => { failed = `the WebAssembly engine could not start: ${e?.message || e}`; })
          .then(flush);
      }
    
      /** A helper: the threaded module on the main thread's memory, with its own stack, which waits for each search to
       * start and searches it until it ends (threads.rs). It never returns to its messages: the worker that started it
       * ends it, or, when that worker has ended and its heartbeat has been silent for SILENT_WAITS seconds, it ends itself. */
      function helper() {
        self.onmessage = ({ data: { module, memory: shared, thread, stack } }) => {
          let x;
          try {
            x = new WebAssembly.Instance(module, { env: { memory: shared, now: clock, on_iteration() {}, wake() {} } }).exports;
            x.__stack_pointer.value = stack;
          } catch (e) {
            self.postMessage({ type: 'error', thread, message: e?.message || String(e) });
            return;
          }
          const words = new Int32Array(shared.buffer), seq = x.control() >>> 2, pulse = seq + 1;
          self.postMessage({ type: 'ready', thread });
          let beat = Atomics.load(words, pulse), silent = 0;
          for (let seen = 0; ;) {
            if (Atomics.wait(words, seq, seen, 1000) === 'timed-out') {
              const now = Atomics.load(words, pulse);
              if (now !== beat) [beat, silent] = [now, 0];
              else if (++silent >= SILENT_WAITS) { self.close(); return; }
              continue;
            }
            seen = Atomics.load(words, seq);
            x.help(thread);
            [beat, silent] = [Atomics.load(words, pulse), 0];
          }
        };
      }
    
      /** Set the board to a request's position: its FEN, then its moves. */
      function setPosition(msg) {
        const refused = wasm.set_position(write(msg.fen));
        if (refused < 0) throw new Error(read(-refused));
        for (const m of msg.moves ?? []) {
          const illegal = wasm.play(write(m));
          if (illegal < 0) throw new Error(read(-illegal));
        }
      }
    
      const given = (x) => (x == null ? NaN : Number(x));
    
      /** Answer one message from the page, as the ClojureScript workers' handle does. */
      function handle(msg) {
        const kind = msg?.type, id = msg?.id;
        if (!wasm && !failed) { waiting.push(msg); return; }
        try {
          if (failed && kind !== 'ping') throw new Error(failed);
          switch (kind) {
            case 'ping': post({ type: 'pong', id }); break;
            case 'search': {
              post({ type: 'started', id }); // at once: the page waits 5 s for this, not for a result
              setPosition(msg);
              searching = id;
              if (control) Atomics.store(control.words, control.search, msg.seq ?? 0); // its number, by which the page may give it up
              const len = ENGINE.think(wasm, msg, given);
              post(reply('best', id, len));
              break;
            }
            case 'evaluate': setPosition(msg); post({ type: 'evaluate', id, score: wasm.evaluate() }); break;
            case 'perft': {
              setPosition(msg);
              const t = performance.now(), nodes = wasm.perft(msg.depth);
              post({ type: 'perft', id, nodes, ms: performance.now() - t });
              break;
            }
            case 'clear': wasm.clear(); post({ type: 'cleared', id }); break;
            default: post({ type: 'error', id, message: `unknown message type ${kind == null ? 'nil' : JSON.stringify(kind)}` });
          }
        } catch (e) {
          post({ type: 'error', id, message: e?.message || String(e) });
          if (e instanceof WebAssembly.RuntimeError) start(); // a trap leaves the engine half way: start afresh
        } finally {
          searching = null;
          if (control) Atomics.store(control.words, control.search, 0);
        }
      }
    
      self.onmessage = (e) => handle(e.data);
      start();
    })();
    Cargo.toml · 24 lines
    # Chessmate 2026's engine in Rust, compiled to WebAssembly for /chessmate: a port of chessmate.v2's board,
    # evaluation and search (src/machine/chess/chessmate/v2), which plays node for node as they do. scripts/chess/build.mjs
    # compiles it into public/machine/chessmate.wasm, and with its threads feature into public/machine/chessmate-threads.wasm,
    # which searches on every core.
    [package]
    name = "chessmate"
    version = "2026.1.0"
    edition = "2021"
    publish = false
    
    [lib]
    crate-type = ["cdylib"]
    path = "src/lib.rs"
    
    [features]
    # The threaded build, chessmate-threads.wasm, which build.mjs compiles with atomics and shared memory: the helpers'
    # exports, the host's `wake`, and an allocator for many threads (lib.rs, threads.rs).
    threads = []
    
    [profile.release]
    opt-level = 3
    lto = true
    codegen-units = 1
    panic = "abort"