2048 WASM
Tags: showdev, rust, gamedev
Published: 2021-01-26 14:59:21

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](https://dev-to-uploads.s3.amazonaws.com/i/7p0yw9r3ypckni403hme.png)

# 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](https://dev-to-uploads.s3.amazonaws.com/i/ts8h9aio73hmxihjvdyo.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/zopndp2wviw3glzw3ysw.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/ed68wuxzlgun9g48andl.png)

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](https://awesomeopensource.com/project/flosse/rust-web-framework-comparison) provides a list of front-end frameworks for Rust.
The most popular option - [Yew](https://yew.rs/) - looks interesting, especially if you have experience with React.

Instructions for creating a project can be found [here](https://yew.rs/docs/en/getting-started/build-a-sample-app):

```bash
cargo new --lib rust-2048 && cd rust-2048
```

Next, add dependencies to Cargo.toml:

```toml
[dependencies]
yew = "0.17"
wasm-bindgen = "0.2.67"
```

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

```rust
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();
}
```

Create a directory for static files:

```bash
mkdir static
```

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

```html
<!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>
```

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

```bash
wasm-pack build --target web --out-name wasm --out-dir ./static
```

I got the following error while building:

```text
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"]
```

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

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

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):

```bash
cd static
python -m SimpleHTTPServer
```

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:

```text
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

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

```text
0, 0, 2, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

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.

```text
         /-------- empty cell, skip
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

2.

```text
      /-------- empty cell, skip
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

3.

```text
    /-------- start to shift to the right
2, 4, 0, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

4.

```text
       /-------- continue
2, 0, 4, 0
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

5.

```text
         /-------- 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
```

6.

```text
 /-------- move to the right
2, 0, 0, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

7.

```text
    /-------- continue
0, 2, 0, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

8.

```text
       /-------- stop and go to the next line
0, 0, 2, 4
0, 0, 0, 0
0, 0, 0, 0
0, 0, 0, 0
```

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](https://crates.io/crates/rand) 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](https://yew.rs/docs/en/concepts/html/),
and about components in general [here](https://yew.rs/docs/en/concepts/components). 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.