Maackesgeist: A Chess Engine for Raumschach
Version 2 — A Fully Documented Engine with Quiescence Search, Transposition Table, Iterative Deepening, Allocation-Free Search, Incremental Hashing, Aspiration Windows, and Null-Move Pruning
Raumschach — literally “space chess” in German — was invented by Ferdinand Maack of Hamburg and first published in 1907. It is played on a 5×5×5 board comprising five stacked 5×5 levels, and introduces two pieces absent from orthodox chess: the Bishop, restricted to face-diagonals, and the Unicorn, restricted to space-diagonals (triagonals). The Queen combines all three sliding directions of three-dimensional space. Despite more than a century of theoretical attention — most authoritatively chronicled by Anthony Dickins (1914–1987) in A Guide to Fairy Chess (1969–71) — computer opposition for Raumschach has remained underdeveloped and underdocumented.
Maackesgeist is the AI component of a browser-based Raumschach implementation, written entirely in JavaScript and executable without server infrastructure. This paper documents Version 2 of the engine, a substantial revision of the original. It is organized as follows: §2 corrects a priority claim made in the Version 1 documentation; §3 describes the board representation and geometry; §4 covers move generation; §5 presents the complete algorithm with pseudo-code; §6 describes the evaluation function; §7 identifies current weaknesses; and §8 proposes improvements for Version 3.
Maackesgeist Version 1 was not the first publicly playable AI opponent for Raumschach. Most have been described as using minimax with alpha-beta pruning, and at least one was claimed to use a custom algorithm for Raumschach. To the best of the knowledge of the author, all have were introduced without publishing implementation details, evaluation functions, or pseudo-code.
In contrast, Maackesgeist is — to the best of the present author’s knowledge — the most fully documented Raumschach engine, and the first for which a complete technical account (including board representation, direction geometry, legal move generation, evaluation function, and the full search algorithm with pseudo-code) has been made publicly available. No published account of any other Raumschach engine’s internals is known. The absence of such documentation in other implementations makes meaningful comparison of playing strength or algorithmic novelty impossible; Maackesgeist Version 2 is presented here in the spirit of reproducibility and open scholarship.
It should also be noted that Maackesgeist Version 2 goes considerably further than bare minimax with alpha-beta pruning. The additions of quiescence search, a Zobrist transposition table with incremental hashing, iterative deepening with aspiration windows, null-move pruning, an allocation-free search tree (make/unmake throughout, including legal move generation), killer move heuristics, a history heuristic, and mobility evaluation collectively represent a substantial advance in documented technique for this game, regardless of what undocumented engines may have implemented.
The board is a three-dimensional JavaScript array board[l][r][f], where l ∈ {0–4} indexes the level (A through E), r ∈ {0–4} the rank (1–5), and f ∈ {0–4} the file (a–e). Each occupied cell holds a plain object { type, color }. For the game logic (rendering, move execution, legality checking), the board is still cloned via deep copy. Inside the search tree, however, Version 2 replaces all cloning with in-place make/unmake, as described in §5.1.
| Name | Count | Description | Pieces |
|---|---|---|---|
| Orthogonal (RD) | 6 | ±1 along exactly one axis | Rook, Queen |
| Face-diagonal (BD) | 12 | ±1 along exactly two axes | Bishop, Queen |
| Space-diagonal (UD) | 8 | ±1 along all three axes | Unicorn, Queen |
| All (QD) | 26 | Union of the above | Queen, King |
The Knight extends to three dimensions via all permutations of (±2, ±1, 0), yielding 24 distinct leap directions. All direction sets are precomputed as constant arrays at startup.
Move generation is unchanged from Version 1 at the pseudo-legal level. pieceMoves generates pseudo-legal moves for a single piece via ray-casting (sliders) or single-step enumeration (King, Knight, Pawn). allPseudo aggregates over all pieces of a given color. legalMoves filters pseudo-legal moves using make/unmake — it applies each candidate move in-place, tests whether the moving side’s King is in check, and immediately unmakes the move, producing no heap allocations. isAttacked performs a reverse-ray trace to determine whether a given square is attacked by a given color. The entire search tree, from root to quiescence leaf, is now allocation-free.
Version 2 replaces the clone-based board copy inside the search with in-place make and unmake operations. makeMove moves the piece object directly on the board array and returns an undo record {captured, origType, piece} sufficient to reverse the operation. unmakeMove restores the board to its prior state using that record. Promotion is handled by replacing the piece object; unmake restores the original.
A deterministic linear congruential generator (seed 0xDEADBEEF, multiplier 1664525, addend 1013904223) produces 1,750 pseudo-random 32-bit keys: one per (piece type × color × square) triple, plus one for the side to move. The root hash is computed from scratch once per move decision by iterating all 125 cells. All subsequent hashes inside the search tree are updated incrementally in O(1): hashMove XORs out the moving piece at its origin, XORs out any captured piece at the destination, XORs in the moving piece (or its promotion) at the destination, and flips the side key. Unmake requires no hash work because the parent hash is simply passed down the call stack and the child hash is discarded when the recursive call returns.
A 131,072-entry (217) flat array serves as the transposition table, indexed by hash & TT_MASK. Each entry stores the hash, search depth, score, a flag (EXACT / LOWER / UPPER), and the best move found at that node. The replacement policy is depth-preferred: a new entry replaces an existing one only if it was computed at greater or equal depth.
Moves are scored and sorted in descending order before being searched. The priority stack is:
| Priority | Condition | Score |
|---|---|---|
| 1 | TT best move | 2,000,000 |
| 2 | Capture (MVV/LVA: victim value × 10 − attacker value) | 1,000,000 + Δ |
| 3 | Killer move slot 0 at current ply | 900,000 |
| 4 | Killer move slot 1 at current ply | 890,000 |
| 5 | History heuristic (accumulated depth² on beta cutoffs) | 0 – 32,000 |
Killers are stored per ply in a two-slot array; each beta cutoff on a quiet move promotes that move into the killer table and increments its history score. The history table is aged (halved) between root calls to favour recent search experience.
At depth zero, rather than calling evalBoard directly, the search enters quiescence. Quiescence computes a stand-pat score from the static evaluation, then extends the search through all available captures (up to 6 additional ply) until no captures remain. This eliminates the horizon effect: the engine no longer mistakes a tactically unstable position for a quiet one merely because it lies at the nominal search boundary.
The search receives the current board hash as a parameter rather than recomputing it at every node. Each recursive call derives the child hash via hashMove (O(1)). Null-move pruning is applied before the move loop: if the position is not in check, is not King-and-Pawns-only (which would risk a spurious zugzwang cutoff), and the remaining depth is at least 3, the engine grants the opponent a free move by flipping the side key without altering the board, then searches at depth−3 with a one-move-wide window. A cutoff at this reduced depth indicates the position is already strong enough that full search is unnecessary. The nullOk flag prevents consecutive null moves.
Each depth pass is wrapped in a narrow aspiration window centred on the score returned by the previous pass. For depth 1, a full [−∞, +∞] window is used since no prior score exists. For depth 2 and beyond, the window is [prevScore−50, prevScore+50]. If the search returns a score outside that window (a fail-low or fail-high), the pass is immediately re-searched at full width. The full-width re-search benefits from the improved move ordering and TT entries deposited by the failed narrow search, so its cost is modest. The root hash is computed once from scratch; all recursive calls derive child hashes incrementally via hashMove.
| Level | Name | Max Depth | Character |
|---|---|---|---|
| 0 | Beginner | 0 | Random legal move; no search |
| 1 | Weak | 2 | Iterative deepening to 2 plies + quiescence |
| 2 | Average | 3 | Iterative deepening to 3 plies + quiescence |
| 3 | Strong | 4 | Iterative deepening to 4 plies + quiescence |
| 4 | RSM | 5 | Iterative deepening to 5 plies + quiescence |
| 5 | GRSM | 6 | Iterative deepening to 6 plies + quiescence |
The evaluation function returns a score in centipawn units from White’s perspective. It combines material, centrality bonuses, piece-specific positional adjustments, and a mobility term.
True Zobrist hashing, as described in the original 1970 paper, uses 64-bit random keys to make hash collisions negligibly rare. Maackesgeist uses 32-bit keys because JavaScript’s native integers are 32-bit. With a 217-entry table and 32-bit keys, the probability of a spurious collision — where two different positions share the same hash index and the same 32-bit key fragment — is non-trivial over the course of a long game. Collisions cause the engine to return a cached score for the wrong position, potentially corrupting the search. 64-bit emulation via two 32-bit integers (as used by engines such as Stockfish in pure JavaScript ports) would reduce collision risk dramatically.
The current replacement policy (depth-preferred: replace only if new entry is at least as deep) has a known pathology: it allows old, deep entries from earlier positions to persist and block newer, shallower but more relevant entries for the current position. In practice, engines use hybrid replacement schemes — for example, a two-bucket system with one always-replace slot and one depth-preferred slot — to ensure the TT remains fresh without discarding valuable deep results.
Late move reductions (LMR) apply a reduced search depth to moves that appear late in the ordered move list, on the reasoning that well-ordered moves leave only poor or irrelevant moves toward the end and these do not require full-depth verification. LMR is one of the most effective single techniques in modern chess engine design and is absent from Maackesgeist.
The quiescence search in Version 2 extends only on captures. Check evasions and checking moves, which may be as tactically forcing as captures, are not searched in quiescence. This means that a position where the opponent is forced to deal with an immediate check — but where no captures are involved — may be misevaluated at the leaf. Extending quiescence on checks (with a depth limit to prevent explosion) would improve accuracy in tactical positions involving sacrifices and forcing check sequences.
Delta pruning is a quiescence-specific optimization: if even capturing the most valuable available piece cannot raise the score above alpha, the position is hopeless and further capture search is skipped. Without delta pruning, the quiescence search explores captures that have no chance of improving the score, wasting time on clearly losing capture attempts.
The evaluation function now includes material, centrality, piece-specific bonuses, and mobility, but remains blind to King safety, pawn structure, and game-phase awareness. The engine has no incentive to avoid exposing its King beyond what is enforced by check detection at the leaves. Doubled, isolated, and passed pawns are unrecognised. Piece values and weights do not adjust as the game transitions from opening to middlegame to endgame.
The engine plays the opening from scratch on each game, without a library of sound opening sequences. This makes its early play random-seeming and often structurally weak relative to even a modest opening book. In the endgame, tablebases could provide perfect play in positions with few pieces; the engine currently relies entirely on search, which at 4-ply depth may fail to find the correct winning plan in positions requiring precise technique.
The difficulty levels correspond to fixed search depths. The engine does not adapt to how much time remains on the clock. A player using the Classical time control (90+30) can engage the Strong engine and experience slow thinking; a player on Blitz (5+3) faces the same Strong engine playing at the same depth, regardless of time pressure. A time-management layer that allocates a fraction of remaining clock time to each move, and terminates iterative deepening when that budget is exhausted, would be a substantial practical improvement.
The improvements are ordered by estimated impact.
Priority 1 (highest): Two-bucket transposition table. Maintain two entries per TT slot: one depth-preferred (the current behavior) and one always-replace. This keeps the table responsive without sacrificing deep results.
Priority 2: Delta pruning in quiescence. Before searching a capture, check whether even the maximum possible material gain can raise the score above alpha; if not, skip the move. This eliminates clearly futile capture searches.
Priority 3: Check extensions in quiescence. Extend quiescence search when the side to move is in check, up to a small additional depth (2 plies). This prevents misevaluation of positions where the only forcing sequence involves checks without captures.
Priority 4: Late move reductions. After the first few moves at each node, reduce the search depth for moves ranked low in the ordered list. Re-search at full depth only if the reduced search raises alpha. LMR is among the most effective techniques in modern engine design and scales well with the high branching factor of Raumschach’s 3D search tree.
Priority 5: Time management. Replace fixed-depth search with an iterative-deepening loop that terminates when a per-move time budget is exhausted. Allocate time dynamically based on remaining clock and estimated moves to the control, with bonus time for positions where the previous iteration’s score fluctuated significantly.
Priority 6: 64-bit Zobrist keys. Emulate 64-bit integers using pairs of 32-bit values (or adopt BigInt) to reduce hash collision probability to negligible levels.
Maackesgeist Version 2 represents a substantial advance over Version 1. The addition of quiescence search eliminates the most serious class of tactical errors. The Zobrist transposition table prevents redundant computation across transpositions, and its hash is now maintained incrementally in O(1) per node rather than recomputed from scratch. Iterative deepening with aspiration windows provides better move ordering at each depth while pruning substantially more nodes on narrowed passes. Null-move pruning delivers an approximate two-ply effective-depth bonus at minimal cost. The three-tier ordering system (MVV/LVA, killer moves, history heuristic) further improves alpha-beta pruning efficiency. The mobility term adds a meaningful positional signal beyond material and centrality. In-place make/unmake — now applied throughout the entire search, including legal move generation — makes the search tree entirely allocation-free.
At the same time, the weaknesses identified in §7 are real. The most pressing are the absence of late move reductions, delta pruning in quiescence, and check extensions. Beyond that, the absence of time management, a two-bucket transposition table, and 64-bit Zobrist keys leaves Version 2 short of what is achievable within the constraints of a browser-native single-file implementation. Version 3 should address at least the first four priorities in §8.
Among documented Raumschach engines — the only class on which comparison is possible — Maackesgeist remains the most technically detailed implementation on public record. It is offered in the hope that further documentation of Raumschach computation, by this and other authors, will eventually bring to three-dimensional chess the analytical rigour that a century of theoretical literature has always deserved.