Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ashokaas/BeeHex/llms.txt

Use this file to discover all available pages before exploring further.

Game State Management

The game engine is built around three core classes that manage game state at different levels of complexity.

GameInstance Class

Location: src/app/hex/[gameId]/GameInstance.ts The GameInstance class represents an active game session with full player information.
class GameInstance {
  private game_id: GameId;
  private game_parameters: GameParameters | LocalGameParameters;
  private grid_array: Array<Array<number>>;
  private first_player_id: UserId;
  private second_player_id: UserId;
  private turn: number;
  private own_id: UserId;
}

Grid Representation

The game board is stored as a 2D array where:
  • 0 = empty cell
  • 1 = red player (connects left to right)
  • 2 = blue player (connects top to bottom)
// Example 5x5 grid
[
  [0, 0, 1, 0, 0],
  [1, 0, 2, 0, 0],
  [0, 2, 1, 0, 0],
  [0, 0, 0, 2, 0],
  [0, 0, 0, 0, 1]
]

Key Methods

isValidMove(i: number, j: number): boolean {
  return this.grid_array[i][j] === 0;
}
Validates that a cell is empty before allowing a move.

AI Engine Architecture

BeeHex implements a sophisticated minimax-based AI with parallel exploration using Web Workers.

Class Hierarchy

SimpleGameInstance

Location: src/app/hex/[gameId]/Algorithm.ts:7 Lightweight representation of a game state without evaluation.
class SimpleGameInstance {
  public grid: number[][];
  public moves: Array<Coordinate>;
  public turn: number;
  protected gridHash: string;
  
  constructor(grid: Array<Array<number>>, turn: number, moves: Array<Coordinate> = []) {
    this.grid = grid;
    this.turn = turn;
    this.moves = moves;
    this.gridHash = hashGrid(grid);
  }
}

Move Generation

getValidMoves(): Array<Coordinate> {
  const validMoves: Array<Coordinate> = [];
  for (let i = 0; i < this.grid.length; i++) {
    for (let j = 0; j < this.grid[i].length; j++) {
      if (this.isValidMove(i, j)) {
        validMoves.push([i, j]);
      }
    }
  }
  return validMoves;
}

Immutable Move Application

The engine uses immutable state updates - each move creates a new game instance.
playMoveYX(y: number, x: number): SimpleGameInstance {
  if (this.isValidMove(y, x)) {
    let newGrid = this.grid.slice();
    let row = newGrid[y];
    newGrid[y] = array_cache.getFromOne(row, x, this.getCurrentPlayer());
    let newMoves = this.moves.slice();
    newMoves.push([y, x]);
    let newTurn = this.turn + 1;
    return new SimpleGameInstance(newGrid, newTurn, newMoves);
  } else {
    throw new Error("Invalid move");
  }
}

ScoredGameInstance

Location: src/app/hex/[gameId]/Algorithm.ts:95 Extends SimpleGameInstance with position evaluation and tree structure.
class ScoredGameInstance extends SimpleGameInstance {
  public score: Score;
  public previousInstances: Array<ScoredGameInstance> = [];
  public nextInstances: Map<number, ScoredGameInstance>;
  public leadingInstance: ScoredGameInstance | null = null;
  public completedBranch: boolean;
}

Score Propagation

Scores propagate up the tree using minimax logic:
updateScore() {
  if (this.nextInstances.size == 0) {
    console.warn("updateScore() called with no children")
  }
  
  let minMax;
  if (this.turn % 2 === 0) {
    // Blue player (minimize)
    minMax = (a, b) => a.getScore().isBiggerThan(b.getScore()) ? b : a
  } else {
    // Red player (maximize)
    minMax = (a, b) => a.getScore().isBiggerThan(b.getScore()) ? a : b
  }
  
  const nextInstances = this.nextInstances.values().toArray();
  const leadingInstance = nextInstances.reduce(minMax);
  let newScore = Score.fromRaw(leadingInstance.getScore().raw());
  
  if (newScore.isWinCountdown) {
    // Add 0.5 moves to countdown
    newScore.score += newScore.score > 0 ? 0.5 : -0.5;
  }
  
  this.grid = empty_array; // Clear grid to save memory
  this.completedBranch = leadingInstance.completedBranch;
  
  if (!currentScore.isEqualTo(newScore)) {
    this.score = newScore;
    this.leadingInstance = leadingInstance;
    this.updatePreviousInstances();
  }
}

Score Class

Location: src/app/hex/[gameId]/Algorithm.ts:783 Represents position evaluation with special handling for forced wins.
class Score {
  public score: number;
  public isWinCountdown: boolean;
  
  constructor(score: number, isWinCountdown: boolean) {
    this.score = score;
    this.isWinCountdown = isWinCountdown;
  }
}

Score Semantics

When isWinCountdown = false, the score represents a heuristic evaluation:
  • Positive values: Red (player 1) is winning
  • Negative values: Blue (player 2) is winning
  • Magnitude: Distance from victory (lower = better for that player)
return new Score(player2Score[0] - player1Score[0], false);

Position Evaluation Heuristic

Location: src/app/hex/[gameId]/Algorithm.ts:526 The basicHeuristic function evaluates positions using a sophisticated pathfinding algorithm.

Algorithm Overview

1

Calculate path costs

For each player, compute minimum cost to connect their edges using Dijkstra-style search.
2

Detect forced wins

If cost = 0, the player has already won. Return win countdown score.
3

Compute differential

Return player2_cost - player1_cost as heuristic evaluation.

Path Cost Calculation

function attributeScore(grid: number[][], player: number): [number, number] {
  let size = grid.length;
  let minCost = grid.length ** 2;
  let minBridges = grid.length ** 2;
  let hexagonsToCheck: Array<CheckableHexagon> = [];
  let checkedHexagons: Array<Coordinate> = [];
  
  // Initialize starting edge
  if (player === 1) { // Red: left edge
    for (let i = 0; i < grid.length; i++) {
      if (grid[i][0] === 1) {
        hexagonsToCheck.push([[i, 0], 0, 0]);
      } else if (grid[i][0] === 0) {
        // Empty cell costs 1, or 0 if bridge exists
        hexagonsToCheck.push([[i, 0], detectBridgeCost(i, 0), bridges]);
      }
    }
  }
  // ... (similar for player 2: top edge)
  
  // Dijkstra-style search
  while (hexagonsToCheck.length > 0) {
    hexagonsToCheck.sort((a, b) => b[1] - a[1]);
    let hexagon = hexagonsToCheck.pop()!;
    checkedHexagons.push(hexagon[0]);
    
    let surroundingHexagons = getSurroundingHexagons(size, hexagon[0][0], hexagon[0][1]);
    let freeHexagons = filterAntiHexagons(grid, surroundingHexagons, 3 - player);
    
    for (let surroundingHexagon of freeHexagons) {
      let cost = calculateMoveCost(hexagon, surroundingHexagon, grid, player);
      let bridges = isBridge ? hexagon[2] + 1 : hexagon[2];
      
      if (boundCheck === size - 1) { // Reached target edge
        if (cost < minCost) {
          minCost = cost;
          minBridges = bridges;
        }
      } else {
        hexagonsToCheck.push([surroundingHexagon, cost, bridges]);
      }
    }
  }
  
  return [minCost, minBridges];
}

Bridge Detection

A “bridge” is a connection pattern that allows jumping over opponent pieces without additional cost.
function getInnerBridgeHexagons(hex1: Coordinate, hex2: Coordinate): Array<Coordinate> {
  let diffY = hex2[0] - hex1[0];
  let diffX = hex2[1] - hex1[1];
  
  // Example: jump from (y,x) to (y+1,x+1) requires both (y,x+1) and (y+1,x) empty
  if (diffX === 1 && diffY === 1) {
    return [[y, x - 1], [y - 1, x]];
  }
  // ... (6 possible bridge patterns)
}

Explorer and Web Workers

Location: src/app/hex/[gameId]/Algorithm.ts:281 The Explorer class orchestrates parallel game tree search using Web Workers.

Explorer Architecture

class Explorer {
  private game: ScoredGameInstance;
  private exploredBoardCount: number = 0;
  private heuristic: Function;
  private updateCallback: Function;
  private mainInstances: Array<MainScoredGameInstance>;
  private worker: Worker;
  private instances: Map<GridHash, ScoredGameInstance>;
  private bestMoves: Array<MainScoredGameInstance>;
  
  constructor(grid: Array<Array<number>>, turn: number, heuristic: Function, updateCallback: Function) {
    let simpleGame = new SimpleGameInstance(grid, turn, []);
    this.worker = new Worker(new URL("AlgorithmWorker.ts", import.meta.url));
    this.worker.addEventListener("message", this._workerCallback.bind(this));
    
    this.game = this.rateInstance(simpleGame);
    this.instances = new Map<GridHash, ScoredGameInstance>();
    this.instances.set(this.game.getGridHash(), this.game);
    
    this.mainInstances = this.populateMainInstances();
    this.sendNextInstanceToWorker();
  }
}

Exploration Strategy

1

Initialize root position

Create ScoredGameInstance from current board state and evaluate with heuristic.
2

Populate main instances

Generate all first-level moves as MainScoredGameInstance objects (special class that reports back to UI).
3

Explore second level

For each main instance, explore opponent replies to depth 2.
4

Send to worker

Identify best incomplete branch and send to Web Worker for deep exploration (2 more levels).
5

Process worker results

Receive explored game tree, reconstruct instance graph, propagate scores upward.
6

Update UI

Return top 10 moves with their scores and optimal continuations.
7

Repeat

Continue sending next best incomplete branch until tree is fully explored.

Worker Communication

Worker Code: src/app/hex/[gameId]/AlgorithmWorker.ts
// AlgorithmWorker.ts
self.addEventListener("message", (event) => {
  let packet = event.data as AlgorithmWorkerBoundGenericPacket;
  
  if (packet.type === AlgorithmWorkerBoundPacketType.EXPLORE_INSTANCE) {
    const game = ScoredGameInstance.fromRaw(packet.game);
    const resultMap = new Map<GridHash, RawScoredGameInstance>();
    const leaves: Array<GridHash> = [];
    
    // Explore 2 levels deep
    let nextInstances = explore(game);
    for (let nextInstance of nextInstances.values()) {
      resultMap.set(nextInstance.getGridHash(), nextInstance.raw());
      
      let furtherInstances = explore(nextInstance);
      for (let furtherInstance of furtherInstances.values()) {
        resultMap.set(furtherInstance.getGridHash(), furtherInstance.raw());
        leaves.push(furtherInstance.getGridHash());
      }
    }
    
    postMessage({
      type: AlgorithmExplorerBoundPacketType.RESULT,
      id: packet.id,
      result: { gameInstances: resultMap, leaves: leaves }
    });
  }
});

Performance Monitoring

// From Algorithm.ts:451
private _workerCallback(event: MessageEvent) {
  const currentTime = Date.now();
  this.exploredBoardCount += 1;
  
  if (currentTime - this.startTime > 5000) {
    let boardsPerSecond = Math.round(
      (this.exploredBoardCount * 1000) / (currentTime - this.startTime)
    );
    console.log(`Explored ${this.exploredBoardCount} boards in ${currentTime - this.startTime} ms (${boardsPerSecond} boards/s)`);
    this.startTime = currentTime;
    this.exploredBoardCount = 0;
  }
}
On modern hardware, BeeHex can explore 10,000+ positions per second for 9x9 boards.
interface RecommendedMove {
  coordinate: Coordinate;          // [y, x] of suggested move
  score: Score;                     // Evaluation of this move
  optimalRoute: Array<Coordinate>;  // Best continuation from this position
}
Example output from Explorer:
[
  {
    "coordinate": [4, 4],
    "score": { "score": -2, "isWinCountdown": false },
    "optimalRoute": [[4, 4], [3, 5], [5, 3], [4, 6]]
  },
  {
    "coordinate": [2, 3],
    "score": { "score": -1, "isWinCountdown": false },
    "optimalRoute": [[2, 3], [3, 2], [1, 4]]
  }
]

Next Steps

WebSocket Protocol

Learn about the multiplayer communication protocol and packet specifications

Build docs developers (and LLMs) love