:#/ GSLF Journal
← :#/ GSLF Journal

Conway's Game of Life in Rust

On this page

Conway's Game of Life stands as a canonical example of a cellular automaton. The state of each cell, either **ALIVE** or **DEAD**, evolves synchronously across a grid based on four simple, local rules. Drawing inspiration from a video by Salvatore Sanfilippo, I built a simple implementation of Conway's Game of Life in Rust, featuring an infinite toroidal grid.

<figure class="video">
<iframe width="560" height="315" src="https://www.youtube.com/embed/Mao0AyWuIq8?si=iMYxoIi7AVyWMR27" title="RustLandia - COnways Game of Life" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</figure>

Toroidal Grid Geometry and Coordinate Mapping#

A key idea of this implementation is the handling of grid boundaries. Rather than imposing constraints that terminate the simulation, or adding infinite space to the grid, i chooce a toroidal topology. This configuration seamlessly connects opposing edges—the left with the right and the top with the bottom—simulating a continuous, donut-shaped universe.

fn get_unbound_coords(x: isize, y: isize) -> Coords {
    let cols = GRID_COLS as isize;
    let rows = GRID_ROWS as isize;

    let corrected_x = ((x % cols) + cols) % cols;
    let corrected_y = ((y % rows) + rows) % rows;

    Coords { x: corrected_x as usize, y: corrected_y as usize }
}

The double modulo operation, ((x % cols) + cols) % cols provides a robust and branchless solution for wrapping coordinates, correctly handling both positive and negative values.

Double Buffering#

To prevent race conditions and ensure that cell updates are based on the state of the previous generation, the simulation employs a double buffering strategy. Two grids are maintained: one representing the current state and a second for the next state.

update_grid(&grid1, &mut grid2);
std::mem::swap(&mut grid1, &mut grid2);

After each generation's new state is computed, the roles of the two grids are exchanged using std::mem::swap. This operation performs a pointer-level exchange, which is a near-zero-cost operation, effectively avoiding the need for deep, memory-intensive copies.

Pattern Instantiation#

Classic patterns such as gliders and pulsars are defined declaratively as arrays of coordinate offsets. This design enables a single, reusable helper function to instantiate any pattern.

fn draw_pattern(
    grid: &mut [[Status; GRID_COLS]; GRID_ROWS],
    x: isize, y: isize,
    offsets: &[(isize, isize)]
) {
    for (dx, dy) in offsets {
        let c = get_unbound_coords(x + *dx, y + *dy);
        set_cell(grid, Cell { coords: c, status: Status::ALIVE });
    }
}

You can find this implementation on GitHub:
gslf/RustLandia: Things in Rust