DEV Community

Cover image for 2048 WASM
Max Bantsevich for dev.family

Posted on

2048 WASM

This article is an experiment with Rust with its subsequent compilation in WASM.
It was interesting to try these technologies on something more difficult than calculating factorial, so the choice fell on the well-known 2048 game.

Alt Text

Game

The original game represents a 4 by 4 square field. Each square can be either empty or occupied by a tile with a number that is a power of 2. The starting state of the field has 2 filled cells.

Alt Text

The player can make a move in one of 4 directions: left, right, up and down, moving all tiles all the way in the selected direction. A new tile appears after each move. The new tile will contain a 2 with a 90% probability or a 4
with a probability of 10%.

For example, let's move from the starting state shown above to the right:

Alt Text

The tile on the second row rested against the right border, and a 2 appeared on the first row, the third column.

If a tile rests on another tile containing the same number, they are merged into one tile of the next power of two.

Let's move up from the previous state:

Alt Text

Twos of the last column of the first and second rows merged into one, and in addition, a four appeared on the third row.

Purpose of the game: reach the tile with the number 2048.

Preparation

Since one of the goals of this experiment for me was to play with Rust, there is no task of choosing a language.

This page provides a list of front-end frameworks for Rust.
The most popular option - Yew - looks interesting, especially if you have experience with React.

Instructions for creating a project can be found here:

cargo new --lib rust-2048 && cd rust-2048
Enter fullscreen mode Exit fullscreen mode

Next, add dependencies to Cargo.toml:

[dependencies]
yew = "0.17"
wasm-bindgen = "0.2.67"
Enter fullscreen mode Exit fullscreen mode

The example provides code for creating a simple counter. src/lib.rs:

use wasm_bindgen::prelude::*;
use yew::prelude::*;
struct Model {
    link: ComponentLink<Self>,
    value: i64,
}
enum Msg {
    AddOne,
}
impl Component for Model {
    type Message = Msg;
    type Properties = ();
    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
            value: 0,
        }
    }
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::AddOne => self.value += 1
        }
        true
    }
    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        // Should only return "true" if new properties are different to
        // previously received properties.
        // This component has no properties so we will always return "false".
        false
    }
    fn view(&self) -> Html {
        html! {
            <div>
                <button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button>
                <p>{ self.value }</p>
            </div>
        }
    }
}
#[wasm_bindgen(start)]
pub fn run_app() {
    App::<Model>::new().mount_to_body();
}
Enter fullscreen mode Exit fullscreen mode

Create a directory for static files:

mkdir static
Enter fullscreen mode Exit fullscreen mode

Inside it we create index.html with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Yew Sample App</title>
    <script type="module">
      import init from "./wasm.js";
      init();
    </script>
  </head>
  <body></body>
</html>
Enter fullscreen mode Exit fullscreen mode

Build the project using wasm-pack (installed via cargo install wasm-pack):

wasm-pack build --target web --out-name wasm --out-dir ./static
Enter fullscreen mode Exit fullscreen mode

I got the following error while building:

Error: crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your Cargo.toml file:
[lib]
crate-type = ["cdylib", "rlib"]
Enter fullscreen mode Exit fullscreen mode

Okay, so let's do it. Open Cargo.toml and add at the end:

[lib]
crate-type = ["cdylib", "rlib"]
Enter fullscreen mode Exit fullscreen mode

Now we can start any static server and check the result (in the example they suggest installing miniserve, but I will use a standard Python module):

cd static
python -m SimpleHTTPServer
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:8000 and see the working counter. Great, now we can start building the game itself.

Basics

Commit: https://github.com/dev-family/wasm-2048/commit/6bc015fbc88c1633f4605944fd920b2780e261c1

Here I described the basic logic of the game and implemented moving tiles without merging. Also, I've added helper structures and methods for convenience.

Let's consider the logic of the movement of tiles using this field as an example:

2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

Suppose a move was made to the right. The following result is expected:

0, 0, 2, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

This behavior can be achieved if you go from the end of the selected direction and move all the tiles in the selected direction.
I will iterate manually for a better understanding:

  1. Selected direction is right => go from right to left.
         /-------- empty cell, skip
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

2.

      /-------- empty cell, skip
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

3.

    /-------- start to shift to the right
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

4.

       /-------- continue
2, 0, 4, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

5.

         /-------- reached the end, return to the next position from which we began to move
2, 0, 0, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

6.

 /-------- move to the right
2, 0, 0, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

7.

    /-------- continue
0, 2, 0, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

8.

       /-------- stop and go to the next line
0, 0, 2, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
Enter fullscreen mode Exit fullscreen mode

It is also worth noting that in the case of horizontal moves, the rows move independently of each other, and in the case of vertical moves, the columns.

In the code, this algorithm is implemented by the methods Direction#build_traversal (building a path to traverse the field), Grid#traverse_from (movement of a specific cell in the direction), and Grid#move_in is a public method using the previous two.

Merging

Commit: https://github.com/dev-family/wasm-2048/commit/7e08b5af6008e6f9d716d7c9a41b0810e869df9e

Combining tiles has one nuance: tiles do not merge more than once, in the case of overlapping 4 identical tiles, the result will be 2 tiles of the next power. So, the tiles should have a certain state that is needed to be dropped before each move.

Additionally, I had to change the structure of TestCase, because correct behavior can only be tested in more than
one move.

Adding new tiles

Commit: https://github.com/dev-family/wasm-2048/commit/6082409412f6c19943c453c5d706d57bbcef538b

The rand package is used for random, which also works in the WASM environment by adding the wasm-bindgen feature.

In order not to break previous tests, which are not prepared for new tiles I've added a new flag field enable_new_tiles to the Grid structure.

Since new tiles need to be added only in the case of a valid move (i.e. at least one field change occurred during the move), the traverse_from signature changes. The method now returns a boolean value that indicates whether there was movement.

UI

Commit: https://github.com/dev-family/wasm-2048/commit/356e0889a84d7fc2582662e76238f94fc69bfed7

The UI part is pretty simple to implement, especially for those familiar with React and/or JSX. You can read about the html! Macro from Yew here,
and about components in general here. It reminds me a lot of React in pre-hook times.

There was no documentation of working with the keyboard, and, in principle, the services are not documented in any way, so you need to read the sources, watch examples.

Animations

Commit: https://github.com/dev-family/wasm-2048/commit/e258748ab114ec5f930dbeb77d674bdbc5e63b1a.

To make the interface look more lively, we need to add animations.

They are implemented on top of regular CSS transitions. We make tiles to remember their previous position while rendering we display the tile immediately in the old position, and on the next tick - in the new one.

Latest comments (1)

Collapse
 
ben profile image
Ben Halpern

This is a great walkthrough! I'm going to give this a try.