Connect Four | Repository

When I first wanted to learn Rust in 2017, I had no idea where to start. I had written some C starting back in 7th grade, however I wasn’t particularly good at it. The only language I was competent at was Javascript, and there weren’t a lot of resources that bridged the gap between Javascript and Rust.

My hope with this series is that it’ll allow people familiar with Javascript to incrementally adopt Rust.

There are a lot of similarities between the ecosystems surrounding these languages. As all the Rust code is compiled to WASM, it can open up a path for adoption into existing Javascript project workflows, hopefully lowering the barrier of entry.

There are also a lot of differences between these languages, some of which might be out of the scope of this series. I’ll add links to more information when encountering jargon that might not make the most sense to somebody unfamiliar with the Rust ecosystem just yet.

In this series I go over building a small browser based Connect-4 game, which uses Rust compiled to WebAssembly for the core gameplay mechanics and state management. This is very much a follow along, as that’s what I find most effective for information retention. Here is the accompanying repository, that I’ll use for reference. It can also serve as checkpoints in case the readers find their own implementations diverge.

Directory Structure

connectors/
├─ connecto.rs/                // 1. workspace
│  ├─ build.sh                 // 2. WASM build script
│  └─ connectors/
│     └─ src/lib.rs            // 3. Rust crate's (connectors) entry point
└─ www/                        // 4. client-side code

This is the general directory structure I use for building applications with Rust and Javascript. I’ve found that this works best for building the rust crates [1] into packages that can be directly consumed by the Javascript projects.

1.

Here is the definition of Cargo workspaces from the Rust Book.

“…you might find that the library crate continues to get bigger and you want to split your package further into multiple library crates. Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.”

It’s similar in nature to a yarn workspace where different npm packages can share project dependencies.

2.
#!/bin/bash

set -euo pipefail

TARGET=web
OUTDIR=../../www/connectors

# The only interesting part
wasm-pack build connectors --target $TARGET --release --out-dir $OUTDIR

It basically means that we’re going to compile the connectors crate for web target into the Javascript project’s directory so it can be imported.

3.
// use statements are similar to an `import` statement
// here we're importing the `wasm_bindgen` macro from `wasm_bindgen` crate's `prelude` module
// <crate>::<module>::<macro>
use wasm_bindgen::prelude::wasm_bindgen;

// wasm_bindgen takes care of creating the glue code we need to call the `say_hello` function from Javascript
#[wasm_bindgen]
pub fn say_hello() {
  // web_sys crate encapsulates a bunch of useful Javascript functions for ease of use
  //
  //                      reference       into
  //                      ↓               ↓
  web_sys::console::log_1(&"Hello, WASM!".into());
}

In Javascript all non-primitives types are passed by reference, but in Rust you have to specify whether something is a reference or a value. For more details on this topic, check out this section of the Rust Book. Will come back to the into call at a later point, but here’s what Rust by Example has to say.

4.

The client side code will be the most familiar to Javascript engineers, it’s a single Javascript file that starts an http server using node and serves the index.html.

Let’s look at the javascript code:

import init, { say_hello } from "./connectors/connectors.js";

// WASM modules compiled for the web require initialization
init().then(() => {
  // calling Rust function we declared earlier
  say_hello();
});

Running the code

Now that the project structure is somewhat out of the way, let’s trying running the code.

# building the engine
cd connecto.rs
chmod +x build.sh
./build.sh

# starting the node server
cd www
node server.mjs

If you open up http://localhost:8000 in your favorite browser, you should see “Hello, WASM!” in the console.

Connect Four

At this point we’ve (hopefully) established a good baseline understanding of different parts of the codebase, we can start working towards the connect four game.

Let’s look at the crate changes first.

engine.rs

This is the module which will hold all the data structures relevant to the gameplay. The most important part of connect-4 are the states of each position. We can store all three positions “empty”, “red”, and “black” as variants of an enum. And as we need to import this enum into the entrypoint lib.rs, I’m declaring it using a pub keyword. This is necessary as data in Rust is private by default.

// connecto.rs/connectors/src/engine.rs
//
// declaring the enum public allows us to access it outside of the `engine.rs` file
// variants for a public enum are also public
   pub enum TileType {
       Empty,
       Red,
       Black,
   }
lib.rs

Using engine.rs in the entrypoint lib.rs.

// connecto.rs/connectors/src/lib.rs
//
// this is similar to importing a local module in Javascript
mod engine;

// this is a bit of controversial indirection, that isn't necessary but allows all other local modules to keep their imports organized.
// Instead of each module having to keep track of it's own crate and local imports, they can just say `use crate::prelude::*` and call it a day.
mod prelude {
    ...
    pub use crate::engine::*;
}

// this makes everything imported inside the `prelude` module available to the `lib.rs`
use prelude::*;
...

Now to the significant bit. I’ve updated the say_hello function to declare a few variables with the let keyword and assigned enum variants. The let keyword is different from Javascript in that it by default doesn’t not allow re-assignment to those variables.

#[wasm_bindgen]
pub fn say_hello() {
    let tile_1 = TileType::Red;
    let tile_2 = TileType::Black;
    let tile_3 = TileType::Empty;

    web_sys::console::log_1(&format!("Tile 1 belongs to: {}", tile_1).into());
    web_sys::console::log_1(&format!("Tile 2 belongs to: {}", tile_2).into());
    web_sys::console::log_1(&format!("Tile 3 belongs to: {}", tile_3).into());
}

What happens on compiling the crate via the build.sh?

I get a few errors, including the following:

error[E0277]: `engine::TileType` doesn't implement `std::fmt::Display`
  --> connectors/src/lib.rs:19:63
   |
19 |     web_sys::console::log_1(&format!("Tile 1 belongs to: {}", tile_1).into());
   |                                                               ^^^^^^ `engine::TileType` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `engine::TileType`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)

This is the one I really care about:

the trait `std::fmt::Display` is not implemented for `engine::TileType`

Let’s go ahead and implement the trait for TileType.

impl fmt::Display for TileType {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        // Rust's match statements are something between a ternary and a switch statement in Javascript
        // But as every statement in Rust is an expression (i.e. returns a value), we can assign the values to `tile_repr`
        // And then write the value to the format "stream"
        let tile_repr = match self {
            TileType::Empty => "Empty",
            TileType::Red => "Player Red",
            TileType::Black => "Player Black",
        };

        write!(f, "{tile_repr}")
    }
}

Compiles without a hitch! And the browser console now logs:

Tile 1 belongs to: Player Red
Tile 2 belongs to: Player Black
Tile 3 belongs to: Empty

Traits are Rust’s way of defining shared behavior. They are more akin to composition via object extension than classical inheritence.

The trait fmt::Display requires that the function fn fmt be implemented, which gives our enum TileType the ability to define how each of its variants are to be formatted when using the format! macro.

Let’s try one more thing, and implement a method on our enum directly called is_empty.

impl TileType {
    //               the borrowed self here is used for a regular method
    //               ↓
    pub fn is_empty(&self) -> bool {
        // if the match expression is only used to return bool values, you can just use the shorthand macro
        matches!(self, TileType::Empty)
    }
}

Enums in Rust can also have methods. This allows us to check if a particular TileType variant is empty, like the following:

#[wasm_bindgen]
pub fn say_hello() {
    // existing code

    web_sys::console::log_1(&format!("Is Tile 3 empty: {}", tile_3.is_empty()).into());
}

After re-compiling, the console should now log four statements:

Tile 1 belongs to: Player Red
Tile 2 belongs to: Player Black
Tile 3 belongs to: Empty
is Tile 3 empty: true

Remember that this method is associated to the concrete instance of the enum, so you’d call the method on the variant instead of the enum itself.

Here’s the complete diff.

In the next post we’re going to look into calling custom Javascript functions from the Rust side.