Python
Programming

Conway's Game of Life

A zero-player cellular automaton on a grid where cells are either live or dead

Conway's Game of Life

1. The Challenge

  • Context: The goal was to build a robust simulation engine for Conway's Game of Life, a zero-player cellular automaton. Rather than a simple hard-coded script, the requirement was a flexible "engine" capable of ingesting custom initial states from external files (e.g., source_grid.txt) to test specific patterns like Gliders and Spaceships.
  • The Obstacle: The core engineering challenge is state management. In a grid where every cell’s future depends on its neighbors' current state, you cannot modify the grid in-place during an update cycle. Doing so corrupts the data for the next cell in the loop. Additionally, parsing variable-sized grid data from text files requires robust input handling.

2. The Solution Architecture

The application follows a modular Input -> Processing -> Render pipeline:

  1. Input Parsing: The system reads raw text files (source_binary.txt or source_grid.txt), stripping whitespace and converting characters (e.g., 1/0 or X/.) into a 2D boolean array.
  2. Simulation Loop: A central main.py orchestrates the lifecycle.It holds two grids in memory:
    • current_frame: The data being rendered.
    • buffer_frame: The canvas where the next generation is calculated.
  3. Key Decisions:
    • Double Buffering: I chose to generate a fresh grid for every frame (using copy.deepcopy or list comprehensions) rather than mutating the existing one. This increases memory usage slightly but guarantees mathematical accuracy for the simulation rules.
    • Python Lists vs. NumPy: For this iteration, I stuck to native Python lists to ensure zero dependencies, making the script portable to any environment without needing pip install.

3. Implementation Highlights

A. Safe State Updates (Double Buffering)

This snippet demonstrates the logic that prevents "data pollution." We iterate through the current grid but apply changes to a strictly separate next_gen list.

def compute_next_generation(grid):
    rows = len(grid)
    cols = len(grid[0])
    # Create a new blank grid to avoid mutating the current state while reading it
    next_gen = [[0 for _ in range(cols)] for _ in range(rows)]

    for r in range(rows):
        for c in range(cols):
            alive_neighbors = count_neighbors(grid, r, c)
      
            # Apply Conway's 4 Rules
            if grid[r][c] == 1:
                # Rule: Die if underpopulated (<2) or overpopulated (>3)
                if alive_neighbors < 2 or alive_neighbors > 3:
                    next_gen[r][c] = 0
                else:
                    next_gen[r][c] = 1 # Stay alive
            else:
                # Rule: Reproduce if exactly 3 neighbors
                if alive_neighbors == 3:
                    next_gen[r][c] = 1
              
    return next_gen

B. Dynamic Grid Loading

To allow for custom patterns, the engine parses external files. This logic handles potential formatting errors (like uneven line lengths) by normalizing row widths.

def load_grid_from_file(filepath):
    grid = []
    try:
        with open(filepath, 'r') as f:
            for line in f:
                # Strip newline chars and convert '1'/'0' string to int
                row = [int(char) for char in line.strip() if char in '01']
                if row:
                    grid.append(row)
    except FileNotFoundError:
        print(f"Error: {filepath} not found. Loading default random grid.")
        return generate_random_grid()
  
    return grid

4. Challenges & Overcoming Roadblocks

  • The "Index Out of Bounds" Trap: When checking the 8 neighbors of a cell at the edge of the grid (e.g., grid[0][0]), the code would crash by trying to access index [-1].
  • The Fix: I implemented a Toroidal (Wrap-around) logic using the modulo operator %. This connects the top edge to the bottom and left to right, creating an infinite surface effect without infinite memory.
    • Logic: neighbor_row = (r + i + rows) % rows ensures that if we look "above" row 0, we check the last row.

5. Results & Impact

  • Performance: The engine successfully runs stable simulations of complex patterns (Glider Guns) at 60+ iterations per second on standard hardware.
  • Extensibility: By decoupling the grid configuration from the code, I can now test any initial state by simply editing project_details.txt or source_grid.txt.

Check the Code on GitHub