Project

OTEngine

I've played and competed in chess for years — including a regional over-the-board tournament win — so this project started less as “let's learn Rust” and more as wanting to understand, at the bitboard level, a game I already loved.

A traditional (non-neural) chess engine built from bitboard board representation up through search and evaluation: magic bitboards for O(1) sliding-piece move generation, negamax with alpha-beta pruning, iterative deepening, principal variation search, a Zobrist-hash-keyed transposition table, null-move pruning, late move reduction, and quiescence search with static exchange evaluation. Compiled to WebAssembly and running live in a browser Web Worker so the UI never freezes mid-search.

Loading engine…

How it works

OTEngine is a traditional (non-neural) UCI chess engine, the kind that competed against each other for decades before neural evaluation became standard — built from bitboard board representation up through search and evaluation.

Board Representation

The board is stored as bitboards — eight 64-bit integers, one bit per square, one per piece type plus one each for all-white and all-black pieces. A single AND between two bitboards finds every white knight at once; trailing_zeros() pulls individual squares off that result in O(1) per piece.

Alongside the bitboards, the engine keeps an 8x8 array for O(1) "what's on this square" lookups, a Zobrist hash (built by XOR-ing random numbers assigned to every piece/square combination, castling rights, en passant file, and side to move) that powers the transposition table, and an incrementally updated evaluation base so material and piece-square values are nearly free to maintain at every node.

Move Generation

Non-sliding pieces (pawns, knights, kings) use attack patterns precomputed at compile time — a single lookup plus a bitboard AND. Sliding pieces (bishops, rooks, queens) use magic bitboards: every possible attack pattern for every square and blocker configuration is precomputed once at startup into large lookup tables, and a "magic number" hashes the current occupancy into an index — giving a sliding piece's full attack set, blockers included, in O(1) regardless of how many pieces are on the board.

Moves are generated pseudo-legally first, then filtered by making the move, checking whether the king is left in check, and unmaking it if so — a deliberate simplicity/speed tradeoff over precomputing pins and checkers up front.

Search

The core search is negamax with alpha-beta pruning, layered with iterative deepening (searching depth 1, 2, 3… so a legal best move is always ready when the clock runs out), principal variation search (cheap zero-width searches for all but the first move, with a full re-search only if one turns out better), and a Zobrist-keyed transposition table that avoids redoing identical work across transposing move orders.

Null-move pruning tests "what if I just passed my turn" to prune branches that are still winning for the opponent even after a free move. Late move reduction searches later-ordered moves to a reduced depth first, re-searching at full depth only if they look promising. Move ordering (transposition table move, then MVV-LVA captures, then killer moves and history heuristic) is what makes all of this pruning effective in the first place.

Quiescence search extends past the nominal depth limit on captures only, using Static Exchange Evaluation and delta pruning to resolve tactical sequences accurately without the horizon effect — the classic failure mode of evaluating a position the instant after losing a queen but one ply before recapturing.

Evaluation

The evaluation function combines material and tapered piece-square tables (blending separate middlegame/endgame tables based on how much material remains), a nonlinear king-safety score based on attacked squares around the enemy king, mobility, and pawn-structure terms (doubled, isolated, backward, and passed pawns, with bonuses for passed pawns that can mutually support each other).

Additional terms include a bishop-pair bonus, rooks on open/semi-open files, and a mop-up evaluation that only activates with a decisive material lead — driving the winning king toward the center and the losing king toward the edge to actually convert won endgames.

Web Deployment

The same Rust engine runs in the browser without a second implementation. It's compiled to WebAssembly via wasm-bindgen, which generates a JS glue module exposing an EngineBridge class. Rather than a separate JS-facing API, EngineBridge exposes one send_command method that accepts the exact same UCI strings the engine already reads from stdin — the identical search and evaluation code runs whether it's talking to a terminal, Lichess, or a browser tab.

The WASM module is loaded inside a Web Worker rather than on the main thread, so a multi-second search never freezes the board, the clock, or any other UI. The main thread posts a search request with the move history and time-control parameters; the worker replies with the best move once the search completes.