Skip to content

Commit

Permalink
chore: add the Wasm Game of Life from the Rust Wasm tutorial
Browse files Browse the repository at this point in the history
Signed-off-by: Drew Hess <[email protected]>
  • Loading branch information
dhess committed Apr 29, 2024
1 parent f24bfda commit 50f11b5
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 14 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"gol",
"greet",
]
resolver = "2"
Expand Down
33 changes: 19 additions & 14 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,6 @@
doCheck = false;
};

fileSetForCrate = crate: lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./Cargo.toml
./Cargo.lock
crate
];
};

mkWasmNpmPackage = pkgName: crateArgs: craneLibWasm.mkCargoDerivation (crateArgs // {
doInstallCargoArtifacts = false;

Expand Down Expand Up @@ -161,10 +152,15 @@
greetCrateArgs = baseArgs: pname: baseArgs // {
inherit pname;
cargoExtraArgs = "${cargoExtraArgs} --package greet";
src = fileSetForCrate ./greet;
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./greet/Cargo.toml; }) version;
};

golCrateArgs = baseArgs: pname: baseArgs // {
inherit pname;
cargoExtraArgs = "${cargoExtraArgs} --package gol";
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./gol/Cargo.toml; }) version;
};

greet-crate = craneLib.buildPackage (greetCrateArgs individualCrateArgs "${pname}-greet");

greet-crate-wasm = craneLibWasm.buildPackage (greetCrateArgs individualCrateArgsWasm "${pname}-greet-wasm");
Expand All @@ -173,6 +169,14 @@

greet-crate-wasm-npm = mkWasmNpmPackage "greet" (greetCrateArgs individualCrateArgsWasm "${pname}-greet-wasm-npm");

gol-crate = craneLib.buildPackage (golCrateArgs individualCrateArgs "${pname}-gol");

gol-crate-wasm = craneLibWasm.buildPackage (golCrateArgs individualCrateArgsWasm "${pname}-gol-wasm");

gol-crate-wasm-check = mkWasmCheck "gol" (golCrateArgs individualCrateArgsWasm "${pname}-gol-wasm-check");

gol-crate-wasm-npm = mkWasmNpmPackage "gol" (golCrateArgs individualCrateArgsWasm "${pname}-gol-wasm-npm");

inputsFrom = [
config.treefmt.build.devShell
config.pre-commit.devShell
Expand All @@ -196,13 +200,14 @@
};
} // (pkgs.lib.optionalAttrs (system == "x86_64-linux") {
inherit greet-crate-wasm-check;
inherit gol-crate-wasm-check;
});

packages = {
default = greet-crate-wasm-npm;
inherit greet-crate;
inherit greet-crate-wasm;
inherit greet-crate-wasm-npm;
default = gol-crate-wasm-npm;
inherit greet-crate gol-crate;
inherit greet-crate-wasm gol-crate-wasm;
inherit greet-crate-wasm-npm gol-crate-wasm-npm;
};

treefmt.config = {
Expand Down
36 changes: 36 additions & 0 deletions gol/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "gol"
version = "0.1.0"
authors = [
"Nick Fitzgerald <[email protected]>",
"Hackworth Ltd <[email protected]>",
]
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/hackworthltd/nix-rust-wasm-npm"
description = "The Game of Life in Wasm, from the Rust Wasm tutorial."

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
cfg-if = "1.0.0"
wasm-bindgen = "0.2.92"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }

[dependencies.web-sys]
version = "0.3.69"
features = [
"console",
]

[dev-dependencies]
wasm-bindgen-test = "0.3.42"
226 changes: 226 additions & 0 deletions gol/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
extern crate cfg_if;
extern crate wasm_bindgen;
extern crate web_sys;

mod utils;

use std::fmt;
use wasm_bindgen::prelude::*;
use web_sys::console;

pub struct Timer<'a> {
name: &'a str,
}

impl<'a> Timer<'a> {
pub fn new(name: &'a str) -> Timer<'a> {
console::time_with_label(name);
Timer { name }
}
}

impl<'a> Drop for Timer<'a> {
fn drop(&mut self) {
console::time_end_with_label(self.name);
}
}

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

impl Cell {
fn toggle(&mut self) {
*self = match *self {
Cell::Dead => Cell::Alive,
Cell::Alive => Cell::Dead,
};
}
}

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

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

/// Get the dead and alive values of the entire universe.
pub fn get_cells(&self) -> &[Cell] {
&self.cells
}

/// Set cells to be alive in a universe by passing the row and column
/// of each cell as an array.
pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
for (row, col) in cells.iter().cloned() {
let idx = self.get_index(row, col);
self.cells[idx] = Cell::Alive;
}
}

fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;

let north = if row == 0 { self.height - 1 } else { row - 1 };

let south = if row == self.height - 1 { 0 } else { row + 1 };

let west = if column == 0 {
self.width - 1
} else {
column - 1
};

let east = if column == self.width - 1 {
0
} else {
column + 1
};

let nw = self.get_index(north, west);
count += self.cells[nw] as u8;

let n = self.get_index(north, column);
count += self.cells[n] as u8;

let ne = self.get_index(north, east);
count += self.cells[ne] as u8;

let w = self.get_index(row, west);
count += self.cells[w] as u8;

let e = self.get_index(row, east);
count += self.cells[e] as u8;

let sw = self.get_index(south, west);
count += self.cells[sw] as u8;

let s = self.get_index(south, column);
count += self.cells[s] as u8;

let se = self.get_index(south, east);
count += self.cells[se] as u8;

count
}
}

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
#[allow(clippy::new_without_default)]
impl Universe {
pub fn tick(&mut self) {
// let _timer = Timer::new("Universe::tick");

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 neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// 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;
}

pub fn new() -> Universe {
utils::set_panic_hook();

let width = 128;
let height = 128;

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 width(&self) -> u32 {
self.width
}

/// Set the width of the universe.
///
/// Resets all cells to the dead state.
pub fn set_width(&mut self, width: u32) {
self.width = width;
self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
}

pub fn height(&self) -> u32 {
self.height
}

/// Set the height of the universe.
///
/// Resets all cells to the dead state.
pub fn set_height(&mut self, height: u32) {
self.height = height;
self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
}

pub fn cells(&self) -> *const Cell {
self.cells.as_ptr()
}

pub fn toggle_cell(&mut self, row: u32, column: u32) {
let idx = self.get_index(row, column);
self.cells[idx].toggle();
}
}

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)?;
}
writeln!(f)?;
}

Ok(())
}
}
13 changes: 13 additions & 0 deletions gol/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use cfg_if::cfg_if;

cfg_if! {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function to get better error messages if we ever panic.
if #[cfg(feature = "console_error_panic_hook")] {
extern crate console_error_panic_hook;
pub use self::console_error_panic_hook::set_once as set_panic_hook;
} else {
#[inline]
pub fn set_panic_hook() {}
}
}
Loading

0 comments on commit 50f11b5

Please sign in to comment.