// Back to articles

Building Arcane Chess: A State-of-the-Art AI for a Chess Variant

How I built a real-time multiplayer chess variant with 16 spell cards and an AI engine that achieves 18-160x performance improvements through advanced optimization techniques.

Chess has been “solved” in a sense—not mathematically, but practically. Engines like Stockfish play at superhuman levels. The game’s design space has been thoroughly explored over centuries. So when I wanted to build a chess game, I asked myself: what if we added controlled chaos?

That’s how Arcane Chess was born—a multiplayer chess variant where spells, totems, and portals transform the ancient game into something strategically fresh.

The Core Concept: Chess + Spells

The rules are simple: it’s standard chess, but every few turns you select spell cards that can dramatically alter the board state. Summon a knight behind enemy lines. Teleport your queen across the board. Transform a pawn into a temporary queen. Freeze an opponent’s piece for two turns.

The challenge was making this chaotic without being random. Every spell needed to feel powerful yet balanced. Every game needed to be fair while being unpredictable.

16 Spells, Zero Randomness in Balance

I designed 16 spell cards across four rarity tiers (Common, Uncommon, Rare, Legendary). Each card has a power level, a cooldown, and—crucially—a side effect.

RarityCooldownExample Effects
Common1 turnSummon pawn, minor movement buffs
Uncommon2 turnsPiece transformations, freeze effects
Rare3 turnsTeleportation, powerful summons
Legendary4 turnsQueen summons, board-wide effects

The side effects are where the strategic depth lives. A powerful spell might cost you a piece, freeze one of your own units, or give your opponent extra time. These aren’t random—they’re calculated based on power gaps:

assignSideEffect(card: Card, targetPower: number): SideEffect {
    const powerGap = card.power - targetPower;

    if (powerGap > 4.0) return this.heavyPenalty();
    if (powerGap > 2.5) return this.significantPenalty();
    if (powerGap > 1.0) return this.moderatePenalty();
    if (powerGap > 0.3) return Math.random() < 0.6 ? this.lightPenalty() : null;
    return Math.random() < 0.3 ? this.varietyPenalty() : null;
}

This creates meaningful decisions. That legendary spell might win the game—but sacrificing your rook to cast it could lose it too.

The Selection Cadence

Card selection follows a specific rhythm:

  • Turns 1-3: Pure chess, no spells
  • Turn 4: Black selects their first cards (compensation for moving second)
  • Turn 5: White selects
  • Every 4 turns thereafter: Both players refresh their hands

Players maintain a hand of 4-5 cards maximum. Selection is deterministic per seed—given the same game seed and turn number, both players see identical card options. This ensures fairness while maintaining variety across games.

Building a Chess AI That Understands Spells

The hardest engineering challenge was the AI. Traditional chess engines don’t know what to do when a knight can suddenly appear on f7, or when the queen might transform into a pawn next turn.

I implemented a full minimax engine with alpha-beta pruning, then layered on optimizations until it achieved 18-160x performance improvements over the naive implementation.

The Optimization Journey

Phase 1: Make/Unmake Pattern (5-10x speedup)

Instead of cloning the board for every move evaluation, I implemented reversible moves:

class Board {
    makeMove(move: Move): MoveMetadata {
        const metadata = this.captureMoveState(move);
        this.applyMove(move);
        return metadata;
    }

    unmakeMove(move: Move, metadata: MoveMetadata): void {
        this.restoreState(metadata);
    }
}

This eliminated thousands of object allocations per search.

Phase 2: Move Ordering (1.2-1.5x additional)

Alpha-beta pruning works best when good moves are evaluated first. I implemented:

  • MVV-LVA (Most Valuable Victim - Least Valuable Attacker): Captures where you take a queen with a pawn are evaluated first
  • Killer Moves: Moves that caused cutoffs at the same depth in sibling nodes
  • History Heuristic: Moves that historically led to good positions

Phase 3: Late Move Reduction + Null Move Pruning (1.5-2.7x additional)

// Late Move Reduction
if (depth >= 3 && moveIndex >= 4 && !isCapture && !inCheck) {
    const reduction = Math.floor(Math.log(depth) * Math.log(moveIndex) / 2.5);
    score = -this.search(depth - 1 - reduction, -alpha - 1, -alpha);

    if (score > alpha) {
        // Re-search at full depth if reduced search looks promising
        score = -this.search(depth - 1, -beta, -alpha);
    }
}

This dramatically reduces the tree size by doing shallow searches for moves that are likely bad, only re-searching if they surprise us.

Phase 4: Transposition Tables with Zobrist Hashing

Chess positions repeat. A lot. I implemented Zobrist hashing—a technique where each piece-square combination has a random 64-bit number, and the board hash is the XOR of all active pieces:

class ZobristHash {
    private pieceSquareKeys: bigint[][]; // [piece][square]
    private sideToMoveKey: bigint;

    hash(board: Board): bigint {
        let h = 0n;
        for (const piece of board.pieces) {
            h ^= this.pieceSquareKeys[piece.type][piece.square];
        }
        if (board.sideToMove === 'black') h ^= this.sideToMoveKey;
        return h;
    }
}

The transposition table stores evaluated positions. When we reach the same position through a different move order, we can reuse the previous evaluation.

Phase 5: Parallel Search with PVS (2-4x additional)

Modern CPUs have multiple cores. I implemented a worker pool that searches different branches in parallel using Principal Variation Search:

// Search principal variation at full window
const pvScore = -this.search(depth - 1, -beta, -alpha);

// Search remaining moves with null window
const promises = remainingMoves.map(move =>
    this.workerPool.search(move, depth - 1, -alpha - 1, -alpha)
);

const results = await Promise.all(promises);
// Re-search any moves that beat alpha at full window

Card-Aware Evaluation

The evaluation function doesn’t just count material—it considers the spell dimension:

class CardContextEvaluator {
    evaluate(position: Position, hand: Card[], opponentHand: Card[]): number {
        let score = this.materialScore(position);

        // Piece safety considering opponent's spells
        score += this.spellThreatAnalysis(position, opponentHand);

        // Value of our current hand
        score += this.handStrength(hand, position);

        // Positional bonuses adjusted for spell potential
        score += this.spellAwarePositioning(position, hand);

        return score;
    }
}

A queen that can be teleported to a winning square is worth more. A king near a potential portal exit is in more danger than pure material evaluation suggests.

Performance Results

DifficultySearch DepthResponse Time
Easy310-50ms
Medium530-130ms
Hard7100-400ms
Expert9300-1500ms

The AI plays reasonable chess at lower difficulties while providing genuine challenge at Expert level—all while understanding spell interactions.

Real-Time Multiplayer Architecture

Beyond the AI, Arcane Chess supports real-time multiplayer via WebSockets:

┌─────────────┐        ┌─────────────┐        ┌─────────────┐
│   Client    │◄──────►│   Server    │◄──────►│   Client    │
│  (React)    │ WS/TCP │  (Node.js)  │ WS/TCP │  (React)    │
└─────────────┘        └─────────────┘        └─────────────┘


                       ┌─────────────┐
                       │   MongoDB   │
                       │  (Sessions) │
                       └─────────────┘

Key challenges solved:

  • State synchronization: Server is authoritative; clients optimistically render then reconcile
  • Checkmate verification: Both clients must agree a position is checkmate (prevents cheating)
  • Timeout handling: Server validates timeout reports with tolerance for network latency
  • Card selection sync: Both players must select cards within a time window

The Totem and Portal Systems

Two additional systems add tactical variety:

Totems are stationary magical objects:

  • Resurrection Totem: When a friendly piece is captured, transforms into a copy of that piece
  • Frost Totem: Freezes the attacker when a nearby friendly piece is taken
  • Movement Boost: Aura that enhances friendly piece mobility
  • Slowing Field: Restricts enemy movement in range

Portals create bidirectional teleportation links. A knight entering one portal exits the other—opening up knight forks from anywhere on the board.

Tech Stack Summary

LayerTechnology
FrontendReact 19, TypeScript, Zustand, Tailwind CSS
BackendNode.js, Express 5, Socket.io
DatabaseMongoDB with Mongoose ODM
InfrastructureDocker Compose, Nginx reverse proxy
Chess EngineCustom TypeScript (116 core files + 31 AI files)

The entire chess engine is a shared common package used by both frontend and backend, ensuring move validation is identical on both sides.

Lessons Learned

1. Game balance is an optimization problem. The card balancing system went through dozens of iterations. Analytics dashboards tracking win rates by card selection proved essential.

2. Chess engines are fascinating. Building one from scratch—even for a variant—taught me more about algorithms than any textbook. The difference between a 1-second think and a 1-minute think is entirely in the code, not the hardware.

3. Complexity compounds. Each new feature (spells, totems, portals) multiplied testing surface area. The temporary effects manager—tracking which pieces are frozen, transformed, or buffed—became the most complex single class in the codebase.

4. Multiplayer is 90% edge cases. Disconnections, reconnections, simultaneous moves, timeout disputes—the happy path was implemented in a week. The edge cases took months.


Arcane Chess is free to play at arcane-chess.com. Challenge the AI or a friend, and discover what happens when centuries-old strategy meets magical chaos.

Sometimes the best move isn’t on the board—it’s in your hand.