ブロックゲームでRustキャッチアップ

## 目的

「ブラウザのしくみ」が気になって仕方がないので購入したのだが、そもそもRust書いたことないわ...という状況だったので前段階としてRustのキャッチアップをする。

せっかくなのでWebAssemblyもキャッチアップしたい...という欲が出たのでブロックゲーム的なものを作る。

[購入したものはこちら](https://www.amazon.co.jp/%EF%BC%BB%E4%BD%9C%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%EF%BC%BD%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF%E2%94%80%E2%94%80HTTP%E3%80%81HTML%E3%80%81CSS%E3%80%81JavaScript%E3%81%AE%E8%A3%8F%E5%81%B4-WEB-DB-PRESS-plus%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/4297145464/ref=cm_cr_arp_d_product_top?ie=UTF8)

## 注意

Rust初心者目線で気になった点を書いているので、同じようなシチュエーションの方の読み物になれば嬉しいなと思っています。（wasmについても何もわかっていないです）


## rustインストール

さくっとインストール。

https://doc.rust-jp.rs/book-ja/title-page.html

## WebAssemblyビルドツールをインストール

ビルドツールは別途インストールが必要っぽい。
`rustup`はツールチェインマネージャー。
`cargo`はパッケージマネージャー。

```zsh
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
```

## VSCode 拡張

こちらの記事を参考に導入

https://zenn.dev/t4aru/articles/4a77ec07432e57

## Rustセットアップ

適当なプロジェクトディレクトリを作成して以下のコマンド実行。
最小のプロジェクトが生成された。

```zsh
cargo init
```

必要なパッケージをインストール

```zsh
cargo add wasm-bindgen
cargo add js-sys
cargo add web-sys --features "CanvasRenderingContext2d,Document,Element,HtmlCanvasElement,Window"
```

`cargo.toml`に以下を追加。
shared libraryとして出力するための設定。

```
[lib]
crate-type = ["cdylib"]
```

`src/lib.rs`にWebAssemblyとして出力する内容を記述

`lib.rs`はライブラリクレートのエントリーポイントらしいので、WebAssembly出力時にはこっちを使う。
`main.rs`はバイナリクレートらしい。

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
```

最後にwasm用のビルドをする。
ビルド結果は`pkg`に生成される。

```zsh
wasm-pack build --target web
```


ここまででRustのセットアップは完了
検証用にwasmをコールするためのHTMLなどを準備する

## 呼び出し側のHTML準備

rootにこんな感じのファイルを作成

```zsh
mkdir www
touch www/index.html
touch www/index.js
```

htmlでは所定のjsを読み込む

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Rust Block Game</title>
  </head>
  <body>
    <script type="module" src="./index.js"></script>
  </body>
</html>
```

Rustのビルド成果物は`pkg`に生成する想定。

```js
import init, { greet } from '../pkg/rust_block_game.js';

async function run() {
    await init();
    const result = greet("Rust");
    console.log(result);
}

run();
```

ここまでで`index.html`のコンソール上に`Hello, Rust!`が表示される。
こういう感じなのか。なるほど(´・ω・｀)

wasmはjsからロードする模様。
wasmからDOMを操作できなかったりするらしいので、適材適所で使うという理解。

## ざっくりグリッド実装

めちゃくちゃざっくり実装。
グリッドの生成と、ブロックの回転と移動まで。

```rust
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};

#[derive(Clone, Copy, PartialEq)]
pub enum Cell {
    Empty,
    Filled(u8),
}
// テトリミノの形状を定義
#[derive(Clone, Copy, PartialEq)]
pub enum BlockType {
    I,
    O,
    T,
    S,
    Z,
    J,
    L,
}

#[derive(Clone)]
pub struct BlockShape {
    block_type: BlockType,
    cells: Vec<Vec<bool>>, // trueなら埋まっているセル
    color: u8,
}

impl BlockShape {
    pub fn new(block_type: BlockType) -> Self {
        let (cells, color) = match block_type {
            BlockType::I => (
                vec![
                    vec![false, false, false, false],
                    vec![true, true, true, true],
                    vec![false, false, false, false],
                    vec![false, false, false, false],
                ],
                0, // cyan
            ),
            BlockType::O => (
                vec![vec![true, true], vec![true, true]],
                1, // yellow
            ),
            BlockType::T => (
                vec![
                    vec![false, true, false],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                2, // purple
            ),
            BlockType::S => (
                vec![
                    vec![false, true, true],
                    vec![true, true, false],
                    vec![false, false, false],
                ],
                5, // green
            ),
            BlockType::Z => (
                vec![
                    vec![true, true, false],
                    vec![false, true, true],
                    vec![false, false, false],
                ],
                6, // red
            ),
            BlockType::J => (
                vec![
                    vec![true, false, false],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                3, // blue
            ),
            BlockType::L => (
                vec![
                    vec![false, false, true],
                    vec![true, true, true],
                    vec![false, false, false],
                ],
                4, // orange
            ),
        };

        BlockShape {
            block_type,
            cells,
            color,
        }
    }

    // 時計回りに90度回転
    pub fn rotate(&mut self) {
        let n = self.cells.len();
        let mut rotated = vec![vec![false; n]; n];

        for i in 0..n {
            for j in 0..n {
                rotated[j][n - 1 - i] = self.cells[i][j];
            }
        }

        self.cells = rotated;
    }
}

#[wasm_bindgen]
pub struct Game {
    context: CanvasRenderingContext2d,
    canvas: HtmlCanvasElement,
    board: Vec<Vec<Cell>>,
    board_width: usize,
    board_height: usize,
    cell_size: f64,

    current_block: Option<BlockShape>,
    current_pos: (usize, usize), // (x, y)
}

#[wasm_bindgen]
impl Game {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Result<Game, JsValue> {
        let document = web_sys::window().unwrap().document().unwrap();
        let canvas = document.get_element_by_id("game-canvas").unwrap();
        let canvas: HtmlCanvasElement = canvas
            .dyn_into::<HtmlCanvasElement>()
            .map_err(|_| ())
            .unwrap();

        let canvas_width = 300.0;
        let canvas_height = 600.0;
        canvas.set_width(canvas_width as u32);
        canvas.set_height(canvas_height as u32);

        let context = canvas
            .get_context("2d")?
            .unwrap()
            .dyn_into::<CanvasRenderingContext2d>()
            .unwrap();

        let board_width = 10;
        let board_height = 20;
        let cell_size = canvas_width / board_width as f64;

        let board = vec![vec![Cell::Empty; board_width]; board_height];

        Ok(Game {
            context,
            canvas,
            board,
            board_width,
            board_height,
            cell_size,
            current_block: None,
            current_pos: (0, 0),
        })
    }

    pub fn draw(&self) {
        // 背景を黒に
        self.context.set_fill_style_str("black");
        self.context.fill_rect(
            0.0,
            0.0,
            self.canvas.width() as f64,
            self.canvas.height() as f64,
        );

        // グリッドとセルを描画
        for (y, row) in self.board.iter().enumerate() {
            for (x, cell) in row.iter().enumerate() {
                match cell {
                    Cell::Empty => {
                        // 空のセルは灰色の枠線のみ
                        self.context.set_stroke_style_str("gray");
                        self.context.stroke_rect(
                            x as f64 * self.cell_size,
                            y as f64 * self.cell_size,
                            self.cell_size,
                            self.cell_size,
                        );
                    }
                    Cell::Filled(color) => {
                        // 塗りつぶされたセルは色付きで描画
                        let color_str = match color {
                            0 => "cyan",
                            1 => "yellow",
                            2 => "purple",
                            3 => "blue",
                            4 => "orange",
                            5 => "green",
                            6 => "red",
                            _ => "white",
                        };
                        self.context.set_fill_style_str(color_str);
                        self.context.fill_rect(
                            x as f64 * self.cell_size,
                            y as f64 * self.cell_size,
                            self.cell_size,
                            self.cell_size,
                        );
                    }
                }
            }
        }

        if let Some(block) = &self.current_block {
            for (i, row) in block.cells.iter().enumerate() {
                for (j, &is_filled) in row.iter().enumerate() {
                    if is_filled {
                        let x = self.current_pos.0 + j;
                        let y = self.current_pos.1 + i;

                        self.context.set_fill_style_str(match block.color {
                            0 => "cyan",
                            1 => "yellow",
                            2 => "purple",
                            3 => "blue",
                            4 => "orange",
                            5 => "green",
                            6 => "red",
                            _ => "white",
                        });

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

        // デバッグ用
        web_sys::console::log_1(
            &format!(
                "Debug Info:\nBoard: {}x{}\nCell Size: {}\nCanvas: {}x{}",
                self.board_width,
                self.board_height,
                self.cell_size,
                self.canvas.width(),
                self.canvas.height()
            )
            .into(),
        );
    }

    // 左に移動
    pub fn move_left(&mut self) {
        if let Some(_) = &self.current_block {
            if self.current_pos.0 > 0 {
                self.current_pos.0 -= 1;
                self.draw();
            }
        }
    }

    // 右に移動
    pub fn move_right(&mut self) {
        if let Some(block) = &self.current_block {
            if self.current_pos.0 + block.cells.len() < self.board_width {
                self.current_pos.0 += 1;
                self.draw();
            }
        }
    }

    // 下に移動
    pub fn move_down(&mut self) {
        if let Some(block) = &self.current_block {
            if self.current_pos.1 + block.cells.len() < self.board_height {
                self.current_pos.1 += 1;
                self.draw();
            }
        }
    }

    // ミノを回転
    pub fn rotate(&mut self) {
        if let Some(block) = &mut self.current_block {
            block.rotate();
            self.draw();
        }
    }

    // テスト用：テノを生成して表示
    pub fn spawn_test_mino(&mut self) {
        let block = BlockShape::new(BlockType::T); // Tミノをテスト用に生成
        self.current_block = Some(block);
        self.current_pos = (self.board_width / 2 - 2, 0); // 上端中央に配置
        self.draw();
    }

    pub fn test_fill(&mut self) {
        self.board[0][0] = Cell::Filled(0); // cyan
        self.board[1][1] = Cell::Filled(1); // yellow
        self.board[2][2] = Cell::Filled(2); // purple
        self.draw();
    }
}
```

```js
import init, { Game } from '../pkg/rust_block_game.js';

async function run() {
    await init();
    const game = new Game();
    // game.draw_test();
    // game.test_fill();
    game.spawn_test_mino();


    // キーボードイベントの処理
    document.addEventListener('keydown', (event) => {
        switch (event.key) {
            case 'ArrowLeft':
                game.move_left();
                break;
            case 'ArrowRight':
                game.move_right();
                break;
            case 'ArrowDown':
                game.move_down();
                break;
            case 'ArrowUp':    // 上キーで回転
                game.rotate();
                break;
        }
    });

}

run();
```


### 結論

最低限の記述はなんとなくわかったので、Rustを書いたことがない人の取っ掛かりとしてはいいかも？と思いました。
業務アプリからは少し外れてしまうので、wasmに興味がない場合は普通にバックエンドを書くとかの方がわかりやすそう。

書き心地良すぎてヤヴァイ。これはハマる。