DEV Community

Cover image for Game of Life in Rust
Dinesh Kumar Gnanasekaran
Dinesh Kumar Gnanasekaran

Posted on • Updated on

Game of Life in Rust

Introduction

Rust is an uprising programming language that is loved by the community. The main reason for the upsurge of Rust in recent times is that the language is built for performance and concurrency with safety in mind. At present, the popular use case of Rust is WebAssembly.

So, I wanted to get a gist of the language. The best way(according to me) to learn is to recreate something in the new programming language, which you have already done in a familiar programming language. Hence, I went on to program a simple version of Conway’s Game of Life.

Conway’s Game of Life

Game of Life is a cellular automaton proposed by the mathematician John Horton Conway. The universe of Game of Life is an infinitely large plane with square cells, which are either in two states alive, or dead, bound by certain rules.

  • A live cell with less than two live neighbors dies.
  • A live cell with more than three live neighbors dies.
  • A dead cell with exactly three neighbors becomes alive.
  • A live cell with two or three neighbors continues to live.

For more information on Game of Life visit this link. To visualize and play with Game of Life you can visit my web application.

Getting Rust

The first step is to install Rust, which you can do by visiting the official Rust site.

You can create your Rust project by entering the following command in your terminal.

cargo new game_of_life
Enter fullscreen mode Exit fullscreen mode

Get into the directory using the command

cd game_of_life
Enter fullscreen mode Exit fullscreen mode

Inside this main directory, you will see a main.rs file inside the src folder. This main.rs is the main file that will be built and executed.

Rusting Game of Life

In this example, we going to recreate a blinker, an interesting pattern in Game of Life.

In our example, the universe of Game of Life is a two-dimensional vector in Rust, which we will call it a grid, with 1 representing a live cell and 0 representing a dead cell.
The code is given below.

// function to compute the next generation
fn gol(grid: &Vec<Vec<i8>>) -> Vec<Vec<i8>> {

    // get the number of rows
    let n = grid.len();

    // get the number of columns
    let m = grid[0].len();

    // create an empty grid to compute the future generation
    let mut future: Vec<Vec<i8>> = vec![vec![0; n]; m];

    // iterate through each and every cell
    for i in 0..n {
        for j in 0..m {

            // the current state of the cell (alive / dead)
            let cell_state = grid[i][j];

            // variable to track the number of alive neighbors
            let mut live_neighbors = 0;

            // iterate through every neighbors including the current cell
            for x in -1i8..=1 {
                for y in -1i8..=1 {

                    // position of one of the neighbors (new_x, new_y)
                    let new_x = (i as i8) + x;
                    let new_y = (j as i8) + y;

                    // make sure the position is within the bounds of the grid
                    if new_x > 0 && new_y > 0 && new_x < n as i8 && new_y < m as i8 {
                        live_neighbors += grid[new_x as usize][new_y as usize];
                    }
                }
            }

            // substract the state of the current cell to get the number of alive neighbors
            live_neighbors -= cell_state;

            // applying the rules of game of life to get the future generation
            if cell_state == 1 && live_neighbors < 2 {
                future[i][j] = 0;
            } else if cell_state == 1 && live_neighbors > 3 {
                future[i][j] = 0;
            } else if cell_state == 0 && live_neighbors == 3 {
                future[i][j] = 1;
            } else {
                future[i][j] = cell_state;
            }
        }
    }

    // return the future generation
    future
}

// main function
fn main() {

    // set the number of rows and columns of the grid
    let (rows, cols) = (5, 5);

    // create the grid
    let mut grid: Vec<Vec<i8>> = vec![vec![0; cols]; rows];

    // set the initial state of the grid (blinker)
    grid[1][2] = 1;
    grid[2][2] = 1;
    grid[3][2] = 1;

    // print the initial state of the grid;
    println!("Initial grid:");
    grid.iter().for_each(|i| {
        println!("{:?}", i);
    });

    println!("");

    // Number of generations
    const ITR: u8 = 5;

    // compute and print the next generation
    for i in 0..ITR {
        grid = gol(&grid);

        println!("Generation {}:", i+1);
        grid.iter().for_each(|i| {
            println!("{:?}", i);
        });
        println!("");
    }
}
Enter fullscreen mode Exit fullscreen mode

Then run the command given below to build and execute the file.

cargo run
Enter fullscreen mode Exit fullscreen mode

The output of the code is given below. As you can see from the output, the oscillating structure blinker in the middle of the grid.

Initial grid:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 1:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Generation 2:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 3:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Generation 4:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 5:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Enter fullscreen mode Exit fullscreen mode

Observations

At a first glance, the Rust code looks similar to any C styled language, but there are some interesting, distinct points to note.

At line 33 shown below

live_neighbours += grid[new_x as usize][new_y as usize]
Enter fullscreen mode Exit fullscreen mode

We need to cast the type of the variables new_x and new_y to usize. Suppose if the variable used to index an element of a vector is an integer, then we might get into overflow conditions like using negative numbers. The Rust compiler gives us an error if we index a vector with any other types except unsigned types.

Suppose we want to access the variables of unsigned type with signed type in calculations, then we need to cast the variables to one common type. Like at lines 28 and 29 shown below

let new_x = (i as i8) + x;
let new_y = (j as i8) + y;
Enter fullscreen mode Exit fullscreen mode

We need to cast the type of the variables i and j to i8 because the variables new_x, new_y, x, and y are of type i8 whereas i and j are of type usize because the variables n and m are of type usize. Also, if our code leads to some negative values of the variable of type usize, the compiler will panic, stopping the execution.

If we want our variables to be mutable, we must make sure to tell that to the compiler by using the keyword mut. As shown in lines 11 and 65.

let mut future: Vec<Vec<i8>> = vec![vec![0; n]; m];
let mut grid: Vec<Vec<i8>> = vec![vec![0; cols]; rows];
Enter fullscreen mode Exit fullscreen mode

Conclusion

I had a fun time implementing Game of Life in Rust. The development time of the program took a bit of time because the Rust compiler kept on complaining, but the execution was smooth. Whereas for other languages, we write the program, and we might run into errors at runtime, then come again to debug the code.

The Rust compiler also gives us suggestions for the errors and warnings which beat compilers of other programming languages. Often I was in doubt while checking the output because the program(in Rust) gave the output that is desired in the very first trial. This shows the power of the Rust compiler; once the code compiles then there is a high chance that the program might work.

The observations we saw coincides with the safety that Rust promises. This is just the tip of the iceberg, Rust also provides a lot of other features like borrowers, concurrency, and so on. The future of Rust while looking at the current state is bright, but will it stand the test of time; only time will tell the tale.

Top comments (0)