Maze Generation in PHP: Bitwise Algorithms and Grid Puzzles

Maze Generation in PHP: Bitwise Algorithms and Grid Puzzles

  1. PHP 🐘
  2. 2023-04-20 21:00
  3. 10 min read

Maze generation is one of those programming puzzles that combines elegant mathematics with visual satisfaction. The problem is deceptively simple: given a grid of cells, each with four walls, generate a connected maze where every cell is reachable and there are no loops.

Oscar Merida’s PHP Puzzles column presents this challenge in a fantasy setting — the wizard Archlin needs a maze to trap approaching treklins, and you must design the PHP data structures to represent it. The underlying concepts, however, are pure computer science: bitwise encoding, 2D array traversal, and graph theory fundamentals.

What You’ll Learn

  • How to represent maze cells using 4-bit binary encoding
  • How PHP’s bitwise operators (&, |) work with hexadecimal constants
  • How to render ASCII maze visualizations from a 2D array
  • The relationship between bit masks and wall configurations
  • How to enumerate all 16 possible cell states (0x0 through 0xF)
  • Practical applications of bitwise math in PHP game development

Representing a Maze Cell With Bits

Every cell in a square grid maze has four walls: north, south, east, and west. Each wall can be either solid (present) or open (absent). A cell with no walls has all four passages open. A cell with all four walls is a sealed box.

This binary nature — each wall is either on or off — maps naturally to bits. Using four bits, you can represent all 16 possible wall combinations:

NSEWDecimalHex
000000x0
000110x1
001020x2
001130x3
010040x4
010150x5
011060x6
011170x7
100080x8
100190x9
1010100xA
1011110xB
1100120xC
1101130xD
1110140xE
1111150xF

Cells with a single wall are powers of two (1, 2, 4, 8). Cells with multiple walls are the sum of those values. For example, hex 0xD (decimal 13) is 8 + 4 + 1, meaning north, south, and west walls are solid while the east wall is open.

Why Bitwise Representation Matters

Using bits instead of a complex object structure gives you three advantages:

  1. Memory efficiency: Each cell is a single integer, not an array of four booleans
  2. Fast operations: Bitwise AND (&) and OR (|) operate in a single CPU cycle
  3. Serialization simplicity: The entire maze grid is a 2D array of small integers

Encoding a Maze Grid in PHP

Here is a 5x5 maze encoded as a PHP 2D array using hexadecimal values:

<?php
 
$maze = [
    [0x9, 0xC, 0xA, 0xD, 0xA],
    [0x7, 0x9, 0x0, 0xA, 0x3],
    [0xB, 0x7, 0x7, 0x1, 0x2],
    [0x5, 0xC, 0xA, 0x3, 0x7],
    [0xD, 0xC, 0x4, 0x6, 0xF]
];

Each value in this array represents a single cell. Value 0x9 means the north (8) and east (1) walls are solid, while south and west are open. Value 0x0 means all four walls are open — an empty passage. Value 0xF means all four walls are closed — a sealed box.

This compact representation makes it trivial to store, transmit, or generate mazes. The entire 5x5 grid fits in 25 bytes of raw data.

Rendering the Maze With Bitwise Operations

Converting the numeric grid into a visual representation requires checking which walls are present in each cell. This is where PHP’s bitwise AND operator comes in.

Define constants for the directions you want to render:

const EAST = 0x2;
const SOUTH = 0x4;

To check if a cell has an east wall, perform a bitwise AND between the cell value and the EAST constant:

$cell = 0xA; // binary: 1010
$hasEastWall = ($cell & EAST) === EAST; // (1010 & 0010) === 0010 → true

If the result equals the constant, that bit is set and the wall exists.

The Full Renderer

The complete rendering code draws the bottom and right walls of each cell, along with the top border:

<?php
 
$maze = [
    [0x9, 0xC, 0xA, 0xD, 0xA],
    [0x7, 0x9, 0x0, 0xA, 0x3],
    [0xB, 0x7, 0x7, 0x1, 0x2],
    [0x5, 0xC, 0xA, 0x3, 0x7],
    [0xD, 0xC, 0x4, 0x6, 0xF]
];
 
const EAST = 0x2;
const SOUTH = 0x4;
 
$repeat = count($maze[1]);
echo '┰' . str_repeat('───┰', $repeat) . PHP_EOL;
 
foreach ($maze as $row) {
    $east = '┃';
    $south = '╋';
 
    foreach ($row as $cell) {
        $east .= (($cell & EAST) === EAST ? ' ┃' : '  ');
        $south .= (($cell & SOUTH) === SOUTH ? '───' : '   ')
               . (($cell & EAST) === EAST ? '╋' : '┰');
    }
 
    echo $east . PHP_EOL . $south . PHP_EOL;
}

The renderer works in two passes per row:

  1. East walls pass: For each cell, checks if the east wall exists. If it does, draws a vertical bar on the right edge. If not, leaves the space open, creating a passage to the neighboring cell.

  2. South walls pass: For each cell, checks if the south wall exists. If it does, draws a horizontal line of three dashes. If not, leaves spaces, creating a passage to the cell below. The corner character (╋ or ┰) depends on whether the current cell has an east wall.

Understanding Bitwise AND in This Context

Bitwise AND compares two numbers bit by bit. A bit is set in the result only if it is set in both operands:

Cell value:  0xA  =  1010  (north and east walls)
EAST:        0x2  =  0010
AND result:        0010  (= EAST, so wall exists)

Cell value:  0x5  =  0101  (south and west walls)
EAST:        0x2  =  0010
AND result:        0000  (not EAST, so wall is absent)

This is why 0x2 (east wall) and 0x4 (south wall) are power-of-two values — they correspond to individual bits in the nibble.

Maze Generation Algorithms

Encoding and rendering a manually designed maze is the first step. The real challenge is generating valid mazes algorithmically. The Wikipedia entry on maze generation algorithms describes several approaches:

Depth-First Search (Recursive Backtracker)

This is the simplest algorithm to implement. Start at a random cell, mark it visited, and randomly choose an unvisited neighbor. Remove the wall between the current cell and the chosen neighbor, then recurse into the neighbor. If no unvisited neighbors remain, backtrack.

The resulting maze has a single solution path and no loops — a perfect maze. Each cell’s wall state is updated as walls are removed during traversal.

Prim’s Algorithm

Prim’s algorithm grows the maze from a single starting cell by maintaining a frontier of walls adjacent to visited cells. Randomly pick a wall from the frontier, add the neighboring cell to the maze, and remove the wall. This produces mazes with a different character — more dead ends, less obvious solution path.

Eller’s Algorithm

Eller’s algorithm generates mazes one row at a time, making it memory-efficient for very large grids. It processes each row by randomly connecting adjacent cells and then adding vertical connections to the next row.

Extending the Implementation

The basic representation can be extended in several directions:

Adding Cell Classes

While the bitwise representation is efficient, encapsulating the logic in a class can improve readability:

class MazeCell
{
    public function __construct(
        private readonly int $walls,
    ) {}
 
    public function hasNorthWall(): bool
    {
        return ($this->walls & 0x8) === 0x8;
    }
 
    public function hasSouthWall(): bool
    {
        return ($this->walls & 0x4) === 0x4;
    }
 
    public function hasEastWall(): bool
    {
        return ($this->walls & 0x2) === 0x2;
    }
 
    public function hasWestWall(): bool
    {
        return ($this->walls & 0x1) === 0x1;
    }
 
    public function removeWall(int $wallMask): MazeCell
    {
        return new MazeCell($this->walls & ~$wallMask);
    }
}

Solver Integration

A maze without a solver is only half the story. Breadth-first search (BFS) finds the shortest path through the maze by tracking which cells are reachable through open walls:

function solveMaze(array $grid, int $startRow, int $startCol, int $endRow, int $endCol): array
{
    $rows = count($grid);
    $cols = count($grid[0]);
    $visited = array_fill(0, $rows, array_fill(0, $cols, false));
    $queue = [[$startRow, $startCol]];
    $parent = [];
 
    while (!empty($queue)) {
        [$r, $c] = array_shift($queue);
 
        if ($r === $endRow && $c === $endCol) {
            // Reconstruct path
            return [];
        }
 
        // Check each direction using bitwise operations
        // ...
    }
 
    return [];
}

Real-World Use Cases

Game Development

Maze generation is directly applicable to dungeon and level generation in role-playing games. The bitwise representation allows dungeons to be stored as compact data structures that can be transmitted over the network efficiently.

Puzzle Applications

Online puzzle platforms can generate unlimited mazes from a single algorithm. The compact integer representation makes it trivial to seed a random number generator and share maze seeds instead of full grid data.

Educational Tools

Bitwise operations are one of those concepts that every developer encounters but few master. Maze generation provides a visual, engaging context for learning how bits work.

Robotics Path Planning

The same grid-based representation used in maze generation is fundamental to robot path planning algorithms like A* and Dijkstra’s algorithm.

Best Practices

  • Use hexadecimal constants for wall masks: 0x1, 0x2, 0x4, 0x8 is clearer than raw decimal values
  • Encapsulate bitwise logic in named methods: $cell->hasEastWall() is more readable than ($cell & 0x2) === 0x2
  • Validate grid dimensions: Ensure the grid is rectangular before processing
  • Separate generation from rendering: The maze data structure should not depend on the output format
  • Use type declarations: array and int type hints catch encoding errors early

Common Mistakes to Avoid

  • Confusing bit positions: Always document which bit represents which direction. A comment or constant is worth the effort.
  • Off-by-one errors in grid traversal: Remember that $grid[$row][$col] uses row-major order. The first index is the row (vertical), the second is the column (horizontal).
  • Neglecting the outer border: The maze must have solid outer walls except for designated entrance and exit points.
  • Using object overhead for each cell: For large grids, an array of integers is dramatically more memory-efficient than an array of objects.
  • Hardcoding hex values without comments: 0xA means nothing to a future reader. Use constants or at minimum, a table mapping values to wall configurations.

Frequently Asked Questions

Why use hexadecimal for the cell values? Hexadecimal maps directly to 4-bit nibbles. Each hex digit (0-F) corresponds to exactly one wall configuration. This makes the data self-documenting for anyone who understands the bit mapping.

How do I generate a maze algorithmically? Start with a grid where every cell has all four walls (0xF). Use a maze generation algorithm like recursive backtracker or Prim’s to selectively remove walls between cells, creating passages.

Can this representation handle triangular or hexagonal grids? The 4-bit encoding is specific to square grids with four cardinal directions. Other grid types require different bit assignments and more complex rendering.

What is the maximum grid size this approach supports? Memory is the only limit. A 1000x1000 grid requires 1 million integers. In PHP, this is approximately 80 MB of memory — impractical but possible. For large grids, generate rows on demand using Eller’s algorithm.

Does PHP’s bitwise AND work differently on 32-bit vs. 64-bit systems? No. Bitwise operations on integers up to 0xF operate identically on both architectures.

How do I create a maze with a guaranteed solution? Algorithms like recursive backtracker always produce a perfect maze with exactly one path between any two cells. This guarantees a solution exists.

Can I use this in a Laravel application? Absolutely. Maze generation fits naturally into a service class. You could build a maze generator as a Laravel service, render it to a view using Blade, and store seeds in the database for sharing.

What about non-square mazes? The same approach works for any rectangular grid. Change the array dimensions and the renderer adapts. The bitwise encoding is independent of the aspect ratio.

Conclusion

Maze generation in PHP is an excellent exercise in combining fundamental computer science concepts — bitwise operations, graph theory, and array traversal — into a working, visual result. Oscar Merida’s puzzle demonstrates that complex-looking problems often have elegant, compact solutions when you choose the right representation.

The bitwise encoding approach (four bits per cell, one integer per cell, one 2D array per maze) is efficient, extensible, and educational. You can store a maze in a database as a simple text representation of the 2D array, transmit it as JSON, or render it to any output format.

Whether you are building a game, creating educational content, or simply sharpening your algorithmic skills, maze generation in PHP is a rewarding project. Start with a 5x5 grid, implement the recursive backtracker algorithm, and watch as your screen fills with passages and walls — all controlled by bits.

Now grab the PHP-o-nomicon and start encoding. Archlin’s treklins won’t trap themselves.

PHP Maze Generation Algorithms Bitwise Operations PHP Puzzles Data Structures Grid Algorithms