Rust + WebAssembly で、ブラウザ上で動く「ラングトンのアリ」を実装する

## はじめに

先日、会社の同僚が「ラングトンのアリ」を紹介してくれました。
面白そうなので、Rust +  WebAssembly を使い、ブラウザ上で動くものを実装してみました。

使用したRustのバージョンは、記事執筆時点で最新の安定版である`1.83.0`です。

## 「ラングトンのアリ ( Langton's ant )」とは

https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%82%B0%E3%83%88%E3%83%B3%E3%81%AE%E3%82%A2%E3%83%AA

- 2次元格子上で動作するシンプルなチューリングマシンの一種
- 2次元格子上の各マスは「白」か「黒」の状態を持つ
- 2次元格子上の任意の位置を「アリ」とする（赤などで表示する）
- 下記のルールに従って「アリ」が動く
  1. 現在いるマスが白なら右に90°回転し、黒なら左に90°回転する
  1. 現在いるマスの色を反転する（白ならば黒、黒ならば白）
  1. 1マス前進する
  1. 1. に戻る

## 準備

:::note info
まだRustとCargoをインストールしていない場合、[こちら](https://www.rust-lang.org/ja/learn/get-started)などを参考にインストールしておいて下さい。
:::

### `wasm-pack`のインストール

今回は[wasm-pack](https://rustwasm.github.io/wasm-pack/)という、`wasm-bindgen`を使用してRustでのWebAssembly開発を良い感じに扱ってくれるツールを使用するため、

```bash
$ cargo install wasm-pack
```

でインストールしておきます。

### ローカルでHTTPサーバをホストできる環境の用意

WebAssemblyを動かすために、ローカルでHTTPサーバをホストできる環境を用意します。

これは好きな方法で良いのですが、

1. Node.jsがインストールされている環境であれば[http-server
](https://www.npmjs.com/package/http-server)
2. Pythonがインストールされている環境であれば[http.server](https://docs.python.org/3/library/http.server.html)
3. VS Codeで開発していれば[HTML Preview](https://marketplace.visualstudio.com/items?itemName=george-alisson.html-preview-vscode)拡張機能

などを使うのが良いと思います。

### Rustプロジェクトの新規作成

任意のディレクトリで

```bash
$ cargo new --lib langtons_ant
```

などで新しいRustプロジェクトを作成しておきます。

## 実装

### `Cargo.toml`の編集

作成されたディレクトリの中に、`Cargo.toml`というファイルがあり、中身が下記のようになっているはずです。

```toml:Cargo.toml
[package]
name = "langtons_ant"
version = "0.1.0"
edition = "2021"
```

この下に次の中身をコピペします。

```toml:Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
  "CanvasRenderingContext2d",
]

# Windows環境では下記を書かないと`wasm-package build`時にエラーになる
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
```

### `index.html`の作成

次に、`Cargo.toml`と同じプロジェクトのルートディレクトリに、`index.html`という名前でHTMLファイルを作ります。

:::note info
今回はサンプルプロジェクトのため、HTMLファイルの中にJavaScriptを埋め込むという簡易的な形を取りましたが、実際にWebAssemblyのプロジェクトを作成する際にはこのような形は避け、きちんとフロントエンド開発環境を整備するほうが良いでしょう。
:::

```html:index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>Langton's Ant</title>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        canvas {
            background-color: #eee;
            display: block;
            margin: 0 auto;
            border: 1px solid #ccc;
        }
    </style>
</head>

<body>
    <canvas id="canvas" width="640" height="480"></canvas>
    <script type="module">
        import init, { LangtonsAnt } from "./pkg/langtons_ant.js";

        async function main() {
            await init();

            const canvas = document.getElementById("canvas");
            const ctx = canvas.getContext("2d");

            const cellSize = 4;

            const width = canvas.width / cellSize;
            const height = canvas.height / cellSize;

            const ant = new LangtonsAnt(width, height);

            function update() {
                ant.step();
                ant.render(ctx, cellSize);
                requestAnimationFrame(update);
            }

            update();
        }

        main();
    </script>
</body>

</html>
```
### `src/lib.rs`の編集

`src/lib.rs`に次の内容を実装します。

```Rust:src/lib.rs
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
use web_sys::CanvasRenderingContext2d;

#[derive(Debug, PartialEq)]
enum Direction {
    Up,
    Right,
    Down,
    Left,
}

impl Direction {
    pub fn turn_right(&self) -> Direction {
        match &self {
            Direction::Up => Direction::Right,
            Direction::Right => Direction::Down,
            Direction::Down => Direction::Left,
            Direction::Left => Direction::Up,
        }
    }

    pub fn turn_left(&self) -> Direction {
        match &self {
            Direction::Up => Direction::Left,
            Direction::Right => Direction::Up,
            Direction::Down => Direction::Right,
            Direction::Left => Direction::Down,
        }
    }
}

#[wasm_bindgen]
pub struct LangtonsAnt {
    width: usize,
    height: usize,
    x: usize,
    y: usize,
    direction: Direction,
    black_cells: HashSet<(usize, usize)>,
}

#[wasm_bindgen]
impl LangtonsAnt {
    #[wasm_bindgen(constructor)]
    pub fn new(width: usize, height: usize) -> LangtonsAnt {
        LangtonsAnt {
            width,
            height,
            x: width / 2,
            y: height / 2,
            direction: Direction::Up,
            black_cells: HashSet::new(),
        }
    }

    pub fn step(&mut self) {
        if self.black_cells.contains(&(self.x, self.y)) {
            self.black_cells.remove(&(self.x, self.y));
            self.direction = self.direction.turn_left();
        } else {
            self.black_cells.insert((self.x, self.y));
            self.direction = self.direction.turn_right();
        }

        match self.direction {
            Direction::Up => {
                if self.y > 0 {
                    self.y -= 1;
                } else {
                    self.y = self.height - 1;
                }
            }
            Direction::Right => {
                if self.x < self.width - 1 {
                    self.x += 1;
                } else {
                    self.x = 0;
                }
            }
            Direction::Down => {
                if self.y < self.height - 1 {
                    self.y += 1;
                } else {
                    self.y = 0;
                }
            }
            Direction::Left => {
                if self.x > 0 {
                    self.x -= 1;
                } else {
                    self.x = self.width - 1;
                }
            }
        }
    }

    pub fn render(&self, context: &CanvasRenderingContext2d, cell_size: f64) {
        context.set_fill_style_str("white");
        context.fill_rect(
            0.0,
            0.0,
            (self.width as f64) * cell_size,
            (self.height as f64) * cell_size,
        );

        context.set_fill_style_str("black");
        for &(x, y) in &self.black_cells {
            context.fill_rect(
                (x as f64) * cell_size,
                (y as f64) * cell_size,
                cell_size,
                cell_size,
            );
        }

        context.set_fill_style_str("red");
        context.fill_rect(
            (self.x as f64) * cell_size,
            (self.y as f64) * cell_size,
            cell_size,
            cell_size,
        );
    }
}

#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_turn_right() {
        assert_eq!(Direction::Up.turn_right(), Direction::Right);
        assert_eq!(Direction::Right.turn_right(), Direction::Down);
        assert_eq!(Direction::Down.turn_right(), Direction::Left);
        assert_eq!(Direction::Left.turn_right(), Direction::Up);
    }

    #[test]
    fn test_turn_left() {
        assert_eq!(Direction::Up.turn_left(), Direction::Left);
        assert_eq!(Direction::Right.turn_left(), Direction::Up);
        assert_eq!(Direction::Down.turn_left(), Direction::Right);
        assert_eq!(Direction::Left.turn_left(), Direction::Down);
    }

    #[test]
    fn test_new_langtons_ant() {
        let ant = LangtonsAnt::new(10, 10);
        assert_eq!(ant.width, 10);
        assert_eq!(ant.height, 10);
        assert_eq!(ant.x, 5);
        assert_eq!(ant.y, 5);
        assert_eq!(ant.direction, Direction::Up);
        assert!(ant.black_cells.is_empty());
    }

    #[test]
    fn test_step_ant_flips_and_turns_right_when_white() {
        let mut ant = LangtonsAnt::new(10, 10);
        ant.step();
        assert_eq!(ant.x, 6);
        assert_eq!(ant.y, 5);
        assert_eq!(ant.direction, Direction::Right);
        assert!(ant.black_cells.contains(&(5, 5)));
    }

    #[test]
    fn test_step_ant_flips_and_turns_left_when_black() {
        let mut ant = LangtonsAnt::new(10, 10);
        ant.black_cells.insert((5, 5));

        ant.step();
        assert_eq!(ant.x, 4);
        assert_eq!(ant.y, 5);
        assert_eq!(ant.direction, Direction::Left);
        assert!(!ant.black_cells.contains(&(5, 5)));
    }
}

```

以上で、一通りの実装が完了しました。

### ユニットテストが通ることを確認

一応ユニットテストを実装したので、それが通ることを確認しましょう。

```bash
$ cargo test
```

を実行し、

```bash
running 5 tests
test tests::test_new_langtons_ant ... ok
test tests::test_step_ant_flips_and_turns_left_when_black ... ok
test tests::test_step_ant_flips_and_turns_right_when_white ... ok
test tests::test_turn_left ... ok
test tests::test_turn_right ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

のような表示が出ればOKです。

## ビルド

下記を実行し、ビルドします。

```bash
$ wasm-pack build --target web
```

```bash
[INFO]: :-) Done in 8.42s
[INFO]: :-) Your wasm pkg is ready to publish at {プロジェクトのルートディレクトリへのパス}\pkg.
```

のような表示が出たら成功です。

## 実行

プロジェクトのルートディレクトリ（先ほど編集した`Cargo.toml`や`index.html`があるディレクトリ）に移動し、下記の手順などでHTTPサーバを起動します。

### Node.jsがインストールされている環境の場合

```bash
$ npx http-server .
```

### Pythonがインストールされている環境の場合

```bash
$ python -m http.server 8000
```

### VS Codeの拡張機能「HTML Preview」を使用する場合

`index.html`を開いた状態で、タブの右の方にあるプレビューボタンを押すか、`Ctrl+K V`（デフォルトの場合）

立ち上がったサーバー（`http://127.0.0.1:8000/`など）にアクセスし、下記のような画面が出れば成功です。

![langstons_ant_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/9281b633-4184-01eb-7cc6-f2dee868fc00.gif)

## その他

どこかのサーバーへデプロイする場合、`index.html`と`pkg`ディレクトリをデプロイしてやれば動くはずです。

## おわりに

とても単純なアルゴリズムから、複雑な模様が描かれるのは見ていて楽しいですね。

今回はBevyなどのゲームエンジンを使用せず、あえて`wasm-bindgen`を使用してWebAssemblyを直接作成する方法を試してみましたが、WebAssemblyの作成も、JavaScriptからの呼び出しも（この程度であれば）意外とすんなりとできて驚きました。

今回も楽しんでコードを書くことができました。この記事が皆様の良きRustライフの一助になれば幸いです🦀

## 参考文献

https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%82%B0%E3%83%88%E3%83%B3%E3%81%AE%E3%82%A2%E3%83%AA

https://qiita.com/osanshouo/items/40f087cc79a1446ad7ef

https://rustwasm.github.io/docs/book/

https://rustwasm.github.io/wasm-pack/book/

https://rustwasm.github.io/wasm-pack/book/cargo-toml-configuration.html

https://zenn.dev/enken/articles/enken-python-http-server

https://zenn.dev/rabee/articles/node-npx-http-server