After getting a basic game of life running in Rust + WASM, I wanted to expand it a bit further to allow seeding the initial state. Initially, I planned to allow an array of flags to seed any value but decided instead to use strings instead. It proved to be a bit more difficult but also more interesting.

Adding seed arg

I started by adding seed to my existing UniverseOptions struct as Option<String>. Omitting the seed will cause the app to fall to back to a random seed as before.

impl UniverseOptions {
    pub fn new(width: Option<u32>, height: Option<u32>, seed: Option<String>) -> UniverseOptions {
        UniverseOptions {
            width,
            height: match height {
                None => width,
                Some(h) => Some(h)
            },
            seed
        }
    }
}

In Universe, I extract the value and default it to an empty string.

impl Universe {
    pub fn new(opts: Option<UniverseOptions>) -> Universe {
        let UniverseOptions {width: width_opt, height: height_opt, seed: seed_opt} = opts.unwrap_or(UniverseOptions {
            width: None,
            height: None,
            seed: None
        });
        let width = width_opt.unwrap_or(64);
        let height = height_opt.unwrap_or(64);
        let seed = seed_opt.unwrap_or(String::from(""));
        ...
    }
    ...
}

I find a struct to be good way to handle optional args from JS but I’d like a better bridge than using UnverseOptions.new() that would allow me to pass a generic object and it be cast into correct struct. I’m guessing such a thing exists already but I just haven’t discovered it yet.

Refactoring Out Population

The code in Universe::new was quickly getting out of hand so my first step was to refactor the population logic into a new fill_cells method. Initially, it was pretty redundant. I didn’t save the code but something like:

fn fill_cells(width: u32, height: u32, seed: &str) -> Vec<Cell> {
    if seed.len() == 0 {
        // generate random cells and return
    } else {
        // generate cells from text and return
        // this code never worked at this point :/
    }
}

By the end, I eventually moved to the following which only generated the cells once for either branch and delegated populating specific cells to one of two methods.

fn fill_cells(width: u32, height: u32, seed: &str) -> Vec<Cell> {
    let mut cells: Vec<Cell> = (0..width * height).map(|_| Cell::Dead).collect();

    if seed.len() == 0 {
        fill_random(&mut cells);
    } else {
        fill_seed(&mut cells, height, width, seed);
    }

    cells
}

Defining a Font

Since I’m accepting strings, I need a way to convert a given character into a bitmapped glyph. It took me some searching but eventually landed on a constant u8 array with each character being represented by 20 elements of the array. 1 represents an alive cell and 0 a dead cell. This approach allowed me to “see” the character in code so I could predict how it would render on the web. It was a bit laborious to create them all but likely took less time than searching for either an algorithm to create them or an example to steal.

As a future exercise, I’ve considered converting these to bit flags which could be stored more efficiently than an array of u8.

const FONT: [u8; 20 * 26] = [
    // A
    0, 1, 1, 0,
    1, 0, 0, 1,
    1, 1, 1, 1,
    1, 0, 0, 1,
    1, 0, 0, 1,
    // B
    1, 1, 1, 0,
    1, 0, 0, 1,
    1, 1, 1, 0,
    1, 0, 0, 1,
    1, 1, 1, 0,
    // ... etc ...
]

This matches the design of the cells array and requires similar math to convert from the local coordinate system of the font to that of the cells array. Below is an early working version.

I’m tracking the position of the next character with col_offset. Each character is 4 bits wide by 5 bits tall and I’m adding 1 bit of letter spacing so each character printed will increase the col_offset by 5. I iterate over the characters, convert to the index in my font (65 is the character code for A), and map the glyph bits onto the corresponding cells.

let mut col_offset = 0;
for c in seed.chars() {
    let index = c as i32 - 65;

    // guard against overflowing
    if col_offset >= width - 4 {
        break;
    }

    if index >= 0 && index < FONT.len() as i32 {
        for i in 0..20 {
            let cell_index = ((i - i % 4) / 4) * width + i % 4 + col_offset;
            cells[cell_index as usize] = match FONT[(index * 20 + i as i32) as usize] {
                1 => Cell::Alive,
                _ => Cell::Dead
            };
        }

        col_offset += 5;
    }
}

Unit Test!

Unit testing is so important in nearly all projects to deliver a quality product. In projects with multiple layers, particularly when there is translation between those layers, unit testing is critical. I would frequently hit “unreachable executed” errors when something failed in WASM-land and debugging was difficult. Validating that the code worked correctly in Rust helps to limit interop debugging to interop concerns.

TL; DR: Write unit tests!

Adding Centering

I initially avoided text positioning by calculating the “canvas” size in JS based on the text size. So for example, the text “DAD” would be generated inside a 15x5 canvas. This produced some interesting results on its own since the text ran the the bounds and the game of life algorithm wraps around the bounds.

game of life with DAD as seed string

Always up for a challenge, I decided to add support to center the text vertically and horizontally in the canvas which turned out to be pretty straightforward given my current implementation. The existing col_offset variable could be repurposed to start at a non-zero to adjust from where the text began. Vertical centering required a new variable, row_offset, which was calculated as a function of the height of the canvas.

let row_offset = ((height - 5) / 2) as usize;

Rounding Down

In this non-subpixel, anti-aliasing free world, the calculations need to land on whole numbers. As a regular JS engineer, my first instinct for this calculation was to round the value down.

var rowOffset = Math.floor((height - 5) / 2);

This would be possible in Rust too (I’m assuming: I didn’t explicitly try) but casting (or whatever Rust calls it) the floating point value into usize did the trick here.

Type compatibility

I’m still coming to grips with working in a strongly typed language again. I’ve worked in C, C++, and Java in my past but am rusty (pun intended). I’ve discovered as to coerce types around but find I’m still pretty inefficient. I also expect types (e.g. u32 and usize to be usable together) though they can’t. I understand why but have been “spoiled” by JS for too long I guess.

Extending character support

The last feature I wanted to add for this iteration was some extended character support. I’m not interested in creating a complete font but wanted to at least support spaces and the ‘+’ character (so I could output “Welcome to Rust + WASM”).

I could have added space as a glyph in my font but that seemed too large visually so I decided to implement it uniquely as a 2 column width space. This implementation choice affected two parts: width calculation for centering and text rendering.

In retrospect, I might have chosen to render the text into a separate buffer, measure it for centering, and then copy into the output buffer. But I didn’t here. :)

For the width calculation, I refactored out a new function, calc_seed_width, which accepts the seed and returns the width. I also added a has_char function to help filter the text based on the supported glyphs in my font.

fn has_char (c: char) -> bool {
    c >= 'A' && c <= 'Z'
}

fn calc_seed_width (seed: &str) -> usize {
    let mut width: usize = 0;

    for c in seed.chars() {
        if c == ' ' {
            width += 2;
        } else if has_char(c) {
            width += 5;
        }
    }

    width
}

To support text rendering, I did some further refactoring to the font indexing in order to support the “+” character which didn’t line up to its character code position. Using Option allowed me a more intuitive API than “less than zero” for valid indices which I adopted in an updated has_char function.

fn get_font_index (c: char) -> Option<usize> {
    if c >= 'A' && c <= 'Z' {
        Some(c as usize - 65)
    } else if c == '+' {
        Some(27)
    } else {
        None
    }
}

fn has_char (c: char) -> bool {
    match get_font_index(c) {
        None => false,
        _ => true
    }
}

fill_seed before space and “+” support:

for c in seed.chars() {
    let index = c as i32 - 65;

    if index >= 0 && index < FONT.len() as i32 {
        ...
    }
}

… And after space and “+” support:

for c in seed.chars() {
    if c == ' ' {
        col_offset += 2;
    } else if let Some(index) = get_font_index(c) {
        ...
    }
}

Updating the JavaScript side

In order to test more text combinations, I took the lazy approach of plucking the value from the query string. Since the value of document.location.search includes the leading “?”, I stripped that using .substring(1).

const str = document.location.search.substring(1) || 'Rust';

However, I quickly discovered that didn’t work right because the value was URL encoded … I should have remembered that but didn’t.

const str = decodeURI(document.location.search.substring(1)) || 'Rust';

Easter Eggs

With all of that in place, I thought it’d be fun to turn it into an easter egg in which the text would render initially but trigger the game once the mouse hovered. I won’t cover the details here but it made for a fun exercise.

game of life with Welcome to Rust+WASM as seed string

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

const pre = document.getElementById("game-of-life-canvas");
let universe;

const init = () => {
    const str = decodeURI(document.location.search.substring(1)) || 'Rust';
    universe = Universe.new(UniverseOptions.new(128, 7, str.toUpperCase()));
}

const render = () => {
    pre.textContent = universe.render();
    universe.tick();
}

let raf = null;
const renderLoop = () => {
    render();
    raf = window.setTimeout(renderLoop, 64);
};

const play = () => raf === null && renderLoop();

const stop = () => {
    if (raf !== null) {
        window.clearTimeout(raf);
        raf = null;
    }
};

pre.addEventListener('mouseover', () => play());
pre.addEventListener('mouseout', () => stop());
pre.addEventListener('mousedown', () => {
    stop();
    init();
    render();
});
pre.addEventListener('mouseup', () => play());

init();
render();

Thanks for making it this far! These posts have gotten long with all the code samples. If you have suggestions on how to improve the code here, I’d love to hear them. I’m just getting started in Rust and appreciate advice. Reach out via a Twitter, email, or GitHub Issue.