Technical Documentation · Engine Design

Raumfischer: A Chess Engine for Raumschach

Version 1 — A Fischer-Style Heuristic Engine with Two-Bucket Transposition Table, Seven Positional Heuristics, Principal Variation Search, and a Fischer Personality Layer in Move Ordering
International Raumschach Federation · 2026
Raumfischer (German: “space Fischer”) is the AI component of a browser-based Raumschach implementation at raumschach.org. It is an upgrade and replacement of the Maackesgeist engine. It is written entirely in JavaScript and requires no server infrastructure. Raumfischer inherits Maackesgeist’s proven search architecture — allocation-free make/unmake, incremental Zobrist hashing, iterative deepening with aspiration windows, alpha-beta with null-move pruning, Principal Variation Search, quiescence search, and killer/history move ordering — while replacing the evaluation function in its entirety. The evaluation is rebuilt around seven heuristics derived from the documented playing principles of Robert James Fischer, each adapted to the geometry of the 5×5×5 space cube: (1) material with 3D-recalibrated values; (2) piece activity and dormancy; (3) spatial center control; (4) king safety via open slider rays and 3D pawn shelter; (5) pawn structure including passed pawns with quadratic advancement bonus, doubled pawns, level majority, and isolated pawn penalty; (6) rook activation on open level-, rank-, and file-lines; and (7) unicorn parity, a heuristic with no two-dimensional analogue. Move ordering is extended with a “Fischer personality layer” that awards quiet-move bonuses for center entry, rook activation, pawn advances, and unicorn-to-enemy-king-parity transitions. The transposition table is upgraded from a single depth-preferred bucket to a two-bucket scheme, addressing the primary structural deficiency identified in the Maackesgeist Version 2 roadmap. This article presents the complete algorithm in pseudocode — incorporating all search improvements from the initial release — documents all seven evaluation heuristics with their geometric rationale, identifies remaining open questions, and outlines the development agenda for Version 2.

1. Introduction

Raumschach — literally “space chess” in German — was invented by Dr. Ferdinand Maack (1861–1930) of Hamburg and first published in 1907. Its mature version is referred to as “Normal Form”, and is played on a 5×5×5 board composed of five stacked 5×5 levels, and introduces a new piece, the Unicorn, which is confined to space-diagonal (three-axis) rays. The Queen combines all three rider types. Despite more than a century of theoretical attention — documented most authoritatively by Anthony Dickins (1914–1987) in A Guide to Fairy Chess (1969–71) — computer opposition for Raumschach has remained underdeveloped and underdocumented.

Raumfischer is the successor to Maackesgeist as the AI component of raumschach.org. Where Maackesgeist established a technically complete search framework and a compact evaluation function, Raumfischer’s contribution is principally evaluative: a seven-component positional assessment grounded in the playing principles of Robert James Fischer (1943–2008), adapted to the geometry of three-dimensional chess. This article is organized as follows: §2 situates Raumfischer among known Raumschach engines; §3 describes board representation and geometry; §4 covers move generation; §5 presents the complete algorithm in pseudocode; §6 documents the evaluation function; §7 identifies remaining open questions; and §8 outlines the development agenda for Version 2.

The design premise of Raumfischer is that Bobby Fischer’s playing style — geometric clarity, piece activity, structural discipline, and relentless conversion — was never fundamentally about two-dimensional tricks. It was about spatial dominance and piece coordination, principles that translate intact into three dimensions. Each of the seven heuristics in §6 corresponds to a documented Fischer principle, together with its 3D geometric restatement.

2. Raumfischer and the Raumschach Engine Landscape

Several browser-playable AI opponents for Raumschach have been made publicly available over the years. Most have been described as using minimax with alpha-beta pruning; at least one has been presented as using a custom Raumschach algorithm. To the best of the present author’s knowledge, none has published implementation details, evaluation functions, or pseudocode.

Raumfischer is, to the best of the present author’s knowledge, the second fully documented Raumschach engine in the public record — after Maackesgeist, which established that record. The present article makes available a complete technical description of Raumfischer including board representation, direction geometry, legal move generation, the evaluation function with all seven heuristics and their weights, and the complete search algorithm in pseudocode. It is offered in the same spirit of reproducibility and open knowledge as its predecessor.

Relative to Maackesgeist Version 2, Raumfischer makes two categories of change. The first is structural: the transposition table is upgraded from a single depth-preferred bucket to a two-bucket scheme (depth-preferred plus always-replace), addressing the primary structural deficiency identified in the Maackesgeist Version 2 roadmap. The second is evaluative: the six-line material-centrality-mobility function is replaced in its entirety by a seven-component evaluation requiring approximately twenty times as many arithmetic operations per node. All other search infrastructure — make/unmake, Zobrist hashing, null-move pruning, quiescence search, iterative deepening, aspiration windows, and killer/history ordering — is inherited without modification.

3. Board Representation

The board is a JavaScript three-dimensional 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). An occupied cell contains a plain object { type, color }; an empty cell contains null. Piece types are the single-character strings K Q R B U N P; colors are 'w' and 'b'. For game logic (rendering, move execution, legality checking outside the search tree) the board is cloned by deep copy. Within the search tree, all cloning is replaced by in-place make/unmake as described in §5.1.

3.1 Geometric Direction Sets

NameCountDescriptionPieces
Orthogonal (RD)6±1 along exactly one axisRook, Queen
Face-diagonal (BD)12±1 along exactly two axesBishop, Queen
Space-diagonal (UD)8±1 along all three axesUnicorn, Queen
All (QD)26Union of the aboveQueen, King

The Knight extends to three dimensions through all permutations of (±2, ±1, 0), yielding 24 distinct jump directions from an interior cell. All direction sets are precomputed as constant arrays at startup. Three additional precomputed helpers are introduced by Raumfischer: cheby(l,r,f) (Chebyshev distance to Cc3, the geometric center of the cube); isInner(l,r,f) (membership test for the inner 3×3×3 cube of 27 cells); and uParity(l,r,f) (the value of (l+r+f) mod 2, which determines a Unicorn’s permanent parity class).

The choice of Chebyshev distance rather than Manhattan (taxicab) distance for centrality is deliberate and consequential. Chebyshev distance reflects the number of King moves required to reach the center, and therefore the mobility cost of any piece type that moves one step per axis. Manhattan distance over-penalizes edge cells along exactly one axis while under-penalizing true corner cells; Chebyshev distributes the penalty correctly for a piece that moves in all 26 directions.

4. Move Generation

Move generation is unchanged from Maackesgeist Version 2 at the pseudo-legal level. pieceMoves(board, l, r, f) generates pseudo-legal moves for a single piece by ray-casting (sliding pieces) or single-step enumeration (King, Knight, Pawn). allPseudo(board, color) aggregates over all pieces of a given color. legalMoves(board, color) filters pseudo-legal moves using make/unmake: each candidate move is applied in place, the moving side’s King is tested for check via isAttacked, and the move is immediately undone — producing no heap allocations. isAttacked(board, cell, byColor) performs reverse ray-casting from the target cell. The entire search tree, from root to quiescence leaf, is allocation-free.

Pawn promotion is handled inside pieceMoves: when a pawn reaches the promotion cell (level E, rank 5 for White; level A, rank 1 for Black), the move is emitted five times with prom set to each of Q R B U N. The human player selects the promotion piece via a modal; the engine always promotes to Queen (the highest-valued piece) in its own move generation, since getBestMove evaluates all legal moves including all five promotion variants.

5. The Algorithm

5.1 Make / Unmake

Raumfischer inherits Maackesgeist’s allocation-free in-place mutation scheme. makeMove(board, m) moves the piece object directly within the board array and returns an undo record sufficient to reverse the operation. unmakeMove(board, m, undo) restores the board to its prior state using that record. Promotion is handled by replacing the piece object at the destination; unmake restores the original pawn object.

The undo record is extended to include cached mobility data via a parallel array mobCache[l][r][f], which stores the pseudo-legal move count for every occupied cell. On makeMove, the cache entries for the origin, destination, and all cells whose slider ray passes through either square are invalidated and recomputed. On unmakeMove, the prior values are restored from the undo record. This converts the dominant evaluation cost of §6.2 from O(pieces × ray-length) per call to O(invalidated cells) per make/unmake — a substantial speedup at no cost to correctness.

FUNCTION makeMove(board, m)  undoRecord:
  captured  board[m.to]
  piece     board[m.from]
  IF m.prom:
    board[m.to]  {type: m.prom, color: piece.color}
  ELSE:
    board[m.to]  piece
  board[m.from]  null
  mobUndo  updateMobCache(board, m)  // invalidate and recompute affected cells
  RETURN {captured, piece, mobUndo}

FUNCTION unmakeMove(board, m, undo):
  board[m.from]  undo.piece     // restores moving piece (handles promotion)
  board[m.to]    undo.captured  // restores captured piece (or null)
  restoreMobCache(undo.mobUndo)   // restores cached move counts from undo record

5.2 Zobrist Hashing

A deterministic seeded linear congruential generator (seed 0xDEADBEEF, multiplier 1664525, addend 1013904223) produces 1,750 pseudorandom key pairs, each emulating a 64-bit integer as two unsigned 32-bit halves (hi and lo). Native JavaScript integers are 32-bit; the 64-bit emulation reduces the probability of a spurious hash collision — where two different positions share the same table index and the same key fragment — to negligible levels over the depths Raumfischer searches. The root hash is computed from scratch once per move decision by iterating all 125 cells. All subsequent hashes within 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 toggles the side key. Unmake requires no hash work because the parent hash is passed down the call stack and the child hash is discarded when the recursive call returns.

FUNCTION hashBoard(board, color)  hash:
  h  SIDE_KEY if color = Black else 0
  FOR EACH occupied cell (l, r, f):
    h  h XOR ZKEYS[piece.type, piece.color, l, r, f]
  RETURN h (unsigned 32-bit)

FUNCTION hashMove(hash, undo, m)  hash:    // O(1) incremental update
  hash  hash XOR ZKEYS[undo.piece.type, undo.piece.color, m.from]
  IF undo.captured:
    hash  hash XOR ZKEYS[undo.captured.type, undo.captured.color, m.to]
  hash  hash XOR ZKEYS[m.prom OR undo.piece.type, undo.piece.color, m.to]
  RETURN hash XOR SIDE_KEY (unsigned 32-bit)

5.3 Two-Bucket Transposition Table

Raumfischer upgrades Maackesgeist’s single depth-preferred transposition table to a two-bucket scheme, addressing Priority 1 of the Maackesgeist Version 2 improvement roadmap. Two flat arrays of 131,072 entries (217) are maintained in parallel: a depth-preferred bucket (ttDeep) that retains the deepest entry found so far for each slot, and an always-replace bucket (ttAlways) that unconditionally overwrites with the most recently computed entry. Both arrays are 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. On probe, ttDeep is checked first; if no match, ttAlways is checked. On store, ttAlways is always written; ttDeep is written only when the new entry is at least as deep as the existing one. This guarantees that valuable deep results are never discarded while ensuring that the table remains responsive to recently computed positions — the known pathology of the pure depth-preferred scheme.

Both ttDeep and ttAlways are reset to empty at the start of every new game, before getBestMove is first called. This eliminates any risk of cross-game transposition table contamination: a ttProbe hit in a new game cannot return cached scores from a different strategic context established in a prior game. The cost — a cold cache for the opening moves — is negligible in practice, as the table warms rapidly within the first move decision.

FUNCTION ttProbe(hash)  entry | null:
  i  hash AND TT_MASK
  IF ttDeep[i] exists AND ttDeep[i].hash = hash:   RETURN ttDeep[i]
  IF ttAlways[i] exists AND ttAlways[i].hash = hash: RETURN ttAlways[i]
  RETURN null

FUNCTION ttStore(hash, depth, score, flag, bestMove):
  i      hash AND TT_MASK
  entry  {hash, depth, score, flag, bestMove}
  ttAlways[i]  entry                              // unconditional
  IF ttDeep[i] is empty OR ttDeep[i].depth ≤ depth:
    ttDeep[i]  entry                             // depth-preferred

5.4 Move Ordering: The Fischer Personality Layer

Move ordering follows the same four-level priority stack as Maackesgeist: (1) TT best move, (2) captures ordered by MVV/LVA, (3) killer move slot 0, (4) killer move slot 1. Raumfischer extends the fifth level — the history heuristic score for quiet moves — with a set of Fischer-derived bonuses applied before the sort. These bonuses reflect the moves Fischer would prefer on positional grounds, independent of tactical forcing sequences.

PriorityConditionScore
1TT best move2,000,000
2Capture (MVV/LVA: victim value × 10 − attacker value)1,000,000 + Δ
3Killer move slot 0 at current ply900,000
4Killer move slot 1 at current ply890,000
5aCenter entry: destination in inner 3×3×3 cubehist + 55
5bCenter entry: destination is exact Cc3hist + 110
5cRook: destination is enemy home levelhist + 75
5dRook: destination on open level/rank/file linehist + 30–35
5ePawn advance: adv × 18 (adv = level + rank progress)hist + 0–144
5fUnicorn: destination matches enemy king’s parity classhist + 45
5gHistory heuristic (depth² accumulated at beta cutoffs)0–32,000
FUNCTION scoreMv(board, m, ply, ttBest)  integer:
  IF m = ttBest: RETURN 2000000
  victim  board[m.to]
  IF victim exists:
    attacker  board[m.from]
    RETURN 1000000 + VALUE[victim.type] × 10 − VALUE[attacker.type]
  IF m = killers[ply][0]: RETURN 900000
  IF m = killers[ply][1]: RETURN 890000
  // Fischer personality layer on top of history score
  s  history[m.from × 125 + m.to]
  mover  board[m.from]
  IF isInner(m.to): s += 55
  IF m.to = Cc3:    s += 110
  IF mover = Rook:
    IF m.to.level = enemyHomeLevel:     s += 75
    IF isOpenLine(board, m.to, LEVEL):   s += 35
    IF isOpenLine(board, m.to, RANK):    s += 30
    IF isOpenLine(board, m.to, FILE):    s += 30
  IF mover = Pawn:
    adv  (level + rank progress toward promotion)
    s += adv × 18
  IF mover = Unicorn:
    IF uParity(m.to) = uParity(enemyKing): s += 45
  RETURN s

5.5 Quiescence Search

At depth zero, the search enters quiescence rather than calling evalBoard directly. Quiescence computes a stand-pat score from the static evaluation, then extends the search through all available captures (up to 6 additional plies) until no captures remain. This eliminates the horizon effect: the engine does not mistake a tactically unstable position for a quiet one simply because it falls at the nominal search boundary. Stand-pat pruning applies in both directions: if the static score already satisfies the cutoff condition, the captures need not be explored.

Two extensions are applied within the capture loop. Delta pruning skips any capture where even the maximum possible material gain — the victim’s value plus a safety margin — cannot raise the score above alpha; promotions and moves that give check are always exempted. Check extensions give one extra ply of quiescence search when the moving side delivers check, up to a maximum of two additional plies beyond the nominal quiescence budget; this prevents misevaluation of forcing sequences that proceed through checks rather than captures.

FUNCTION quiesce(board, alpha, beta, isMaximizing, qdepth)  score:
  standPat  evalBoard(board)
  IF qdepth = 0: RETURN standPat
  IF isMaximizing:
    IF standPat ≥ beta:  RETURN standPat  // stand-pat cutoff
    alpha  max(alpha, standPat)
  ELSE:
    IF standPat ≤ alpha: RETURN standPat  // stand-pat cutoff
    beta  min(beta, standPat)
  captures  allPseudo(board, color) FILTERED TO captures only
  IF captures is empty: RETURN standPat
  orderMoves(board, captures, MAX_PLY−1, null)
  best  standPat
  FOR EACH cap IN captures:
    // Delta pruning: skip if even the best gain cannot reach alpha
    victim  board[cap.to]
    IF NOT cap.prom:
      IF isMaximizing AND standPat + VALUE[victim.type] + DELTA_MARGIN < alpha: CONTINUE
      IF NOT isMaximizing AND standPat − VALUE[victim.type] − DELTA_MARGIN > beta: CONTINUE
    undo  makeMove(board, cap)
    IF inCheck(board, color): unmakeMove(board, cap, undo); CONTINUE
    // Check extension: +1 ply when this move gives check (bounded at qdepth ≤ CHECK_EXT_MAX)
    ext  1 if inCheck(board, opponent) AND qdepth ≤ CHECK_EXT_MAX else 0
    score  quiesce(board, alpha, beta, NOT isMaximizing, qdepth − 1 + ext)
    unmakeMove(board, cap, undo)
    IF isMaximizing:
      best  max(best, score); alpha  max(alpha, best)
      IF alpha ≥ beta: BREAK
    ELSE:
      best  min(best, score); beta  min(beta, best)
      IF beta ≤ alpha: BREAK
  RETURN best

5.6 Minimax with Alpha-Beta, Null-Move Pruning, Late Move Reductions, and Futility Pruning

The search receives the current board hash as a parameter rather than recomputing it at each 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 risks a spurious zugzwang cutoff), and the remaining depth is at least 3, the engine grants the opponent a free move by toggling the side key without altering the board, then searches to depth−3 with a one-move-wide window. A cutoff at this reduced depth indicates the position is already strong enough that a full search is unnecessary. The nullOk flag prevents consecutive null moves.

Two pruning techniques act within the move loop. Futility pruning, applied at depths 1 and 2, skips quiet non-promotion moves when the static evaluation of the current position plus a margin (250 cp at depth 1; 450 cp at depth 2) cannot reach alpha; captures, promotions, and moves into check are always exempted. Late Move Reductions (LMR) apply after the first k = 4 moves at full depth: subsequently ordered quiet moves are searched at depth−2 with a null window [alpha, alpha+1]; a full-depth re-search is triggered only when the reduced result fails high. Both techniques interact with the Fischer personality layer’s move ordering, which ensures that the best candidates appear early in the list, and are especially valuable given the 3D board’s high branching factor.

FUNCTION minimax(board, depth, alpha, beta, isMaximizing, ply, hash, nullOk=true)  score:
  color   White if isMaximizing else Black
  entry   ttProbe(hash)
  ttBest  entry.bestMove if entry exists else null
  // Transposition table cutoff
  IF entry exists AND entry.depth ≥ depth:
    IF entry.flag = EXACT: RETURN entry.score
    IF entry.flag = LOWER: alpha  max(alpha, entry.score)
    IF entry.flag = UPPER: beta   min(beta,  entry.score)
    IF alpha ≥ beta: RETURN entry.score
  // Leaf node: enter quiescence
  IF depth = 0: RETURN quiesce(board, alpha, beta, isMaximizing, 6)
  // Null-move pruning (R = 3)
  IF nullOk AND depth ≥ 3 AND NOT inCheck(board, color)
              AND NOT onlyKingAndPawns(board, color):
    nmScore  minimax(board, depth−3, beta−1, beta,
                        NOT isMaximizing, ply+1, hash XOR SIDE_KEY, false)
    IF isMaximizing AND nmScore ≥ beta:  RETURN beta
    IF NOT isMaximizing AND nmScore ≤ alpha: RETURN alpha
  moves  legalMoves(board, color)
  IF moves is empty:
    IF inCheck(board, color):
      RETURN (−20000 − depth×50) if isMaximizing    // spacemate against us
      RETURN (+20000 + depth×50) if NOT isMaximizing // spacemate for us
    RETURN 0  // stalemate
  orderMoves(board, moves, ply, ttBest)
  staticEval  evalBoard(board)  // for futility pruning gate
  origAlpha  alpha
  best      −∞ if isMaximizing else +∞
  bestMove  moves[0];  cutoff  false
  FOR EACH m IN moves (moveIndex = 0, 1, …):
    // Futility pruning: skip quiet moves at depth 1–2 when position is clearly lost
    IF depth ≤ 2 AND NOT isCapture(m) AND NOT m.prom AND NOT inCheck(board, color):
      IF isMaximizing AND staticEval + FUTILITY_MARGIN[depth] ≤ alpha: CONTINUE
      IF NOT isMaximizing AND staticEval − FUTILITY_MARGIN[depth] ≥ beta:  CONTINUE
    undo       makeMove(board, m)
    childHash  hashMove(hash, undo, m)
    // Late Move Reductions: reduce non-critical quiet moves after the first k
    IF moveIndex ≥ LMR_THRESHOLD AND depth ≥ 3
       AND NOT isCapture(m) AND NOT m.prom AND NOT inCheck(board, color):
      score  minimax(board, depth−2, alpha, alpha+1,
                          NOT isMaximizing, ply+1, childHash, false)
      IF (isMaximizing AND score ≤ alpha) OR (NOT isMaximizing AND score ≥ beta):
        unmakeMove(board, m, undo); moveIndex++; CONTINUE  // skip full re-search
    score  minimax(board, depth−1, alpha, beta,
                      NOT isMaximizing, ply+1, childHash)
    unmakeMove(board, m, undo)
    IF isMaximizing:
      IF score > best: best  score; bestMove  m
      alpha  max(alpha, best)
    ELSE:
      IF score < best: best  score; bestMove  m
      beta  min(beta, best)
    IF beta ≤ alpha:
      IF move is quiet:
        addKiller(m, ply)
        history[m]  min(32000, history[m] + depth²)
      cutoff  true; BREAK
    moveIndex++
  flag  LOWER if cutoff else (UPPER if best ≤ origAlpha else EXACT)
  ttStore(hash, depth, best, flag, bestMove)
  RETURN best

5.7 Iterative Deepening with Aspiration Windows and Principal Variation Search

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 onward, 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 pass, so its cost is modest. The root hash is computed once from scratch; all recursive calls derive child hashes incrementally via hashMove. King positions are cached once per root call in _wKingCache and _bKingCache for use by the Fischer personality layer inside scoreMv without triggering additional board scans inside the sort comparator.

Principal Variation Search (PVS) is applied within searchRoot. The first root move is searched with the full aspiration window [alpha, hi] to establish the principal variation. Every subsequent move is first probed with a null window [alpha, alpha+1]. A score that strictly exceeds alpha but falls below hi demonstrates that the move genuinely improves on the current best and escaped the null-window probe; only then is a full-window re-search [alpha, hi] triggered. Alpha is updated after each improvement, so every successive null-window probe is conducted against the tightest available lower bound. Given Raumfischer’s strong move ordering — TT best move, MVV/LVA, killers, and the Fischer personality layer — the first move is correct often enough that the null-window probes for the remaining moves confirm quickly and no re-search is required. The implementation change is localized entirely to searchRoot and does not affect minimax, quiesce, or the evaluation function.

FUNCTION getBestMove(board, color, maxDepth)  move:
  moves  legalMoves(board, color)
  IF moves is empty: RETURN null
  IF maxDepth = 0: RETURN randomChoice(moves)  // Beginner level
  // Reset killers; age history; cache king positions for scoreMv
  killers[0..MAX_PLY]  [null, null]
  history[all]         history[all] >> 1
  _wKingCache  findKing(board, White)
  _bKingCache  findKing(board, Black)
  isMaximizing  (color = White)
  rootHash      hashBoard(board, color)   // computed once
  bestMove      moves[0];  prevScore  0
  FUNCTION searchRoot(d, lo, hi)  {bs, bm}:
    bs     −∞ if isMaximizing else +∞;  bm  moves[0]
    alpha  lo
    FOR EACH m IN moves (moveIndex = 0, 1, …):
      undo   makeMove(board, m)
      ch     hashMove(rootHash, undo, m)
      IF moveIndex = 0:
        // PV move: full-window search to establish the principal variation
        score  minimax(board, d−1, alpha, hi, NOT isMaximizing, 1, ch)
      ELSE:
        // PVS: null-window probe — verify move is no better than current best
        score  minimax(board, d−1, alpha, alpha+1, NOT isMaximizing, 1, ch)
        IF score > alpha AND score < hi:
          // Fail-high on null window: genuine improvement; re-search at full width
          score  minimax(board, d−1, alpha, hi, NOT isMaximizing, 1, ch)
      unmakeMove(board, m, undo)
      IF isMaximizing AND score > bs: bs  score; bm  m; alpha  bs
      IF NOT isMaximizing AND score < bs: bs  score; bm  m
    RETURN {bs, bm}
  FOR d  1 TO maxDepth:
    orderMoves(board, moves, 0, bestMove)  // seed with last the best of the iteration
    IF d = 1:
      result  searchRoot(d, −∞, +∞)    // full window on first pass
    ELSE:
      lo  prevScore − 50;  hi  prevScore + 50
      result  searchRoot(d, lo, hi)
      IF result.bs ≤ lo OR result.bs ≥ hi:  // aspiration failed
        result  searchRoot(d, −∞, +∞)
    prevScore  result.bs;  bestMove  result.bm
  RETURN bestMove

5.8 Difficulty Levels

LevelNameMax DepthCharacteristic
0Beginner0Random legal move; no search
1Weak2Iterative deepening to 2 plies + quiescence
2Average3Iterative deepening to 3 plies + quiescence
3Strong4Iterative deepening to 4 plies + quiescence
4RSM5Iterative deepening to 5 plies + quiescence
5GRSM6Iterative deepening to 6 plies + quiescence

6. Evaluation Function — The Seven Fischer Heuristics

The evaluation function returns a score in centipawns from White’s perspective. It is the exclusive site of Raumfischer’s original contribution: seven heuristics derived from Fischer’s documented playing principles, each restated in terms of the geometry of the 5×5×5 space cube. The function iterates all 125 cells once to collect all pieces and locate both kings, then evaluates each heuristic in a single pass over the collected piece list. The dominant computational cost is heuristic §6.2, which calls pieceMoves for every piece to determine its mobility.

§6.1 — Material with 3D-Recalibrated Values

Fischer took material with clinical precision and almost never sacrificed without concrete compensation. Material values are recalibrated from Maackesgeist to reflect the geometry of three-dimensional movement.

PieceRaumfischer (cp)Maackesgeist (cp)Rationale
Pawn100100Unchanged
Knight51035024 jump targets from an interior cell (vs 8 in 2D); immunity to line-blocking
Bishop56035012 ray directions; color-locked to 1/4 of the board across level-file-rank
Unicorn290400Only purely 3D rider; covers only 30 of 125 cells over time; structurally unique
Rook4605006 ray directions; slightly reduced from 2D orthodoxy due to compact board
Queen16201200Combines all 26 directions; dominant in 3D space

Each piece also receives a positional bonus based on its Chebyshev distance to the geometric center Cc3 = (2,2,2) and on occupancy of the equatorial Level C (l = 2).

v ← MAT[type] + cen×wtype + (l=2 ? lvltype : 0)

where cen = 2 − cheby(l,r,f) takes values 0, 1, or 2, and the per-type weights wtype range from 25 (Bishop) to 80 (Queen). The Unicorn receives an additional +25 for occupancy of any cell within the inner 3×3×3 cube, reflecting its heightened mobility from that zone.

§6.2 — Piece Activity: Mobility and Dormancy

“Every piece must work.” Fischer constantly improved his worst piece, and he was quick to recognize that a piece with no good square was a structural liability. In Raumschach, this principle is geometrically acute: a Rook on corner cell Aa1 controls only 8 cells along 3 rays; the same Rook on Cc3 controls 24 cells along 6 rays. Edge and corner cells of the cube impose severe mobility penalties on all piece types.

Mobility bonus: +4 centipawns per pseudo-legal move. This term is computed from pieceMoves (which is already called during legal move generation) and rewards active pieces that exert influence over a large portion of the board.

Dormancy penalty: when a piece’s mobility falls below a type-specific threshold, a penalty of −8 centipawns per move below the threshold is applied. This fires hardest on sliders buried in the cube’s corners and edges.

PieceDormancy threshold
Rook4 pseudo-legal moves
Bishop5 pseudo-legal moves
Unicorn4 pseudo-legal moves
Queen10 pseudo-legal moves
Knight5 pseudo-legal moves
Pawn1 pseudo-legal move
§6.3 — Spatial Center Control

Fischer dominated the center before almost anything else. The three-dimensional center is the inner 3×3×3 cube of 27 cells, with Cc3 at its heart. Controlling this zone maximizes the mobility of every piece type simultaneously.

Two bonuses are awarded per piece:

Inner cube occupancy: +12 cp for any piece physically located within the inner 3×3×3 cube (i.e., all coordinates in {1,2,3}).

Level-C equator bonus: +6 cp for any piece on Level C (l = 2), the spatial equatorial plane. This is the 3D analogue of occupying the fourth and fifth ranks in orthodox chess.

§6.4 — King Safety

Fischer was obsessive about king safety. In Raumschach, the king faces up to 26 attack directions (against 8 in orthodox chess), making king exposure dramatically more dangerous. Two sub-heuristics are computed.

(a) Open slider rays: For each king, the engine traces all 26 ray directions outward from the king’s cell. Each unobstructed ray that terminates at an enemy slider (Rook on orthogonal ray, Bishop on face-diagonal ray, Unicorn or Queen on space-diagonal ray) is counted. Each such ray incurs a penalty of −20 cp.

(b) Three-dimensional pawn shelter: Friendly pawns within ±1 level and ±2 files of the king provide a spatial shield. Each such pawn adds +10 cp to king safety. The shelter radius is deliberately extended in the level axis (the axis of pawn advancement) to reflect that a pawn approaching from below or above provides meaningful cover in 3D.

§6.5 — Pawn Structure

Fischer had the finest pawn intuition in chess history, and treated pawn advances as long-term strategic investments. Three sub-heuristics are applied.

(a) Passed pawns: A pawn is passed in 3D if no enemy pawn can intercept it on any future (level, rank) combination, within ±1 file. The bonus is quadratic in advancement — reflecting Fischer’s understanding that passed pawns become exponentially more dangerous as they approach promotion:

bonus ← 30 + adv² × 5

where adv = l + r for White and adv = (4−l) + (4−r) for Black, ranging from 0 at the home rank to 8 at the promotion cell.

(b) Doubled pawns: Two same-color pawns sharing the same (file, rank) position on different levels cannot unblock each other and are structurally frozen. Each such pair incurs a penalty of −20 cp.

(c) Level pawn majority: Having more pawns than the opponent on a given level creates the potential for a passed pawn on that level. Each unit of majority across all five levels scores +9 cp.

(d) Isolated pawns: A pawn is isolated in 3D if no friendly pawn occupies any other level while sharing either its file or its rank — the 3D generalization of the classical isolated pawn definition. Concretely, a pawn at (l, r, f) is isolated when no friendly pawn exists at any (l′, r′, f) with l′ ≠ l (same file-column, any rank, any other level) and no friendly pawn exists at any (l′, r, f′) with l′ ≠ l (same rank-column, any file, any other level). Such a pawn has no structural support along either of the two planar axes through which it could be attacked or defended, a vulnerability that is geometrically more severe in 3D than in orthodox chess because the attacking approach can originate from any of three dimensions. Each isolated pawn incurs a penalty of −15 cp.

§6.6 — Rook Activation

Fischer’s rooks were always on open files or the seventh rank. In Raumschach, rooks move along three independent axes — level-columns, rank-columns, and file-columns — and the concept of an open line generalizes to all three. The “7th rank” analogue is the enemy’s home level.

For each Rook, the following bonuses apply:

ConditionBonus
Rook on open level-line (no pawns sharing the level-column)+22 cp
Rook on open rank-line (no pawns sharing the rank-column)+18 cp
Rook on open file-line (no pawns sharing the file-column)+18 cp
Rook on enemy home level (E for White, A for Black)+45 cp
Two rooks sharing any axis-aligned line (doubled rooks)+22 cp

The level-line bonus (+22 cp) is deliberately the largest of the three open-line bonuses, reflecting the judgment that the vertical axis is the most underexploited line type in Raumschach: it is the only rook line that passes through all five levels simultaneously, giving the rook influence over the entire depth of the board from a single position.

§6.7 — Unicorn Parity

This heuristic has no two-dimensional analogue and is the most structurally original element of Raumfischer’s evaluation.

The Unicorn moves exclusively through cell corners: each step changes all three coordinates by ±1. It follows that the parity of (l + r + f) is invariant across all Unicorn moves. A Unicorn beginning on a cell where (l+r+f) is even can never reach a cell where (l+r+f) is odd, and vice versa. This confines each Unicorn permanently to exactly 30 of the 125 cells — half the board. It is the 3D analogue of bishop-color confinement in orthodox chess, but more severe: a bishop is confined to half the cells of a given color, whereas the Unicorn is confined to a global parity class of the three-dimensional coordinate sum.

Two sub-heuristics follow from this geometry:

(a) King-parity matching: Fischer would immediately identify which parity class the enemy king occupies and bring a Unicorn of the matching parity to bear. Each own Unicorn whose parity class matches the enemy king’s parity class receives a bonus of +35 cp.

(b) Bad Unicorn pair: If both of a player’s Unicorns share the same parity class, they jointly cover only half the board — the direct analogue of the “bad bishop pair” in which both bishops are confined to the same color. This structural weakness is penalized at −45 cp (applied once per color, not per Unicorn).

uParity(l, r, f) ← (l + r + f) mod 2

The complete evaluation function in pseudocode:

FUNCTION evalBoard(board)  score (centipawns, White’s perspective):
  score  0
  pieces  []; wKing  null; bKing  null
  FOR EACH occupied cell (l, r, f):
    pieces.push({p, l, r, f})
    IF p.type = King: record position in wKing or bKing
  IF wKing or bKing is null: RETURN 0  // terminal / illegal

  // §6.1 Material + positional bonuses
  FOR EACH {p, l, r, f} IN pieces:
    cen  2 − cheby(l, r, f)       // Chebyshev centrality, ∈ {0, 1, 2}
    v    MAT[p.type] + cen × w[p.type] + levelBonus(l, p.type)
    IF p.type = Unicorn AND isInner(l,r,f): v += 25
    score += sign(p.color) × v

  // §6.2 Piece activity: mobility and dormancy
  FOR EACH {p, l, r, f} IN pieces (excluding Kings):
    mob     |pieceMoves(board, l, r, f)|
    score  += sign(p.color) × mob × 4
    thresh  DORMANCY[p.type]
    IF mob < thresh: score += sign(p.color) × (mob − thresh) × 8

  // §6.3 Spatial center
  FOR EACH {p, l, r, f} IN pieces:
    IF isInner(l,r,f): score += sign(p.color) × 12
    IF l = 2:         score += sign(p.color) × 6

  // §6.4 King safety
  FOR EACH color IN {White, Black}:
    [kl, kr, kf]  king position
    rays     openRaysOnKing(kl, kr, kf, opponent)
    shelter  friendly pawns with |Δl| ≤ 1 AND |Δf| ≤ 2
    score   += sign(color) × (shelter × 10 − rays × 20)

  // §6.5 Pawn structure
  FOR EACH color IN {White, Black}:
    FOR EACH own pawn at (l, r, f):
      IF isPassed3D(pawn, oppPawns):
        adv  advancement; score += sign × (30 + adv² × 5)
      IF doubled (same file+rank, different level): score += sign × (−20)
      // Isolated pawn: no friendly pawn in same file-column or rank-column on any other level
      fileSupport  EXISTS own pawn at (l′, r′, f) with l′ ≠ l
      rankSupport  EXISTS own pawn at (l′, r, f′) with l′ ≠ l
      IF NOT fileSupport AND NOT rankSupport: score += sign × (−15)
    FOR EACH level: score += (wPawnCount − bPawnCount) × 9

  // §6.6 Rook activation
  FOR EACH Rook at (l, r, f):
    IF isOpenLine(board, l,r,f, LEVEL):  score += sign × 22
    IF isOpenLine(board, l,r,f, RANK):   score += sign × 18
    IF isOpenLine(board, l,r,f, FILE):   score += sign × 18
    IF l = enemyHomeLevel:                 score += sign × 45
  IF doubled rooks share an axis-line:     score += sign × 22

  // §6.7 Unicorn parity
  FOR EACH color IN {White, Black}:
    kPar  uParity(enemyKing)
    FOR EACH own Unicorn at (l,r,f):
      IF uParity(l,r,f) = kPar: score += sign × 35
    IF both own Unicorns share parity:     score += sign × (−45)

  RETURN score

7. Open Questions

The search infrastructure incorporated during Version 1 development — cached mobility, Principal Variation Search, late move reductions, futility pruning, check extensions and delta pruning in quiescence, 64-bit Zobrist emulation, and cross-game transposition table clearing — resolved all previously identified search weaknesses. Two substantive open questions remain.

Time-based search management was evaluated as a candidate improvement and set aside as inapplicable to the deployment context. Player vs. AI games carry no clock; IRF tournament rules prohibit human vs. inhuman play, making timed competitive games against the engine structurally impossible; and AI vs. AI is not implemented. Fixed-depth search is therefore appropriate as designed, and is in fact preferable in a training context because it delivers consistent quality rather than depth cuts conditioned on elapsed time.

▶ In Progress
7.1 — Heuristic Weights Are Untuned

The seven heuristic components and their weights were derived from first principles and geometric reasoning rather than from empirical tuning against a corpus of Raumschach games. The relative weighting of, for example, the unicorn-parity bonus (+35 cp) against the open-ray king-safety penalty (−20 cp per ray) has not been validated against actual game outcomes. The IRF is currently addressing this with a Bayesian optimisation procedure applied to a self-play corpus: candidate weight vectors are evaluated against quiescence-searched scores, and a Bayesian optimizer (using a Gaussian process surrogate) proposes successive weight updates to minimize prediction error. This is likely to reveal that some weights are substantially miscalibrated relative to their first-principles derivation; updated weights will be incorporated into the engine as tuning completes.

7.2 — No Opening Book

The engine plays the opening from scratch on every game, without a library of sound opening sequences. This makes its early play structurally inconsistent: the engine may correctly identify the value of center control and piece activation in abstract terms, but will not reproduce specific well-studied opening lines that experienced players might employ. A modest opening book of 10–15 moves per main variation would substantially improve the quality and consistency of early play.

8. Development Roadmap

The following items constitute the known agenda for Version 2. Items are identified by descriptive labels rather than priority numbers, which become ambiguous across roadmap revisions.

▶ In Progress
Self-Play Corpus Generation

A corpus of self-play games — Raumfischer instances at depth 3 playing both sides — is being generated as a prerequisite for empirical weight tuning. The corpus serves two roles: it provides the training signal for the weight optimizer (quiescence-searched scores as targets, static evaluation as prediction), and over time it can seed a modest opening book by recording the most frequently played and most successful early move sequences. Corpus size is the binding constraint on tuning quality; generation is ongoing.

▶ In Progress
Empirical Weight Tuning

The IRF is applying mean-field variational Bayes with Pólya-Gamma augmentation to adjust 26 evaluation parameters — five piece values (queen, rook, bishop, unicorn, and knight, with the pawn fixed at 100 cp), five centrality bonuses, two mobility terms, two space bonuses, two king-safety terms, four pawn-structure terms, four rook bonuses, and two unicorn-parity terms — so as to minimize the mean squared error of the static evaluation relative to the quiescence-searched score across the self-play corpus. Variational Bayes, whose coordinate updates are exact and closed-form under the Pólya-Gamma augmentation, converges quickly and consistently. Tuned parameters will be incorporated into the engine as optimization concludes and are expected to improve quantitative calibration substantially, particularly for the unicorn-parity and king-safety terms whose first-principles derivation is most uncertain.

Opening Book

A library of sound opening sequences, derived from the self-play corpus and from annotated human games where available, would replace the engine’s current practice of evaluating the opening from scratch on every game. A book of 10–15 moves per main variation is a realistic initial target. Book construction depends on corpus availability; it is deferred until the corpus is of sufficient size to yield statistically meaningful frequency counts.

9. Conclusion

Raumfischer Version 1 represents a principled extension of the Maackesgeist search framework in the direction of positional sophistication. The core search infrastructure — allocation-free make/unmake, incremental Zobrist hashing, iterative deepening with aspiration windows, null-move pruning, quiescence search, and killer/history move ordering — is retained without modification from Maackesgeist, having been validated there and requiring no revision. Raumfischer’s original contribution is principally evaluative: the seven-component Fischer evaluation replaces a compact three-term function with one that recognizes king safety, pawn structure, rook activation, and the geometrically unique phenomenon of unicorn parity confinement.

Seven search improvements were incorporated during Version 1 development and are documented in §5 alongside the original architecture. Cached mobility eliminates the dominant per-evaluation cost of the mobility heuristic by updating mobCache incrementally on every make and unmake rather than regenerating pseudo-legal move counts from scratch at each leaf. Principal Variation Search confines full-window searches to the first root move and to cases where a subsequent move fails high on a null-window probe; given Raumfischer’s strong move ordering, the null-window probes confirm the majority of moves quickly and the full-window re-searches that would have been required in plain alpha-beta simply do not occur. Late Move Reductions extend the effective search depth by reducing the exploration of moves late in the ordered list, with a full-depth re-search triggered only when the reduced result raises alpha. Futility pruning provides complementary savings at shallow depths, where LMR does not apply. Check extensions in quiescence prevent misevaluation of forcing sequences that proceed through checks rather than captures. Delta pruning eliminates captures in quiescence that cannot improve the score even in the best case. 64-bit Zobrist key emulation reduces hash collision probability to negligible levels. Together these improvements substantially increase search speed and accuracy at no cost to correctness, and the transposition table is additionally reset between games to prevent cross-game contamination.

The remaining development agenda is threefold: completing the self-play corpus, applying variational Bayes to calibrate the heuristic weights empirically, and constructing a modest opening book from the resulting game records.

The deeper wager behind Raumfischer is that Fischer’s playing style was not a product of two-dimensional chess geometry but of a general spatial intelligence — a preference for active pieces, dominant squares, structural discipline, and remorseless conversion of advantages — that translates intact into three dimensions. Whether the engine plays as Fischer would have played Raumschach is a question that will only be settled empirically, one game at a time.

References

Dickins, A. S. M. (1971). A Guide to Fairy Chess. New York: Dover Publications.

Maack, F. (1907). Das Schachraumspiel: Dreidimensionales Schachspiel. Hamburg.

Shannon, C. E. (1950). Programming a computer for playing chess. Philosophical Magazine, 41(314), 256–275.

Zobrist, A. L. (1970). A new hashing method with application for game playing. Technical Report 88, University of Wisconsin.

International Raumschach Federation. (2026). Maackesgeist: A Raumschach Engine (Version 2). raumschach.org/maackesgeist.html

Source code: raumschach.html (2026). International Raumschach Federation. raumschach.org/raumschach.html