Skip to main content

Overview

The maze is procedurally generated using a Tetris-like piece placement algorithm, creating unique layouts for every game. The MyMaze class manages the entire tile grid and collision system.

Tile Types

Each tile in the 28x31 grid represents a specific game element:
ValueTypeDescription
0WallSolid blue walls with shader effects
1EmptyWalkable space without collectibles
2DotSmall collectible dots (most common)
3PillLarge power pills (make ghosts vulnerable)
4TeleportPortal tiles on left/right edges

MyMaze Class Structure

Properties

class MyMaze extends THREE.Object3D {
  constructor(cubeSize) {
    super();
    
    this.dotNumber = 0;                    // Count of collectible dots
    this.shaderMaterial = this.createShaderMaterial();
    this.mazeData = this.mazeGenerator();  // 2D array of tile types
    this.teleportPositions = [];           // Coordinates of portal tiles
    this.validPositions = [];              // All walkable positions
  }
}

Maze Data Structure

The maze is stored as a 2D array (31 rows × 28 columns):
this.mazeData = [
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
  [0,2,2,2,2,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,2,2,2,2,0],
  [0,2,0,0,0,0,2,0,0,0,0,0,2,0,0,2,0,0,0,0,0,2,0,0,0,0,2,0],
  [0,3,0,1,1,0,2,0,1,1,1,0,2,0,0,2,0,1,1,1,0,2,0,1,1,0,3,0],
  // ... more rows
];

Tile Construction

Each tile is created based on its type:
for(var i = 0; i < MyConstant.MAZE_HEIGHT; i++) {
  for(var j = 0; j < MyConstant.MAZE_WIDTH; j++) {
    var position = new THREE.Vector3(
      j * cubeSize,
      0,
      i * cubeSize
    );
    var cube;
    var validPosition;
    
    switch (this.mazeData[i][j]) {
      case 0:  // Wall
        cube = new MyTile(position, "wall", cubeSize, true, this.shaderMaterial);
        break;
        
      case 1:  // Empty space
        cube = new MyTile(position, "empty", cubeSize, false);
        validPosition = new THREE.Vector2(i, j);
        this.validPositions.push(validPosition);
        break;
        
      case 2:  // Dot
        cube = new MyTile(position, "dot", cubeSize, false);
        validPosition = new THREE.Vector2(i, j);
        this.validPositions.push(validPosition);
        this.dotNumber += 1;
        break;
        
      case 3:  // Power pill
        cube = new MyTile(position, "pill", cubeSize, false);
        validPosition = new THREE.Vector2(i, j);
        this.validPositions.push(validPosition);
        this.dotNumber += 1;
        break;
        
      case 4:  // Teleport
        cube = new MyTile(position, "teleport", cubeSize, true);
        let teleportPosition = new THREE.Vector2(i, j);
        this.teleportPositions.push(teleportPosition);
        break;
    }
    
    this.add(cube);
  }
}
Walls have hitboxes (has_hitbox: true) while walkable tiles do not, optimizing collision detection.

MyTile Class

Individual tiles are created with the MyTile class:
class MyTile extends THREE.Object3D {
  constructor(position, type, size, has_hitbox, maze_material) {
    super();
    
    this.has_hitbox = has_hitbox;
    this.size = size;
    
    // Create hitbox for walls
    if(has_hitbox) {
      this.hitbox = new THREE.Box3();
      var hitbox_pos = new THREE.Vector3(position.x, position.y, position.z);
      var hitbox_size = new THREE.Vector3(size, size, size);
      this.hitbox.setFromCenterAndSize(hitbox_pos, hitbox_size);
    }
    
    // Create visual cube based on type
    switch (type) {
      case "wall":
        if(MyConstant.ACTIVE_SHADER) {
          this.cube = new MyCube(position, maze_material, size);
        }
        break;
        
      case "dot":
        this.sphereGeom = new THREE.SphereGeometry(size/6, 20.0, 20.0);
        this.dot = new THREE.Mesh(this.sphereGeom, MyMaterial.WHITE);
        this.dot.position.set(position.x, position.y, position.z);
        this.add(this.dot);
        break;
        
      case "pill":
        this.sphereGeom = new THREE.SphereGeometry(size/3, 20.0, 20.0);
        this.dot = new THREE.Mesh(this.sphereGeom, MyMaterial.WHITE);
        this.dot.position.set(position.x, position.y, position.z);
        this.add(this.dot);
        break;
        
      case "teleport":
        this.portal = new MyPortal(position, size);
        this.add(this.portal);
        break;
    }
  }
}
Dots are 1/6 the tile size while pills are 1/3, making pills visually distinct.

Collision Detection

The maze handles collision detection for all characters:

checkCollision Method

checkCollision(hitbox, pos, dir) {
  var collision = false;
  var pos_check;
  var pos_aux;
  
  // Determine which tiles to check based on movement direction
  if(dir.x != 0) {
    pos_aux = pos.y;  // Check vertical neighbors
  }
  else {
    pos_aux = pos.x;  // Check horizontal neighbors
  }
  
  // Check tiles in movement direction
  for(var i = pos_aux - 1; i <= pos_aux + 1 && !collision; i++) {
    if(dir.x != 0) {
      pos_check = i * (MyConstant.MAZE_WIDTH) + (pos.x + dir.x);
    }
    else {
      pos_check = (pos.y + dir.y) * (MyConstant.MAZE_WIDTH) + i;
    }
    
    // Check if tile has hitbox and if it intersects
    if(this.children[pos_check].has_hitbox) {
      var box = this.children[pos_check].getCollisionBox();
      collision = box.intersectsBox(hitbox);
    }
  }
  
  return collision;
}
The collision system checks 3 tiles in the direction of movement: the direct tile plus two adjacent tiles, ensuring smooth collision detection at corners.

Tile Type Checking

getTileType(pos) {
  var pos_check = new THREE.Vector2(pos.x, pos.y);
  var type = this.mazeData[pos_check.y][pos_check.x];
  if(type == undefined) type = 0;  // Out of bounds = wall
  return type;
}

collisionType(pos, dir) {
  var pos_check = new THREE.Vector2(pos.x + dir.x, pos.y + dir.y);
  return this.mazeData[pos_check.y][pos_check.x];
}

getNeighbors Method

Used for A* pathfinding and determining valid turns:
getNeighbors(pos) {
  var neighbors = [0, 0, 0, 0];  // [right, left, down, up]
  
  // Check right
  var pos_aux = new THREE.Vector2(pos.x + 1, pos.y);
  var tileType = this.getTileType(pos_aux);
  if(tileType != 0) {
    neighbors[0] = 1;  // Can move right
  }
  
  // Check left
  pos_aux = new THREE.Vector2(pos.x - 1, pos.y);
  tileType = this.getTileType(pos_aux);
  if(tileType != 0) {
    neighbors[1] = -1;  // Can move left
  }
  
  // Check down
  pos_aux = new THREE.Vector2(pos.x, pos.y + 1);
  tileType = this.getTileType(pos_aux);
  if(tileType != 0) {
    neighbors[2] = 1;  // Can move down
  }
  
  // Check up
  pos_aux = new THREE.Vector2(pos.x, pos.y - 1);
  tileType = this.getTileType(pos_aux);
  if(tileType != 0) {
    neighbors[3] = -1;  // Can move up
  }
  
  return neighbors;
}
This method returns the valid directions as movement values (-1, 0, 1), which can be directly used for character rotation.

Dot Collection

removeDot Method

Called when Pac-Man collects a dot or pill:
removeDot(pos) {
  var pos_check = pos.y * (MyConstant.MAZE_WIDTH) + pos.x;
  
  var position = this.children[pos_check].position;
  var cubeSize = this.children[pos_check].size;
  
  // Remove the dot mesh from the tile
  this.children[pos_check].remove(this.children[pos_check].dot);
  
  // Update maze data to empty space
  this.mazeData[pos.y][pos.x] = 1;
  
  // Decrement dot counter
  this.dotNumber -= 1;
}

Tracking Progress

getDotNumber() {
  return this.dotNumber;  // Remaining dots to collect
}
The game checks if dotNumber == 0 to determine when the level is complete.

Teleport System

Portals allow characters to wrap around the edges of the maze:
getOtherTeleport(pos, dir) {
  var newPos;
  
  // Calculate position entering the teleport
  var position = new THREE.Vector2(pos.y + dir.y, pos.x + dir.x);
  
  // Find which teleport we entered
  var index = -1;
  for(let i = 0; i < this.teleportPositions.length && index == -1; i++) {
    if(this.teleportPositions[i].x == position.x &&
       this.teleportPositions[i].y == position.y) {
      index = i;
    }
  }
  
  // Teleports are stored in pairs, return the other one
  if(index % 2 == 0) {
    newPos = new THREE.Vector2(
      this.teleportPositions[index+1].x,
      this.teleportPositions[index+1].y
    );
  }
  else {
    newPos = new THREE.Vector2(
      this.teleportPositions[index-1].x,
      this.teleportPositions[index-1].y
    );
  }
  
  return newPos;
}
Teleport tiles are always placed in pairs on opposite sides of the maze, allowing seamless wrapping.

Procedural Generation

The maze uses a multi-stage generation algorithm:

1. Tetris Piece Generation

tetrisGenerator() {
  var tetris = [];  // 10x5 grid of cells
  
  // Each cell contains [up, right] values:
  // [1,1] = open on top and right
  // [1,0] = only open on top
  // [0,1] = only open on right
  // [0,0] = closed
  
  // Force center open for ghost house
  tetris[3][0] = [1,0];
  tetris[3][1] = [1,1];
  tetris[4][0] = [0,0];
  tetris[4][1] = [0,1];
  
  // Fill remaining cells with valid pieces
  // ...
  
  return tetris;
}

2. 3×3 Expansion

tetris3x3Generator() {
  var tetris = this.tetrisGenerator();
  var tetris3 = [];  // 30x15 grid
  
  // Translate each 1x1 cell to 3x3 blocks
  for(let i = 0; i < 10; i++) {
    for(let j = 0; j < 5; j++) {
      var up = tetris[i][j][0];
      var right = tetris[i][j][1];
      
      // Fill 3x3 area based on up/right values
      for(let k = 0; k < 3; k++) {
        tetris3[i*3][j*3 + k] = up;
        tetris3[i*3 + 1][j*3 + k] = 0;
        tetris3[i*3 + 2][j*3 + k] = 0;
      }
      
      tetris3[i*3][j*3 + 2] = up | right;
      tetris3[i*3 + 1][j*3 + 2] = right;
      tetris3[i*3 + 2][j*3 + 2] = right;
    }
  }
  
  return tetris3;
}

3. Full Maze Assembly

mazeGenerator() {
  var maze = [];
  var tetris3 = this.tetris3x3Generator();
  
  // Create 30x30 maze by mirroring tetris3
  for(let i = 0; i < tetris3.length; i++) {
    maze[i+1] = ([...tetris3[i]].reverse()).concat(tetris3[i]);
  }
  
  // Add wall borders
  var wall = [];
  for(let i = 0; i < 30; i++) {
    wall.push(0);
  }
  maze[0] = [...wall];
  maze[30] = [...wall];
  
  // Generate teleport portals
  for(let i = 1; i < MyConstant.MAZE_HEIGHT - 1; i++) {
    if(maze[i][MyConstant.MAZE_WIDTH-2] == 1) {
      if(maze[i-1][MyConstant.MAZE_WIDTH-2] == 0 &&
         maze[i+1][MyConstant.MAZE_WIDTH-2] == 0) {
        maze[i][MyConstant.MAZE_WIDTH-1] = 4;  // Right portal
        maze[i][0] = 4;  // Left portal
      }
    }
  }
  
  // Fill walkable spaces with dots (value 2)
  for(let i = 0; i < MyConstant.MAZE_HEIGHT; i++) {
    for(let j = 0; j < MyConstant.MAZE_WIDTH; j++) {
      // Skip center area (ghost house)
      if(j == 8 && i >= 8 && i <= 18) {
        j = 22;
      }
      
      if(maze[i][j] == 1) {
        maze[i][j] = 2;  // Convert empty to dot
      }
    }
  }
  
  // Place 4 power pills in corners
  maze[row1][1] = 3;
  maze[row1][MyConstant.MAZE_WIDTH - 2] = 3;
  maze[row2][1] = 3;
  maze[row2][MyConstant.MAZE_WIDTH - 2] = 3;
  
  return maze;
}
The maze is always symmetric, created by mirroring the left half to the right side.

Shader Effects

Walls use custom shaders for visual variety:
createShaderMaterial() {
  var color = new THREE.Color(0xBB6649);
  var hsl = new THREE.Object3D();
  color.getHSL(hsl);
  var hue = Math.random();  // Random hue per maze
  color.setHSL(hue, hsl.s, hsl.l);
  
  var amount = Math.random() * (0.5 - 0.25) + 0.25;
  let a1 = Math.random() * (150.0 - 75.0) + 150.0;
  let a2 = Math.random() * (350.0 - 200.0) + 350.0;
  
  var uniforms = {
    amount: { type: "f", value: amount },
    color: { type: "c", value: color },
    borderWidth: { type: "f", value: 4.25 },
    borderColor: { type: "c", value: new THREE.Color(0xc6c6c6) },
    blur: { type: "f", value: 0.0 },
    vecA: { type: "f", value: new THREE.Vector2(a1, a2) },
    vecB: { type: "f", value: new THREE.Vector2(b1, b2) }
  };
  
  var vertexShader = document.getElementById('vertexShader').text;
  var fragmentShader = document.getElementById('fragmentShader').text;
  
  var shaderMaterial = new THREE.ShaderMaterial({
    uniforms: uniforms,
    vertexShader: vertexShader,
    fragmentShader: fragmentShader
  });
  
  return shaderMaterial;
}
Each maze generates a unique color scheme and pattern variation, ensuring visual diversity across games.

Build docs developers (and LLMs) love