好きな単語でクロスなワードパズルを作って共有できるWebサービスを作った

クロスなワードパズル[^1]を作って共有できるWebサービスを作りました。
こんなサービスです。
[![hash4word.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/63f4ffb6-9e4a-b897-e501-d4ef1713d767.gif)](https://kurehajime.github.io/hash4word/?code=44Kk44Oz44Kv44Oq44Oz44KwLOODreODg-OCr-ODnuODsyzjgqzjg47jg7Pjg4njg63jg5Us44Oq44Oz44KvLOODsyzjg6os44KvLOODrQ)

ここで遊べます。

https://kurehajime.github.io/hash4word/

## できること

### 問題を解く

バラバラに散りばめられた単語を入れ替えて、あらかじめ指定された4単語を、→方向と↓方向に作ればゲームクリアです。

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/a527639c-9498-f167-5cf9-b30366dc0961.png)](https://kurehajime.github.io/hash4word/?code=44Kk44Oz44Kv44Oq44Oz44KwLOODreODg-OCr-ODnuODsyzjgqzjg47jg7Pjg4njg63jg5Us44Oq44Oz44KvLOODsyzjg6os44KvLOODrQ)

上の回答例では

* [インクリング](https://www.smashbros.com/ja_JP/fighter/64.html)
* [ロックマン](https://www.smashbros.com/ja_JP/fighter/46.html)
* [ガノンドロフ](https://www.smashbros.com/ja_JP/fighter/23.html)
* [リンク](https://www.smashbros.com/ja_JP/fighter/03.html)

スマブラ参戦キャラクターの4人が答えです。

誰かが作った問題、もしくはランダムに生成された問題を解くことができます。

初期状態では以下の４つの辞書からランダムに問題が生成されます。

* 日本語2048単語
* 英語2048単語
* ポケモン図鑑(日本語)
* ポケモン図鑑(英語)

無限に遊べます。

### 問題を作る
自分で問題を作り、それを共有することもできます。

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/e9ab6fbd-5cb2-8787-c8e0-3e77d687a64a.gif)](https://kurehajime.github.io/hash4word/?code=44GK44GX44GE44KMLOOBneOBkuOBjSzjgYrjgYrjgojjgZ0s44GM44GE44G444GNLOOBiizjgYQs44GNLOOBnQ)


上のような感じで問題を作ったら、右下のSHARE URLからURLコピーして問題URLを共有しましょう。

```
https://kurehajime.github.io/hash4word/?code=44GK44GX44GE44KMLOOBneOBkuOBjSzjgYrjgYrjgojjgZ0s44GM44GE44G444GNLOOBiizjgYQs44GNLOOBnQ
```


## 技術解説

### 利用技術

主な利用技術はこんな感じです。

* 言語：TypeScript
* UIフレームワーク：React
* ビルド関連：Vite

そんなに特殊な構成ではないものの、バックエンドが存在しないという点は少し変わってるかもしれないですね。
バックエンドもJSで動いてるとかCloud Functions使ってるとかそういう意味ではなく、バックエンドが完全に存在していません。ある意味で真のサーバレスです。HTMLをローカルにコピーしてくればオフラインでも遊べるし、自分で問題も作れるし、誰かが作った問題でも(URLさえ知ってれば)遊べます。

### 自動問題生成

問題の生成ロジックは難しそうに見えて、実はとても単純です。

1. 単語のリストを用意する
単語のリストを用意します。単語の数が少なすぎると問題の生成に時間がかかるので多いほうがいいです。このアプリケーションの日本語辞書・英語辞書では[BIP-0039](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md)というビットコインのウォレット管理で利用されるニーモニックフレーズを利用しています。
2. 文字リストを用意する
文字のリストを用意します。「あいうえお…わをん」や「ABCD...XYZ」といったものです。
3. 文字リストからランダムに文字を4つ取り出す
文字リストがひらがななら「お」「い」「そ」「き」とかですね。
この4文字が単語と単語が交差する軸となります。
4. 単語リストを正規表現で検索する
単語リストを4つの正規表現で検索します。
「お」「い」「そ」「き」の例なら
`.{0,3}お.い.{0,3}`
`.{0,3}い.き.{0,3}`
`.{0,3}そ.き.{0,3}`
`.{0,3}お.そ.{0,3}`
こんな感じです。
「任意の文字が最大3文字＋軸となる文字＋任意の1文字＋軸となる文字＋任意の文字が最大3文字」
これで答えとなる単語を作ることが出来ます。
5. ４パターンの単語ができるまで３～４をループする
2000単語ある単語リストなら、100～300回くらいループを回すと見つかります。これで完成です。

### 描画まわり

ReactでSVG画像を動的に生成してゲーム画面を作っています。状態が変化すればSVG画像もそれに合わせて変わるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/5d353a5d-1c6c-5363-3fb0-651ed8ab0e69.png)

ちょっと動きが早すぎて分かりにくいかもしれませんが、パネルを入れ替えた時にアニメーションしています。

![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/360019d7-3e2f-7619-3e28-b1dd02eb59bd.gif)

これはどうやって実現してるのかというと、CSSを利用しています。
基本的にReactとアニメーションは相性が悪い…と思います。Reactは現在の状態をそのままUIに射影する思想なので、愚直に中間状態を持とうとすると再描画が大変なことになってしまいます。[useAnimation](https://usehooks.com/useAnimation/)などReact向けのアニメーションライブラリもありますが、今回の要件は単純だったのでCSSだけで実装しました。

エレメントが移動する際に移動の軌跡をアニメーションするには単にCSSで以下のように指定するだけです。

```
transition: all ease-in-out 0.1s;
```

この例では0.1秒かけて移動します。最初は加速して、最後は減速するように指定しています。
この辺のアニメーションについてはMDNの記事が参考になります。

https://developer.mozilla.org/ja/docs/Web/CSS/animation-timing-function

### 作った問題の共有

前述の通り、このWebアプリケーションにはバックエンドが存在しません。すべてフロントだけで実装しています。

勘の良い方はご察しの通り、このWebアプリケーションでは問題の情報をURLに全部詰め込んでいます。

具体的にはこんな感じでURL化しています。

たとえば以下のような問題の場合は

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33890/e791808d-8d1c-c6db-96e1-226cb44cf2db.png)

このように答えと軸となる文字を連結します。

```
おしいれ,そげき,おおよそ,がいへき,お,い,き,そ
```

そしてこれをBase64URLエンコードします。

```
44GK44GX44GE44KMLOOBneOBkuOBjSzjgYrjgYrjgojjgZ0s44GM44GE44G444GNLOOBiizjgYQs44GNLOOBnQ
```

それをURLの末尾にパラメータとして付加してあげると共有URLの完成です。

```
https://kurehajime.github.io/hash4word/?code=44GK44GX44GE44KMLOOBneOBkuOBjSzjgYrjgYrjgojjgZ0s44GM44GE44G444GNLOOBiizjgYQs44GNLOOBnQ
```

ここで重要なのは、Base64ではなくBase64URLを使うというところです。
Base64はデータを「A~Z,a~z,0~9+,/=」の64種類の文字に置き換えます。
これには罠があり、「+」や「/」が混じるため、URLパラメータに使うと問題が生じます。

そこで考案されたのがBase64 URLエンコードで、URLに混じると不味い「+」と「/」が無難な「-」「_」に置き換えられます。


## おわりに

今回はクロスなワードパズルを作りました。
遊んで作って共有してくれると幸いです。

https://kurehajime.github.io/hash4word/

あと他にもいろいろ作ってるので、よかったら遊んでみてください。


https://kurehajime.github.io/minesweeper/

https://colamone.com/

https://kurehajime.github.io/pongout/

[^1]:厳密な意味でのクロスワードパズルとはちょっと違うので、クロスなワードパズルとしています。