Conway's Game of Life in Rust

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.

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


You'll only receive email when they publish something new.

More from GSLF
All posts