JavaScript–title: “Rust for JavaScript Engineers - All a Board"date: 2025-08-23T16:50:09-07:00tags: [‘rust’, ‘game dev’, ‘WASM’]draft: falsebsky: https://bsky.app/profile/afloat.boats/post/3lx7e6wzpj224

Connect Four | Repository

In the last post we looked at setting up the codebase to build Rust crates, and then importing them into a JavaScript project.

By the end of the post we’d gone over a simple enum based design for each position of the Connect 4 board. In this post, I’m going to expand on how we can take that simple data structure, build out the entire grid, and then render it using HTML elements.

But first, a bit of house-keeping. We’d looked at calling Rust functions from JavaScript, but not the other way around. So let’s dive into that.

Calling Home

Let’s expose a JavaScript function (Math.random), on the global object as that’s how it is made available to WASM. Everything else remains the same.

// only new line
window.mathRandom = Math.random;

// existing code

Right now if we try to call: mathRandom directly in Rust, the compiler won’t let us build.

Let’s repurpose our say_hello function with this:

#[wasm_bindgen]
pub fn say_hello() {
    let random_float = mathRandom();
    web_sys::console::log_1(&format!("Random number: {}", random_float).into());
 }

On re-building connecto.rs, it throws the following error.

error[E0425]: cannot find function `mathRandom` in this scope
  --> connectors/src/lib.rs:22:24
   |
22 |     let random_float = mathRandom();
   |                        ^^^^^^^^^^ not found in this scope

The problem here is that even though the function is attached to the window object, Rust has no way of knowing it exists. Now let’s write the equivalent of a “trust me bro” to placate the Rust compiler.

// wasm bindgen ensures that the glue code for this function exists
#[wasm_bindgen]
// 1.
extern "C" {
    // 2.
    #[wasm_bindgen(js_name = mathRandom)]
    // 3.
    fn math_random() -> f32;
}
1.

The extern block is used as an FFI (Foreign Function Interface) to allow Rust code to call foreign code. This generally means that the safety guarantees of the Rust compiler can’t save you anymore, and you better be really sure about what this function does.

2.

The second thing worth highlighting is the js_name = mathRandom. This is a way to translate names between JavaScript and Rust, so we can maintain appropriate style-guides on either end of the FFI.

3.

In our Rust code the function will be available as math_random and as the javascript function Math.random returns a floating point value between 0-1 the return type of f32 will suffice. Let’s put it to use.

#[wasm_bindgen]
pub fn say_hello() {
    web_sys::console::log_1(&format!("Random number: {}", math_random()).into());
}

Once re-compiled the app should log:

Random number: 0.7674366

The actual random number would vary.

As calling JavaScript’s standard library function is a common thing people do, the js_sys crate conveniently provides us handles to all of them. Let’s swap our hand-rolled version of Math.random and just use, the one provided by the crate.

#[wasm_bindgen]
pub fn say_hello() {
    web_sys::console::log_1(&format!("Random number: {}", js_sys::Math::random()).into());
}

That works as expected: Random number: 0.583796439883168

Printing the grid

For rendering the grid it would be ideal to only print 1 character per enum variant so all the positions align. Let’s update the Display trait implementation.

If you rememeber from the last time, the Display trait is implemented so when we try to print each of the TileType variants the actual string will be displayed.

impl fmt::Display for TileType {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let tile_repr = match self {
            TileType::Red => "R", // formerly: Red
            TileType::Black => "B", // formerly: Black
            TileType::Empty => "_", // formerly: Empty
        };
        write!(f, "{tile_repr}")
    }
}

Let’s try printing the enum variant.

let tile_1 = TileType::Red;
web_sys::console::log_1(&format!("Tile 1 belongs to: {}", tile_1).into());

This now prints: Tile 1 belongs to: R

And with that we’re ready to print the entire connect 4 board. As printing an empty board is not fun, let’s generate a randomized board using the js_sys::Math::random() function.

Game Board

//  all usages of `const`s are replaced by the value at compilation
//  ↓
const ROWS_COUNT: usize = 6;
const COLS_COUNT: usize = 7;

/// This returns a Vec<TileType> of the given length
///
fn get_random_variants(n: usize) -> Vec<TileType> {
    let mut values = Vec::with_capacity(n);
    for _ in 0..n {
        //                                                 for 3 enum variants
        //                                                 ↓
        let random_variant_idx = (js_sys::Math::random() * 3.0).floor() as u8;
        let variant = match random_variant_idx {
            0 => TileType::Empty,
            1 => TileType::Red,
            2 => TileType::Black,
            _ => panic!("Unexpected value encountered")
        };

        values.push(variant);
    }
    // anything not followed by a semi-colon is returned
    values
}

This should be fairly similar to a JavaScript function that tries to populate an array with enum values. The as keyword is used to cast one type of number into another, and we want to go from a floating point number to a whole number. In this case as we’re never going to have more than 3 variants, there’s no need to store anything larger than an unsigned 8-bit int (max value: 255).

Let’s print this sucker.

#[wasm_bindgen]
pub fn say_hello() {
    let board = get_random_variants(ROWS_COUNT * COLS_COUNT);

    // first create a string
    let mut output = String::new();
    //           without enumerate we won't have access to the index
    //           │
    //           │                      index
    //           ↓                      ↓
    board.iter().enumerate().for_each(|(i, tile)| {
        if i > 0 && i % COLS_COUNT == 0 {
            output.push('\n');
        }
        output.push_str(&format!("{} ", tile));
    });
    output.push('\n');
    web_sys::console::log_1(&output.into());
}

This should print a nice randomly populated grid or R | B | _.

Connect-4 boards cannot assume arbitrary random states due to the constraint of each tile dropping to the lowest available position. However we’ll come back to that after we look at sending this data over to the JavaScript side.

Returning the grid

Let’s expose our TileType enum to the JavaScript side by adding a #[wasm_bindgen] macro at it’s top.

So far we’ve only exposed a function and an enum to the JavaScript side, however another data type struct is more suitable for returning custom data structures. Let’s start by creating a new struct that can be later reused to implement the gameplay.

#[wasm_bindgen]
pub struct Board {
    row_count: usize,
    col_count: usize,
}

Nothing new here, the wasm_bindgen macro will create the JavaScript glue code for this struct. usize just means unsigned int corresponding to the size of memory addresses for your particular machine. For most modern machines this translates to u64.

Now let’s expose a constructor:

#[wasm_bindgen]
impl Board {
    pub fn new(row_count: usize, col_count: usize) -> Self {
        Self {
            row_count,
            col_count,
        }
    }
}

The glue code for struct results in a JavaScript class. All JavaScript classes (other than pure static ones) need to be instantiated. new constructor ends up becoming a static method on the corresponding Board class in the JavaScript package. We can instantiate it like this: const board = Board.new(6, 7).

Let’s add another method that can return the randomized board we generated earlier:

    //                     1. Uint8Array
    //                     ↓
pub fn get_board(&self) -> js_sys::Uint8Array {
    let board = get_random_variants(self.row_count * self.col_count);
    // js_sys crate includes utility functions to convert `slices` to typed arrays
    //
    //                       2. slice
    //                       ↓
    js_sys::Uint8Array::from(&board[..])
}
1.

The best way to return array like data from Rust to JavaScript is TypedArrays, and in this case it ends up being a Uint8Array. As each board position can only take one of three enum variants, 8-bits are more than sufficient.

2.

Slices are one of three array like data types available in Rust. The details outside the scope of this tutorial and the Rust book provides a much better explanation than I’d be able to here.

Now let’s get rid of our say_hello function as we won’t been needing that anymore, and print the data received on the JavaScript side.

This is the entirely of the script now:

import init, { Board, TileType } from "./connectors/connectors.js";
const ROW_COUNT = 6;
const COL_COUNT = 7;
// this duplication is necessary
// just so it's easy to render as a grid
const DISPLAY_TILE_MAP = {
  [TileType.Empty]: "_",
  [TileType.Red]: "R",
  [TileType.Black]: "B",
};

init().then(() => {
  const board = Board.new(ROW_COUNT, COL_COUNT);
  const tiles = Array.from(board.get_board()).map(
    (tileEnumValue) => DISPLAY_TILE_MAP[tileEnumValue],
  );
  let boardGrid = "";
  tiles.forEach((tile, idx) => {
    // if next tile starts a new row, add a newline
    boardGrid = (idx + 1) % COL_COUNT === 0
      ? `${boardGrid} ${tile} \n`
      : `${boardGrid} ${tile}`;
  });
  console.log(boardGrid);
});

This should print an entire 7x6 grid of randomized Tiles.

Rendering Board as HTML

This part should be familiar. As we have all the data on the JavaScript side, we can use it to render the Board as HTML instead of logging it to the console.

// ...
const fragment = document.createDocumentFragment();
// create a span per tile and display the TileType
tiles.forEach((tile, idx) => {
  const span = document.createElement("span");
  span.textContent = tile;
  fragment.appendChild(span);
});

const gameBoard = document.getElementById("game-board");
gameBoard.innerHTML = "";
gameBoard.appendChild(fragment);

// ...

We won’t go into the CSS changes, but feel free to get creative here. The full project at this stage is here.

We’re getting close to gameplay now. In the next post we’ll look into adding player turns to the Board struct, and some methods that allow players to make moves.