Software Journal
Open Source Updated Aug 25, 2026 8 min read

Building an Infinite 3D Maze: A Deep Dive into the endless3Dgame Raycasting Engine

A comprehensive, architectural breakdown of the endless3Dgame GitHub repository. Discover how Python, Pygame, and Numba JIT compilation unite to create a highly performant, procedurally generated 3D raycasting engine that achieves infinite exploration with zero memory growth.

Portrait of Benmalek Zohir

Benmalek Zohir

Contributor

Share
Building an Infinite 3D Maze: A Deep Dive into the endless3Dgame Raycasting Engine

Building an Infinite 3D Maze: A Deep Dive into the endless3Dgame Raycasting Engine

Have you ever wondered how classic games like Wolfenstein 3D rendered 3D worlds on hardware with kilobytes of memory? The answer lies in raycasting. In this article, we will perform a deep, line-by-line architectural analysis of the endless3Dgame repository by azario0.

This project is a masterclass in efficient, procedural game development. It generates an infinite 3D maze in real-time using Python, Pygame, and Numba JIT compilation, all while maintaining a footprint of virtually zero megabytes of memory.


🛠️ The Tech Stack

Before diving into the code, let’s understand the tools that make this project possible:

  • Python 3.7+: The core programming language, chosen for its readability and robust ecosystem.
  • Pygame: A set of Python modules designed for writing video games, handling the window creation, input polling, and 2D drawing primitives (which we use to draw our “3D” slices).
  • NumPy: Used for efficient numerical operations and buffer management.
  • Numba: A Just-In-Time (JIT) compiler that translates a subset of Python and NumPy code into fast machine code. This is the secret sauce that makes real-time raycasting viable in pure Python.

🧠 Core Concept: Procedural Generation vs. Stored Data

Traditional games load level data from files into memory. If you want an infinite world, you can’t store infinite data.

endless3Dgame solves this using deterministic procedural generation. Instead of saving a map, it uses a mathematical hash function. When the game needs to know if there is a wall at coordinate (X, Y), it passes those coordinates into a hash function. The function will always return the same result for the same coordinates. This guarantees:

  1. Zero memory growth: No matter how far you walk, the memory usage stays flat.
  2. Consistent layout: If you walk away and come back, the maze looks exactly the same.

🔍 Detailed Code Breakdown

Let’s explore the main file, app.py, and break down its methods and how they interconnect.

1. Configuration & Constants

At the top of the file, global constants define the game’s physics and visual fidelity:

SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
PLAYER_SPEED = 0.05
PLAYER_ROT_SPEED = 0.03
FOV = math.pi / 3  # 60-degree field of view
NUM_RAYS = SCREEN_WIDTH // 3  # Renders 1 ray per 3 pixels for performance
MAX_DEPTH = 25.0  # Maximum render distance
MAZE_SEED = 42  # The deterministic seed for the hash function

Note: NUM_RAYS is set to SCREEN_WIDTH // 3 as a clever optimization. Instead of casting 1200 rays per frame, it casts 400 and stretches the resulting wall slice to 3 pixels wide, drastically reducing CPU load with minimal visual degradation.

2. The Procedural Maze Algorithm

The heart of the infinite world lies in two Numba-optimized functions:

fast_hash(x, y, seed)

@numba.jit(nopython=True, cache=True)
def fast_hash(x, y, seed):
    x, y = int(x), int(y)
    h = seed + x * 374761393 + y * 668265263
    h = (h ^ (h >> 13)) * 1274126177
    h = h ^ (h >> 16)
    return h & 0x7fffffff

This is a custom, lightweight pseudo-random number generator. It uses bitwise XOR (^) and bit-shifting (>>) combined with large prime-like multipliers to scramble the coordinates into a seemingly random, yet perfectly deterministic, integer. The @numba.jit decorator compiles this to C-like machine code, making it execute in nanoseconds.

is_wall(x, y, seed)

@numba.jit(nopython=True, cache=True)
def is_wall(x, y, seed):
    x, y = int(x), int(y)
    if (x % 2 != 0) and (y % 2 != 0):
        return False  # Always open room centers
    if (x % 2 == 0) and (y % 2 == 0):
        return fast_hash(x // 2, y // 2, seed) % 100 < 80  # Pillars
    
    room_x, room_y = x // 2, y // 2
    hash_val = fast_hash(room_x, room_y, seed) % 100
    
    if x % 2 == 0:  # Vertical wall
        return hash_val < 40
    else:  # Horizontal wall
        return hash_val < 35

This function defines the rules of the maze:

  • Odd/Odd coordinates: Always open (the center of a “room”).
  • Even/Even coordinates: 80% chance of being a pillar, adding visual variety.
  • Mixed coordinates: Potential walls connecting the rooms. The hash value determines if a passage exists (60% chance of being open vertically, 65% horizontally). This ensures the maze is navigable and doesn’t trap the player in isolated boxes.

3. The Raycasting Pipeline

Raycasting simulates 3D by casting 2D lines (rays) from the player’s viewpoint and measuring how far they travel before hitting a wall.

cast_ray(start_x, start_y, angle, seed, max_depth)

@numba.jit(nopython=True, cache=True)
def cast_ray(start_x, start_y, angle, seed, max_depth):
    dx = math.cos(angle)
    dy = math.sin(angle)
    step = 0.02
    distance = 0.0
    x, y = start_x, start_y
    
    while distance < max_depth:
        x += dx * step
        y += dy * step
        distance += step
        if is_wall(x, y, seed):
            return distance, x, y
    return max_depth, x, y

This uses a fixed-step ray marching algorithm. It moves the ray forward by 0.02 units per iteration, checking is_wall() at each step. While a true DDA (Digital Differential Analyzer) algorithm is mathematically more efficient, this fixed-step approach is much simpler to implement and, thanks to Numba, runs fast enough for real-time 60 FPS gameplay.

OptimizedRenderer.render_3d()

This class handles the visual magic:

  1. Clears the screen with a ceiling and floor color.
  2. Loops through all NUM_RAYS, calculating the angle for each.
  3. Calls cast_ray to get the distance to the wall.
  4. Fisheye Correction: distance *= math.cos(player_angle - current_angle). Without this, walls would appear unnaturally curved because rays at the edge of the FOV travel a longer physical distance than the center ray.
  5. Perspective Projection: wall_height = min(self.height, self.height / (distance + 0.001)). Closer walls are drawn taller; farther walls are shorter.
  6. Dynamic Shading:
    • Distance fog: shading = max(0.1, 1.0 - (distance / MAX_DEPTH)) darkens walls further away.
    • Face orientation: By checking if the hit was closer to a vertical or horizontal grid line (dx_to_grid > dy_to_grid), it applies different base colors (COLOR_WALL_LIGHT vs COLOR_WALL_DARK). This gives the flat walls a faux-3D cornered appearance.

4. Player Controller & Collision Detection

The Game class manages the player’s state and input.

def handle_input(self):
    # ... rotation logic ...
    dx = dy = 0
    if keys[pygame.K_UP] or keys[pygame.K_w]:
        dx += math.cos(self.player_angle) * PLAYER_SPEED
        dy += math.sin(self.player_angle) * PLAYER_SPEED
        
    # Collision detection
    if not is_wall(self.player_x + dx, self.player_y, MAZE_SEED):
        self.player_x += dx
    if not is_wall(self.player_x, self.player_y + dy, MAZE_SEED):
        self.player_y += dy

Movement is calculated using basic trigonometry (cos for X, sin for Y). Crucially, collision detection is handled by simply asking the is_wall() function if the future position is a wall. If it is, the movement is canceled. This is incredibly efficient because it reuses the exact same procedural function used for rendering.

5. The Minimap System

The MiniMap class provides a top-down tactical view. It iterates over a small grid around the player’s current coordinates, calling is_wall() for each cell and drawing a small rectangle if a wall exists. Because it uses the same deterministic hash, the minimap perfectly matches the 3D view without needing a separate map data structure.


⚙️ How It All Works Together: The Game Loop

The Game.run() method ties every component together in a classic game loop:

  1. Delta Time Calculation: Ensures the game logic updates at a consistent rate, independent of frame rate fluctuations.
  2. Event Polling: Checks for pygame.QUIT or the ESC key to gracefully exit.
  3. State Update: Calls handle_input() to process movement and collisions.
  4. Rendering:
    • Calls renderer.render_3d() to draw the first-person view.
    • Calls minimap.render() to overlay the top-down map.
    • Calls render_ui() to draw the FPS counter and controls.
  5. Display Flip: pygame.display.flip() pushes the rendered buffer to the screen.
  6. Fallback Mechanism: At the bottom of the script, a try...except ImportError block ensures that if the user doesn’t have Numba installed, the script will dynamically rewrite its own code (removing the @numba.jit decorators) and run in pure Python, albeit at a lower frame rate.

🚀 How to Run It Yourself

Ready to explore the infinite maze? You can clone and run the project in just a few steps:

  1. Ensure you have Python 3.7+ installed.
  2. Clone the repository:
    git clone https://github.com/azario0/endless3Dgame.git
    cd endless3Dgame
  3. Install the required dependencies (highly recommended for performance):
    pip install pygame numpy numba
  4. Run the game:
    python app.py

🎯 Conclusion

The endless3Dgame repository is a brilliant demonstration of how algorithmic thinking can overcome hardware limitations. By combining deterministic hashing, ray marching, and Numba JIT compilation, the developer created a seamless, infinite 3D world in under 350 lines of readable Python code.

Whether you are a beginner looking to understand game loops and trigonometry, or an experienced developer interested in procedural generation and performance optimization, this codebase is a fantastic learning resource.

Happy coding, and enjoy getting lost in the infinite maze!

Share
Portrait of Benmalek Zohir

Written by

Benmalek Zohir

Founder, AI Engineer & Full Stack Developer

Benmalek Zohir is an AI Engineer, Full Stack Developer, and technology enthusiast focused on artificial intelligence, software development, and emerging technologies. He is the founder of SoftwareJournal.blog, where he shares practical insights, software discoveries, AI tools, and the latest developments in technology.

The Software Journal Dispatch

One excellent engineering read, every week.

A concise digest of our best new essays on architecture, tooling, databases, and the craft of software. No spam, no noise — unsubscribe anytime.