003 realization of rust assembly Conway life game

Posted by Ansel_Tk1 on Tue, 08 Mar 2022 02:44:23 +0100

0 Introduction

Video address: https://www.bilibili.com/video/BV1eg411g7c8
Relevant source code: https://github.com/anonymousGiga/Rust-and-Web-Assembly

In this section, we will use WebAssembly to implement a simple game.

1 rules of the game

In a two-dimensional grid, the state of each grid is "life" or "death". Each square corresponds to a cell, and each cell is adjacent to the eight squares around it. During each time lapse, the following transformations occur:

1. Any living cell with less than two living neighbors will die.

2. Any living cell with two or three living neighbors can survive to the next generation.

3. Any living cell with more than three neighbors will die.

4. Any dead cell with three living neighbors will become a living cell.

Consider the initial state:

----------------
|  |  |  |  |  |
----------------
|  |  |living|  |  |
----------------
|  |  |living|  |  |
----------------
|  |  |living|  |  |
----------------
|  |  |  |  |  |
----------------

According to the above rules, the next time point will become:

----------------
|  |  |  |  |  |
----------------
|  |  |  |  |  |
----------------
|  |living|living|living|  |
----------------
|  |  |  |  |  |
----------------
|  |  |  |  |  |
----------------

2 design

2.1 design rules

2.1.1 design of the universe

The so-called universe is the design of two-dimensional squares. Because the game of life cycle is played in the infinite universe, but we don't have infinite memory and computing power, we can design the whole universe in three ways:

1. An expanding way.

2. Creating a fixed size universe with fewer cells on the edge than in the middle is a pattern with an end.

3. Create a fixed size universe, but the end on the left is the right.

2.1.2 principles of interaction between rust and Js

1. Minimize replication of linear memory in WebAssembly. Unnecessary copies incur unnecessary overhead.

2. Minimum serialization and deserialization. Like replicas, serialization and deserialization incur overhead, and often replication.

Generally speaking, a good JavaScript and web assembly interface design usually implements a large and long-lived data structure as a Rust type residing in the linear memory of the web assembly, and passes it to JavaScript as an opaque handle.

2.1.3 design of interaction between Rust and Js in our game

We can use an array to represent it. In each element, 0 represents dead cells and 1 represents living cells. Therefore, the 4 * 4 universe is like this:

Indices: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15

        -------------------------------------------------
array  :|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
        -------------------------------------------------
Rows:   |   0       |     1     |         2 |    3     |

To find the index value of a given row and column in the universe, the formula is as follows:

index(row, column, universe) = row * width(universe) + column

3. Rust implementation

Start modifying wasm game of life / SRC / lib Add code to RS

3.1 defining cell state enumeration types

The code is as follows:

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

In the above code, we defined the state of each cell. 0 means death and 1 means life. The #[repr(u8)] above indicates that the following enumeration types occupy 8 bits of memory.

3.2 defining the universe

Let's define the universe as follows:

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

Related methods are defined below:

#[wasm_bindgen]
impl Universe {
	//Get the corresponding index
	fn get_index(&self, row: u32, column: u32) -> usize {
		(row*self.width + column) as usize
	}

	//Get the number of living neighbors
	//Adjacent are - 1, 0, 1
	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
	}

	//Calculate the status of the next tick
	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) {
					(Cell::Alive, x) if x < 2 => Cell::Dead,
					(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
					(Cell::Alive, x) if x > 3 => Cell::Dead,
					(Cell::Dead, 3) => Cell::Alive,
					(otherwise, _) => otherwise,
				};

				next[idx] = next_cell;
			}
		}
		self.cells = next;
	}
	
}

So far, we have basically finished the core logic. However, we want to use black squares to represent living cells and empty squares to represent dead cells. We also need to write the following code:

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(())
	}
}

Next, let's write the rest of the code, create the code of the universe and fill in the code:

#[wasm_bindgen]
impl Universe {
	//establish
	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,
		}
	}

	//fill
	pub fn render(&self) -> String {
		self.to_string()
	}
	
	...
}	

3.3 compilation

Compile with the following command:

wasm-pack build

4 call code writing

Modify wasm game of life / www / index The html is as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>game-of-life-canvas</title>
	<style>
  		body {
  		  position: absolute;
  		  top: 0;
  		  left: 0;
  		  width: 100%;
  		  height: 100%;
  		  display: flex;
  		  flex-direction: column;
  		  align-items: center;
  		  justify-content: center;
  		}
	</style>
  </head>
  <body>
	<pre id="game-of-life-canvas"></pre>
    <script src="./bootstrap.js"></script>
  </body>
</html>

Modify index JS code is as follows:

import { Universe } from "wasm-game-of-life";

const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
//alert("+++++++++++");

function renderLoop() {
	pre.textContent = universe.render();
	universe.tick();
	window.requestAnimationFrame(renderLoop);
}

window.requestAnimationFrame(renderLoop);

5 call

Enter the www directory and execute the command:

npm run start

Enter the following address in the browser to display the changes of cells:

127.0.0.1:8080

Topics: linked list Rust