ReScript で作ったモアイまわし風ブラウザゲームの実装紹介

# はじめに

この記事は [Opt Technologies Advent Calendar 2020](https://qiita.com/advent-calendar/2020/opt-technologies) の 6 日目の記事です。
この記事を書いたタイミングでふと弊社アドカレを見たら ~~取られているのに記事が書かれていない枠がいっぱいあった~~ 枠が空いていたので、折角なのでアドカレ記事ということにしました。 

皆さんは名作ブラウザゲーム[^1]「[モアイまわし](https://skt-products.com/)」をご存知でしょうか？
あの複雑なモアイの挙動をどう実現しているのか朝も起きずに悩んだ末、これならそれっぽくなるんじゃないかという方針を思いついたので、最近（僕の中で）話題の ReScript を使って実装してみました。
そこで今回は、その実装（リポジトリは[こちら](https://github.com/hey-cube/moai-mawashi)）と ReScript の紹介をしたいと思います。
もしおかしなところがあったらコメント欄などで ~~優しく~~ 指摘していただけると助かります。

# 完成品

百聞は一見にしかず、まずは実際に動くものをご覧ください。

https://moai-mawashi.glitch.me/

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">どこかで見たことあるようなブラウザゲームが完成した<a href="https://t.co/rXXiCOTB0K">https://t.co/rXXiCOTB0K</a> <a href="https://t.co/WMjCkVhyIs">pic.twitter.com/WMjCkVhyIs</a></p>&mdash; へいきゅーぶ (@hey_cube) <a href="https://twitter.com/hey_cube/status/1314891669972553730?ref_src=twsrc%5Etfw">October 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# ロジック

今回思いついた方針は「モアイごとに直線を引き、その直線とマウスポインタの距離からモアイの角度を決定する」というものです[^logic]。
ランダムに引いた直線との距離を使うことで、それぞれのモアイがマウスポインタの動きに対して違った動きをしてくれるはずです。

手順としては、まず直線の交点 p をランダムに決め、モアイごとの直線の傾き a<sub>n</sub> と回転の周期[^rotate] c<sub>n</sub> もランダムに決めます。
交点と傾きから切片は求められるので、これでモアイごとに直線が引けることになります。
ゲームが開始したらマウスポインタの位置と直線の距離 d を求め、x = 360 * (d / c<sub>n</sub>) としたとき、`transform: rotate(${x}deg);` をモアイに適用すれば、モアイが回転してくれます。
マウスポインタが直線上にあるときの距離は 0 なので、交点上にマウスポインタがあるときは全てのモアイの回転が 0 （全てのモアイが立っている状態）になります。つまり、交点が正解の位置になる訳です。
最初に周期を決めましたが、「マウスポインタが動ける範囲内で引ける最長の線分の長さ」よりも大きい周期が 1 つでもあった場合、そのモアイは交点以外では立たないことになるので、必ず正解の位置は交点だけになります。

# 使用技術

## ReScript

> The JavaScript-like language you have been waiting for.

https://rescript-lang.org/

ReScript とは、OCaml ライクな AltJS です。
BuckleScript と Reason をご存知の方であれば、それらを統合したものだ[^2]と言えば通じると思います。
Pattern Matching や Variant などの便利な機能が揃っています。
また、Interop が充実している点も非常に使い勝手が良いです。
文法は OCaml と JavaScript を足して 2 で割ったようなものになっています。

## ReasonReact

> ReasonReact helps you use Reason to build React components with deeply integrated, strong, static type safety.

https://reasonml.github.io/reason-react/docs/en/what-and-why

ReasonReact は、Reason（もとい ReScript ）で React を使うためのライブラリです。
ちゃんと（？） React hooks とかも使えます。
なお、本記事では React の細かい説明はしません。ご了承ください。

## Parcel

> Parcel is a web application bundler, differentiated by its developer experience. It offers blazing fast performance utilizing multicore processing, and requires zero configuration.

https://parceljs.org/getting_started.html

今回はバンドラに Parcel を使います。
ReScript ファイルそれぞれが JavaScript ファイルに変換されるので、それらをバンドルするのに使います。
設定ファイルを書かなくて良いので便利ですね。

## Glitch

> Glitch is the friendly community where everyone codes together!!

https://glitch.com/

Glitch はウェブアプリをホスティングできるサービスです。
HTML + JavaScript + CSS から成る静的なサイトから、Express と SQLite を使ったバックエンドを持つものまで様々なアプリを実現できます。

# セットアップ

まず、ReScript のコンパイラである [bs-platform](https://www.npmjs.com/package/bs-platform) をグローバルにインストールします。

```
$ npm install -g bs-platform
```

次に、 `bsb -init` を実行して雛形を生成します[^3]。

```
$ bsb -init moai-mawashi
```

reason-react やら parcel-bundler やらを入れて、

```
$ npm install --save-dev reason-react parcel-bundler
```

[公式ドキュメント](https://rescript-lang.org/docs/manual/latest/build-configuration)や[ bsb のテンプレート](https://github.com/rescript-lang/rescript-compiler/blob/master/jscomp/bsb/templates/react-hooks/bsconfig.json)を参考にしつつ、`bsconfig.json` も書きます。

```bsconfig.json
{
  "name": "moai-mawashi",
  "reason": { "react-jsx": 3 },
  "bsc-flags": ["-bs-super-errors"],
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "commonjs",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "bs-dependencies": ["reason-react"],
  "refmt": 3
}
```

ビルドコマンドとかをどうするかは結構悩んだんですが、最終的にこうなりました。
開発時は `npm run re:watch` しながら `npm run start` することで良い感じに変更がブラウザに反映されます。
Parcel の設定は書いていないので、ビルド結果は `dist/` 以下に吐かれます。

```package.json
  "scripts": {
    "start": "parcel public/index.html --open",
    "build": "bsb -make-world -clean-world && parcel build public/index.html",
    "re:watch": "bsb -make-world -clean-world -w"
  }
```

# 実装

ディレクトリ構造はこんな感じ[^4]。
`.bs.js` ファイルは bsb が吐いた JavaScript ファイルです。

```
moai-mawashi/
├── README.md
├── bsconfig.json
├── dist/
├── node_modules/
├── package-lock.json
├── package.json
├── public
│   ├── index.html
│   └── style.css
└── src
    ├── Helper.bs.js
    ├── Helper.res
    ├── Index.bs.js
    ├── Index.res
    ├── Type.bs.js
    ├── Type.res
    └── components
        ├── App.bs.js
        ├── App.res
        ├── Moai.bs.js
        ├── Moai.res
        ├── Row.bs.js
        └── Row.res
```

`src/components/App.res` を見ながら ReScript の紹介をしていきます。
（Qiita が ReScript のハイライトに対応していないのが悲しい...）

```App.res
@bs.send
external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@bs.val external document: Dom.document = "document"
@bs.val external alert: string => unit = "alert"
@bs.get external offsetTop: Dom.element => int = "offsetTop"
@bs.get external offsetLeft: Dom.element => int = "offsetLeft"
```

最上部にあるこいつらは JavaScript の Interop です。
例えば `@bs.send` で JavaScript のグローバル空間にあるオブジェクトに対するメソッド呼び出しが ReScript 内で行えるようになります。
また、 `@bs.val` で JavaScript のグローバル空間にあるオブジェクトを ReScript 内で呼び出せるようになります。
もうなんとなく分かると思いますが、最後の `@bs.get` によって JavaScript のグローバル空間にあるメソッドを ReScript 内で呼び出せるようになります。

このように、JavaScript の世界にしか存在しないオブジェクトやメソッドを、型をつけた上で自由に呼び出すことができるのです。
また、JavaScript コードそのものを `%%raw("const a = 1")` といった形でベタ書きすることもできます。
ここでは紹介し切れませんが、Interop には[他にも色々あります](https://rescript-lang.org/docs/manual/latest/interop-cheatsheet)。

```App.res
open Belt
open Type
open Helper
```

まず [Belt](https://rescript-lang.org/docs/manual/latest/api/belt#sidebar) とは、ReScript 用の標準ライブラリです。
色々と便利な関数が揃っているので、基本的に ReScript を書くときは必要になるでしょう。
今回はファイルの先頭で open していますが、`bsconfig.json` で `"bsc-flags": ["-open Belt"]` とすることで全ファイルに対して自動で読み込まれるので、そうする方がベターかも。

ところで、ReScript では Module やファイルの import/export が[必要ありません](https://rescript-lang.org/docs/manual/latest/import-export)。
つまりコード中でおもむろに `Belt.Array.length(["test"])` などと書けるのですが、いちいち `Belt.` を書くのをサボるために open しています。
Type と Helper は別ファイルなのですが、同様の理由で open しています。

```App.res
@react.component
let make = () => {
  ...
};
```

`@react.component` スニペットと `make()` 関数を用意する[^5]ことで component が作成できます。
props を受け取らせたい場合は `make()` に名前付き引数を持たせることで実現できます。
それでは `make()` の中身を見ていきましょう。

```App.res
  let (params, setParams) = React.useState(_ => initParams())
  let (mousePoint, setMousePoint) = React.useState((_): point => (0, 0))
  let (origin, setOrigin) = React.useState((_): point => (0, 0))
  let (intersection, setIntersection) = React.useState(_ => randomPoint())
  let (startTime, setStartTime) = React.useState(_ => 0.)
```

ご存知 `useState()` が使えます。
hooks などの React 関連の関数は `React` Module 配下に生えています。

```App.res
  let id = "field"

  React.useEffect1(() => {
    let field = getElementById(document, id)
    setOrigin(_ => (offsetLeft(field), offsetTop(field)))
    setStartTime(_ => Js.Date.now())
    None
  }, [])
```

`useEffect1()` も使えます。
（ここで最初に型をつけた `getElementById()` などを使っていますね。）
`React` Module 配下には `useEffect()` , `useEffect1()` , `useEffect2()` , ... , `useEffect7()` が生えており、それぞれ型が微妙に違います。
よく分かりませんが、用途に合ったものを使いましょう（？）

ちなみにここで使っている [`Js` Module](https://rescript-lang.org/docs/manual/latest/api/js) ですが、これは標準的な JavaScript API をラップしたライブラリです。
つまり、 `Js.Date.now()` は JavaScript の `Date.now()` と同一です。

ところで `React.useEffect1()` の第一引数の関数の最後に `None` とありますが、これは `None` を返しているという意味です。
まず関数の話をしますが、ReScript の関数に JavaScript でいうところの `return` にあたるものはありません。最後に評価された値が戻り値になります。

次に `None` とは何かですが、これは `Option` の一種です。
じゃあ `Option` はなんなんだという話ですが、これは[「存在しない可能性のある値」を表します](https://rescript-lang.org/docs/manual/latest/null-undefined-option)。
JavaScript でいう `null` とか `undefined` がない代わりに、ReScript ではこの `Option` を使うことでフワフワした値を型安全に扱うことができます。
`Option` には `Some('a)` と `None` があり、後者は「値が存在しない」ことを表しています。
`React.useEffect1()` の第一引数の型は `unit => option<unit => unit>` なのですが、今回は副作用を起こすだけなので特段何かを返す必要は（多分）ありません。故に `None` を返している、という訳です。

```App.res
  let rowIds = Array.range(0, horizontalCells - 1)
  let children = Array.map(rowIds, rowId => {
    let key = "row-" ++ Int.toString(rowId)
    let p = Array.getExn(params, rowId)
    let rotations = calculateRotations(intersection, mousePoint, p)

    <Row key rowId rotations />
  })

  <div id onClick onMouseMove> {React.array(children)} </div>
```

ReasonReact では我らが JSX が使えます[^jsx]。
イベントリスナーの登録もできます（イベントリスナーの実装は割愛）。

折角（？）ReScript を使うんだから `Array` じゃなくて `List` を使った方が良いかとも思いましたが、 `Array` の方が取り回しやすかったのでこうなりました。
ReScript は `List` や `Array`、 `Tuple` なんかもあるので、コードの規模や性質から好きなものを選べるのが良いですね。
JSX に直接 `Array` を埋め込むことはできないので、 `React.array()` を忘れないようにしましょう。

# その他 ReScript の特徴

今回のコードでは紹介できませんでしたが、ReScript には他にも色々と便利な機能があります。

- [Variant](https://rescript-lang.org/docs/manual/latest/variant) と [Pattern Matching](https://rescript-lang.org/docs/manual/latest/pattern-matching-destructuring)
- [Pipe](https://rescript-lang.org/docs/manual/latest/pipe)
- [Lazy Value](https://rescript-lang.org/docs/manual/latest/lazy-values)
- etc...

昨今のフロントエンドは大量の状態を管理しないといけないですが、ReScript の強力な型システムはそれと相性が良いような気がします。
シンタックスが JavaScript 風で React の知識も生かせるので、趣味の範囲であれば割と導入もしやすい（と思っている）ので、是非試してみてください。

# 最後に

様々な事情で最近記事を書いていなかったのですが、久々にアウトプットできて満足しました。
僕が書きたいことをただ書き散らかしただけの記事になってしまったような気もしますが、どこかの誰かに面白がってもらえれば幸いです。

[^1]: かつてはフラッシュゲームでしたが、今は素のブラウザで動くみたいです。スマホアプリ版もあるみたい。
[^logic]: 作った後に比較してみたら、本家モアイまわしはもっと複雑な動きをしている気がしました。
[^rotate]: モアイは自発的に回転する訳ではないので「周期」という表現が妥当かは微妙ですが、他に良い単語が思いつきませんでした。
[^2]: Reason を書いたことのある方向けに更に補足すると、ReScript はなんとセミコロンが不要です！
[^3]: ここで `-theme` オプションをつければ reason-react を使った雛形も作れます。
[^4]: 実はこの記事を書く直前まで[ファイルが `.re` だった](https://github.com/hey-cube/moai-mawashi/commit/8d9c8d0114f0c892d1baf1dcadaad8c14ae98051)（つまり私が書いていたのは ReScript じゃなくて Reason だった）のは内緒です。
[^5]: ここは [`~name` という名前付き引数を持たせた方が良かった](https://reasonml.github.io/reason-react/docs/en/components#reactcomponent)かも？
[^jsx]: ReScript 単体でも [JSX は使える](https://rescript-lang.org/docs/manual/latest/jsx)みたいです。