Masai Mahapa

30 days of Rust - Day Twenty Nine - Game Of Life

Day 29 of Rust After having built quite a bit within the Rust ecosystem in the past month. I read that in order to continue learning, one needs to practice with harder goals. They need to be just within reach so that you do not give up on them, but not easy that they are just an activity without growth.

Day 29 - Conway's Game Of Life

I stumbled across the game of Life while watching Lex Fridman's podcast and thought it could be an interesting project. Well, the output of this can seem like it's from an arcade game which is quite cool. Watch this demo here.

Rules of the game

The game is made up of a Universe, which is 2 dimensional (columns and rows). Each cell is either Alive or Dead. Each cell interacts with it's 8 neighbors. The four rules are;

  1. Any live cell with fewer than two live neighbors dies, as if by underpopulation.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

game of life

Setup

Since this is a web assembly program, we can use a wasm template. Run the following command;

cargo generate --git https://github.com/rustwasm/wasm-pack-template

You'll be prompted to enter the project name which we can call wasm-game-of-life.

Install dependencies

In order to check if the template has correctly been set up, we can install dependencies inside the www directory. Run the following command;

npm install

This will install webpack, which is a javascript development server and bundler.

Inside out www/package.json, we add that the package of wasm-game-of-life we are using is local by adding the following lines;

   //...
"dependencies": {                     // Add this three line block!
   "wasm-game-of-life": "file:../pkg"
 }
}

Thereafter we can change the import of wasm-game-of-life inside www/index.js.

import * as wasm from "wasm-game-of-life";
 
wasm.greet();

Run npm install again.

Run local server

We can now run the local server in the terminal with the following command;

npm run start

Implementing Game of Life

Now that our project is well set up, we can start to implement the game of life. We need to firstly define the Cell and Universe. Remove the existing code inside wasm-game-of-life/src/lib.rs and replace with the following.

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
   Dead = 0,
   Alive = 1,
}

We use #[wasm_bindgen] to give Javascript access to the Object. #[repr(u8)] simply means that it should be represented as a single byte as it will just be containing a Dead or Alive (1 or 0) state.

The Universe is made of up width * height.

#[wasm_bindgen]
pub struct Universe {
   width: u32,
   height: u32,
   cells: Vec<Cell>,
}

In order to get the cell at a certain row and column, we need to flatten out the cells into a Vector (1-dimension).

impl Universe {
   fn get_index(&self, row: u32, column: u32) -> usize {
       (row * self.width + column) as usize
   }
   // ...
}
 

We need to calculate how many neighbors of a cell are alive. This can be done by using deltas and the modulo % to avoid having if statements for edges.

impl Universe {
   // ...
 
   fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
       let mut count = 0;
       for delta_row in [self.height - 1, 0, 1].iter().cloned() {
           for delta_col in [self.width - 1, 0, 1].iter().cloned() {
               if delta_row == 0 && delta_col == 0 {
                   continue;
               }
 
               let neighbor_row = (row + delta_row) % self.height;
               let neighbor_col = (column + delta_col) % self.width;
               let idx = self.get_index(neighbor_row, neighbor_col);
               count += self.cells[idx] as u8;
           }
       }
       count
   }
}

Tick method allows us to Calculate the next state of a cell. This is easily done with a match statement taking in the cell and the number of its neighbors which are alive.

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
   pub fn tick(&mut self) {
       let mut next = self.cells.clone();
 
       for row in 0..self.height {
           for col in 0..self.width {
               let idx = self.get_index(row, col);
               let cell = self.cells[idx];
               let live_neighbors = self.live_neighbor_count(row, col);
 
               let next_cell = match (cell, live_neighbors) {
                   // Rule 1: Any live cell with fewer than two live neighbors
                   // dies, as if caused by underpopulation.
                   (Cell::Alive, x) if x < 2 => Cell::Dead,
                   // Rule 2: Any live cell with two or three live neighbors
                   // lives on to the next generation.
                   (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                   // Rule 3: Any live cell with more than three live
                   // neighbors dies, as if by overpopulation.
                   (Cell::Alive, x) if x > 3 => Cell::Dead,
                   // Rule 4: Any dead cell with exactly three live neighbors
                   // becomes a live cell, as if by reproduction.
                   (Cell::Dead, 3) => Cell::Alive,
                   // All other cells remain in the same state.
                   (otherwise, _) => otherwise,
               };
 
               next[idx] = next_cell;
           }
       }
 
       self.cells = next;
   }
 
   // ...
}

Display

use std::fmt;
 
impl fmt::Display for Universe {
   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
       for line in self.cells.as_slice().chunks(self.width as usize) {
           for &cell in line {
               let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
               write!(f, "{}", symbol)?;
           }
           write!(f, "\n")?;
       }
 
       Ok(())
   }
}

render

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
   // ...
 
   pub fn new() -> Universe {
       let width = 64;
       let height = 64;
 
       let cells = (0..width * height)
           .map(|i| {
               if i % 2 == 0 || i % 7 == 0 {
                   Cell::Alive
               } else {
                   Cell::Dead
               }
           })
           .collect();
 
       Universe {
           width,
           height,
           cells,
       }
   }
 
   pub fn render(&self) -> String {
       self.to_string()
   }
}

Render to javascript

Inside the www/index.html.

<body>
 <pre id="game-of-life-canvas"></pre>
 <script src="./bootstrap.js"></script>
</body>

add styles

<style>
 body {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
 }
</style>

Inside www/index.js

import { Universe } from "wasm-game-of-life";
 
const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
 
const renderLoop = () => {
 pre.textContent = universe.render();
 universe.tick();
 
 requestAnimationFrame(renderLoop);
};
 
requestAnimationFrame(renderLoop);

Run npm start and go to http://localhost:8080.

Conclusion

This is the hardest project I've implemented in Rust. I love how javascript can read and write to the Web Assembly linear memory.

Next up I will work on testing the program to actually see if it is following the rules as stated initially.

The github repo for this project is here.

Thanks for spending some time learning Rust with me. Until next time. Peace ✌🏼 .

Share