N150 N300 T3K P100 P150 P300C Galaxy 30 min Validated

Recipe 1: Conway's Game of Life 🎮

Overview

Conway's Game of Life is a cellular automaton where cells evolve based on simple rules:

Why This Project:

Time: 30 minutes | Difficulty: Beginner


Example Output

Game of Life Animation

Classic "Gosper Glider Gun" pattern generating infinite gliders on TT hardware. Simple convolution rules create complex emergent behavior.

View full animation →


Deploy the Project

📦 Deploy All Cookbook Projects

This creates the project in ~/tt-scratchpad/cookbook/game_of_life/.


Project Structure

~/tt-scratchpad/cookbook/game_of_life/
├── game_of_life.py       # Core TTNN implementation
├── visualizer.py          # Matplotlib animation
├── patterns.py            # Glider, blinker, Gosper gun, etc.
├── requirements.txt
└── README.md

Implementation

Step 1: Core Game Logic (game_of_life.py)

"""
Conway's Game of Life using TTNN
Implements parallel computation across tiles for efficient execution.
"""

import ttnn
import torch
import numpy as np

class GameOfLife:
    def __init__(self, device, grid_size=(128, 128)):
        """
        Initialize Game of Life on TT hardware.

        Args:
            device: TTNN device handle
            grid_size: (height, width) - must be multiples of 32 for optimal performance
        """
        self.device = device
        self.grid_size = grid_size

        # Create neighbor counting kernel (3×3 convolution kernel)
        # Pattern:
        # [1, 1, 1]
        # [1, 0, 1]  (center is 0 because we count neighbors, not self)
        # [1, 1, 1]
        kernel = torch.tensor([
            [[1.0, 1.0, 1.0],
             [1.0, 0.0, 1.0],
             [1.0, 1.0, 1.0]]
        ], dtype=torch.float32).reshape(1, 1, 3, 3)

        self.neighbor_kernel = ttnn.from_torch(
            kernel,
            device=device,
            layout=ttnn.TILE_LAYOUT
        )

    def initialize_random(self, density=0.3):
        """
        Create random initial grid.

        Args:
            density: Probability of cell being alive (0.0-1.0)

        Returns:
            TTNN tensor on device with random configuration
        """
        random_grid = (torch.rand(self.grid_size) < density).float()
        return ttnn.from_torch(
            random_grid.unsqueeze(0).unsqueeze(0),  # Add batch and channel dims
            device=self.device,
            layout=ttnn.TILE_LAYOUT
        )

    def initialize_pattern(self, pattern_name):
        """
        Initialize with a known pattern (glider, blinker, etc.)

        Args:
            pattern_name: Name of pattern ('glider', 'blinker', 'gosper_gun')

        Returns:
            TTNN tensor with pattern centered in grid
        """
        from patterns import get_pattern

        grid = torch.zeros(self.grid_size, dtype=torch.float32)
        pattern = get_pattern(pattern_name)

        # Center the pattern
        h, w = self.grid_size
        ph, pw = pattern.shape
        start_h = (h - ph) // 2
        start_w = (w - pw) // 2

        grid[start_h:start_h+ph, start_w:start_w+pw] = torch.tensor(pattern, dtype=torch.float32)

        return ttnn.from_torch(
            grid.unsqueeze(0).unsqueeze(0),
            device=self.device,
            layout=ttnn.TILE_LAYOUT
        )

    def step(self, grid):
        """
        Compute one generation of the Game of Life.

        Uses convolution to count neighbors efficiently:
        - Each cell's 8 neighbors are summed via 2D convolution
        - Game of Life rules applied: birth on 3, survival on 2-3

        Args:
            grid: Current state (TTNN tensor)

        Returns:
            Next state (TTNN tensor)
        """
        # Count neighbors using convolution
        # This is much faster than checking each neighbor individually!
        neighbors = ttnn.conv2d(
            grid,
            self.neighbor_kernel,
            padding=(1, 1),  # Pad edges to handle boundary
            stride=(1, 1)
        )

        # Conway's Rules:
        # Birth: exactly 3 neighbors
        birth = ttnn.logical_and(
            ttnn.eq(neighbors, 3.0),
            ttnn.eq(grid, 0.0)
        )

        # Survival: 2 or 3 neighbors and currently alive
        survival_condition = ttnn.logical_or(
            ttnn.eq(neighbors, 2.0),
            ttnn.eq(neighbors, 3.0)
        )
        survival = ttnn.logical_and(survival_condition, ttnn.eq(grid, 1.0))

        # New state: birth OR survival
        next_grid = ttnn.logical_or(birth, survival)

        # Convert bool back to float
        return ttnn.to_float(next_grid)

    def simulate(self, initial_grid, num_generations=100):
        """
        Run simulation for multiple generations.

        Args:
            initial_grid: Starting configuration
            num_generations: Number of steps to simulate

        Returns:
            List of grids (as numpy arrays) for visualization
        """
        history = []
        grid = initial_grid

        for gen in range(num_generations):
            # Store current state (convert to numpy for visualization)
            grid_np = ttnn.to_torch(grid).squeeze().cpu().numpy()
            history.append(grid_np)

            # Compute next generation
            grid = self.step(grid)

            # Optional: Check for stability
            if gen > 0 and np.array_equal(history[-1], grid_np):
                print(f"Stable state reached at generation {gen}")
                break

        return history

# Example usage
if __name__ == "__main__":
    import ttnn

    # Initialize device
    device = ttnn.open_device(device_id=0)

    # Create game
    game = GameOfLife(device, grid_size=(256, 256))

    # Initialize with random configuration
    initial = game.initialize_random(density=0.3)

    # Or initialize with a pattern:
    # initial = game.initialize_pattern('glider')

    # Run simulation
    history = game.simulate(initial, num_generations=200)

    # Visualize (see visualizer.py)
    from visualizer import animate_game_of_life
    animate_game_of_life(history, interval=50)

    # Cleanup
    ttnn.close_device(device)

Step 2: Patterns Library (patterns.py)

"""
Classic Game of Life patterns
"""

import numpy as np

PATTERNS = {
    'glider': np.array([
        [0, 1, 0],
        [0, 0, 1],
        [1, 1, 1]
    ]),

    'blinker': np.array([
        [1, 1, 1]
    ]),

    'toad': np.array([
        [0, 1, 1, 1],
        [1, 1, 1, 0]
    ]),

    'beacon': np.array([
        [1, 1, 0, 0],
        [1, 1, 0, 0],
        [0, 0, 1, 1],
        [0, 0, 1, 1]
    ]),

    'pulsar': np.array([
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,1,1,1,0,0,0,1,1,1,0,0]
    ]),

    'glider_gun': np.array([
        # Gosper Glider Gun (36×9) - generates gliders indefinitely!
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,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,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,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,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    ])
}

def get_pattern(name):
    """Get pattern by name."""
    if name not in PATTERNS:
        raise ValueError(f"Unknown pattern: {name}. Available: {list(PATTERNS.keys())}")
    return PATTERNS[name]

def list_patterns():
    """List all available patterns."""
    return list(PATTERNS.keys())

Step 3: Visualization (visualizer.py)

"""
Visualization for Game of Life using matplotlib
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter

def animate_game_of_life(history, interval=100, save_path=None):
    """
    Animate Game of Life simulation.

    Args:
        history: List of numpy arrays (one per generation)
        interval: Milliseconds between frames
        save_path: Optional path to save as GIF
    """
    fig, ax = plt.subplots(figsize=(8, 8))
    ax.set_title("Conway's Game of Life on TT Hardware")
    ax.axis('off')

    # Initial frame
    im = ax.imshow(history[0], cmap='binary', interpolation='nearest')
    generation_text = ax.text(0.02, 0.98, '', transform=ax.transAxes,
                             va='top', ha='left', fontsize=12,
                             bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

    def update(frame):
        """Update function for animation."""
        im.set_data(history[frame])
        generation_text.set_text(f'Generation: {frame}')
        return [im, generation_text]

    anim = FuncAnimation(fig, update, frames=len(history),
                        interval=interval, blit=True, repeat=True)

    if save_path:
        writer = PillowWriter(fps=1000//interval)
        anim.save(save_path, writer=writer)
        print(f"Animation saved to {save_path}")

    plt.tight_layout()
    plt.show()

    return anim

def plot_generation(grid, generation_num=0, title=None):
    """
    Plot a single generation.

    Args:
        grid: 2D numpy array
        generation_num: Generation number for title
        title: Custom title (overrides generation_num)
    """
    fig, ax = plt.subplots(figsize=(8, 8))

    if title:
        ax.set_title(title)
    else:
        ax.set_title(f"Generation {generation_num}")

    ax.imshow(grid, cmap='binary', interpolation='nearest')
    ax.axis('off')
    plt.tight_layout()
    plt.show()

def compare_patterns(patterns_dict):
    """
    Display multiple patterns side-by-side.

    Args:
        patterns_dict: {name: grid} dictionary
    """
    n = len(patterns_dict)
    fig, axes = plt.subplots(1, n, figsize=(4*n, 4))

    if n == 1:
        axes = [axes]

    for ax, (name, grid) in zip(axes, patterns_dict.items()):
        ax.set_title(name)
        ax.imshow(grid, cmap='binary', interpolation='nearest')
        ax.axis('off')

    plt.tight_layout()
    plt.show()

Running the Project

Quick Start - Click to Run:

🎮 Run with Random Pattern
cd ~/tt-scratchpad/cookbook/game_of_life && export PYTHONPATH=~/tt-metal:$PYTHONPATH && python3 game_of_life.py

⬆️ Run Glider Pattern
cd ~/tt-scratchpad/cookbook/game_of_life && export PYTHONPATH=~/tt-metal:$PYTHONPATH && python3 -c "from game_of_life import GameOfLife; from visualizer import animate_game_of_life; import ttnn; device = ttnn.open_device(device_id=0); game = GameOfLife(device, grid_size=(256, 256)); initial = game.initialize_pattern(\

♾️ Run Glider Gun (Infinite)
cd ~/tt-scratchpad/cookbook/game_of_life && export PYTHONPATH=~/tt-metal:$PYTHONPATH && python3 -c "from game_of_life import GameOfLife; from visualizer import animate_game_of_life; import ttnn; device = ttnn.open_device(device_id=0); game = GameOfLife(device, grid_size=(256, 256)); initial = game.initialize_pattern(\

Manual Commands:

cd ~/tt-scratchpad/cookbook/game_of_life

# Install dependencies
pip install -r requirements.txt

# Run with random initial state
python game_of_life.py

Run with specific pattern:

python -c "
from game_of_life import GameOfLife
from visualizer import animate_game_of_life
import ttnn

device = ttnn.open_device(device_id=0)
game = GameOfLife(device, grid_size=(256, 256))

# Try different patterns:
# 'glider', 'blinker', 'toad', 'beacon', 'pulsar', 'glider_gun'
initial = game.initialize_pattern('glider_gun')

history = game.simulate(initial, num_generations=500)
animate_game_of_life(history, interval=50)

ttnn.close_device(device)
"

Extensions & Experiments

1. Performance Benchmarking

Test different grid sizes and measure performance:

import time

sizes = [128, 256, 512, 1024, 2048]
for size in sizes:
    game = GameOfLife(device, grid_size=(size, size))
    initial = game.initialize_random(0.3)

    start = time.time()
    game.simulate(initial, num_generations=100)
    elapsed = time.time() - start

    generations_per_sec = 100 / elapsed
    print(f"{size}×{size}: {generations_per_sec:.2f} gen/sec")

2. Custom Rule Sets

Implement variants like HighLife (birth on 3,6) or Day & Night:

def highlife_step(self, grid):
    """HighLife: B36/S23 (birth on 3 or 6, survival on 2 or 3)"""
    neighbors = ttnn.conv2d(grid, self.neighbor_kernel, padding=(1,1))

    birth = ttnn.logical_and(
        ttnn.logical_or(ttnn.eq(neighbors, 3.0), ttnn.eq(neighbors, 6.0)),
        ttnn.eq(grid, 0.0)
    )

    survival = ttnn.logical_and(
        ttnn.logical_or(ttnn.eq(neighbors, 2.0), ttnn.eq(neighbors, 3.0)),
        ttnn.eq(grid, 1.0)
    )

    return ttnn.to_float(ttnn.logical_or(birth, survival))

3. Multi-Color Variants

Track cell "age" or "species":

def step_with_age(self, grid):
    """Cells have age (color) that increases each generation."""
    next_grid = self.step(grid)

    # Increment age of surviving cells
    aged = ttnn.add(grid, 1.0)
    aged = ttnn.where(ttnn.eq(next_grid, 1.0), aged, 0.0)

    return aged

4. 3D Game of Life

Extend to 3D volumes (more complex rules):

# 3D neighbor kernel (3×3×3)
kernel_3d = torch.ones((1, 1, 3, 3, 3))
kernel_3d[0, 0, 1, 1, 1] = 0  # Center cell

# Use 3D convolution
neighbors = ttnn.conv3d(grid_3d, kernel_3d, padding=(1,1,1))

What You Learned

Return to Cookbook Overview