IchigoJam + MixJuice で Wordle

[IchigoJam](https://ichigojam.net/) と [MixJuice](http://mixjuice.shizentai.jp/) で [Wordle](https://www.nytimes.com/games/wordle/index.html) 的なゲームを作ってみた。

※IchigoJamはjig.jpの登録商標です。

# ルール

ゲームの開始時、正解として5文字の英単語が設定される。
プレイヤーは、正解の英単語を当てるため、5文字の英単語を入力する。
5文字であっても、英単語として存在しないと判定された入力は拒否される。
拒否されない入力をすると、その入力と正解との一致状況が提示される。
一致状況は、各文字について以下のいずれかで表される。

* 一致：正解と同じ文字が同じ位置にある
* 場所違い：その文字は正解に含まれるが、別の位置である
* なし：その文字は正解に含まれない

正解に含まれ、「一致」に対応しなかった文字1個につき、1回までのみ「場所違い」を出せる。
これを超える場合、正解に含まれる文字でも (既に「一致」や他の「場所違い」で消費されていれば)「なし」となりうる。

プレイヤーは、提示された一致状況を参考に、次に入力する英単語を決めることができる。
英単語の入力は1ゲームにつき6回まで行うことができ、6回以内に正解の英単語を入力できればクリア、できなければ失敗である。

今回の実装では、一致状況を以下のように表す。

* 一致：文字のまわりを白く囲み、白背景に黒い文字を表示する
* 場所違い：文字のまわりを白く囲み、黒背景に白い文字を表示する
* なし：文字のまわりを白く囲まず、黒背景に白い文字を表示する

# 使用したAPI

## Rando Free Random Word Generator API

[https://random-word-api.vercel.app/](https://random-word-api.vercel.app/)

指定した長さのランダムな英単語を取得できる。
正解の設定に使用する。

たとえば

```
https://random-word-api.vercel.app/api?length=5
```

にアクセスすると

```json
["gonad"]
```

のような形式のデータを返してくれる。

今回は使用しないが、取得する単語の頭文字を指定することもできる。

## Datamuse API

[Datamuse API](https://www.datamuse.com/api/)

スペル、発音、意味など、様々な観点から指定の単語に似た英単語を取得できる。
入力された単語が存在するかどうかの判定に使用する。
(入力のスペルに似た単語を取得し、完全一致する単語が来れば存在すると判定する)

たとえば

```
https://api.datamuse.com/words?max=1&sp=spila
```

にアクセスすると

```json
[{"word":"spill","score":1710}]
```

のような形式のデータを返してくれる。

# プログラム

```
10 'Wordle
20 CLS:FORI=0TO207:POKE#710+I,~PEEK(520+I):NEXT:R=0:LC0,-1:P=26:Q=1:N=0:?CHR$(10);"MJ GETS random-word-api.vercel.app/api?length=5":GSB160:IFN-5END
30 Y=3*R+1:LC6,Y:?CHR$(#E1):L=0
40 GSB180
50 IFC=8ANDL:L=L-1:LC9+3*L,Y:?" "
60 IFL<5AND64<CANDC<91:LC9+3*L,Y:?CHR$(C):[31+L]=C-65:L=L+1
70 IFC-10ORL<5GOTO40
80 LC0,-1:P=41:Q=3:N=0:?CHR$(10);"MJ GETS api.datamuse.com/words?max=1&sp=";:FORI=0TO4:?CHR$([31+I]+97);:NEXT:?"":GSB160:V=N=5:FORI=0TO4:V=VAND[41+I]=[31+I]:NEXT:IFVGOTO100ELSELC23,Y:?"?"
90 GSB180:IFCLC23,Y:?" ":GOTO50ELSEGOTO90
100 LC6,Y:?" ":FORI=0TO25:[I]=0:NEXT:H=5:FORI=0TO4:A=[26+I]:J=A=[31+I]:H=H-J:[36+I]=J*2:[A]=[A]+!J:NEXT:FORI=0TO4:A=[31+I]:J=[36+I]:IF!JAND[A][A]=[A]-1:J=1
110 X=8+3*I:LCX,Y-1:IFJ?CHR$(#88,#8C,#84)
120 LCX,Y:IFJ?CHR$(#8A,[31+I]+65+J/2*#A1,#85)
130 LCX,Y+1:IFJ?CHR$(#82,#83,#81)
140 NEXT:R=R+1:IFHIFR<6GOTO30ELSEFORI=0TO4:?CHR$([26+I]+65);:NEXT:?""
150 END
160 GSB180:Q=Q-(C=34):IF!QAND64<CANDC<91[P+N]=C-65:N=N+1
170 IFC=93RTNELSEGOTO160
180 C=INKEY():C=C-32*(96<CANDC<#7B):RTN
```

[OneFiveCrowdで実行する](https://onefivecrowd.mikecat.work/#text=%27+%EF%BD%BC%EF%BE%9E%EF%BD%AF%EF%BD%BA%EF%BD%B3+%EF%BE%8F%EF%BD%B4+%EF%BE%86+I%2FO+%EF%BE%80%EF%BE%8C%EF%BE%9E+%EF%BD%B6%EF%BE%97%0A%27+%EF%BD%B6%EF%BD%BF%EF%BD%B3+MixJuice+%EF%BE%89+%EF%BD%BE%EF%BE%82%EF%BD%BF%EF%BE%9E%EF%BD%B8+%EF%BD%A6+ON+%EF%BE%86%0A%27+%EF%BD%BC%EF%BE%83+%EF%BD%B8%EF%BE%80%EF%BE%9E%EF%BD%BB%EF%BD%B2%0A%0A%27+%EF%BD%BE%EF%BE%82%EF%BD%BF%EF%BE%9E%EF%BD%B8+%EF%BD%BA%EF%BE%9E+Enter+%EF%BE%83%EF%BE%9E+%EF%BD%BC%EF%BE%9E%EF%BD%AF%EF%BD%BA%EF%BD%B3%0ARUN&prg=CgAIJ1dvcmRsZQAAFACQQ0xTOkZPUkk9MFRPMjA3OlBPS0UjNzEwK0ksflBFRUsoNTIwK0kpOk5FWFQ6Uj0wOkxDMCwtMTpQPTI2OlE9MTpOPTA6P0NIUiQoMTApOyJNSiBHRVRTIHJhbmRvbS13b3JkLWFwaS52ZXJjZWwuYXBwL2FwaT9sZW5ndGg9NSI6R1NCMTYwOklGTi01RU5EAB4AHFk9MypSKzE6TEM2LFk6P0NIUiQoI0UxKTpMPTAAKAAGR1NCMTgwADIAHklGQz04QU5ETDpMPUwtMTpMQzkrMypMLFk6PyIgIgA8ADhJRkw8NUFORDY0PENBTkRDPDkxOkxDOSszKkwsWTo_Q0hSJChDKTpbMzErTF09Qy02NTpMPUwrMQBGABJJRkMtMTBPUkw8NUdPVE80MAAAUAC4TEMwLC0xOlA9NDE6UT0zOk49MDo_Q0hSJCgxMCk7Ik1KIEdFVFMgYXBpLmRhdGFtdXNlLmNvbS93b3Jkcz9tYXg9MSZzcD0iOzpGT1JJPTBUTzQ6P0NIUiQoWzMxK0ldKzk3KTs6TkVYVDo_IiI6R1NCMTYwOlY9Tj01OkZPUkk9MFRPNDpWPVZBTkRbNDErSV09WzMxK0ldOk5FWFQ6SUZWR09UTzEwMEVMU0VMQzIzLFk6PyI_IgBaACZHU0IxODA6SUZDTEMyMyxZOj8iICI6R09UTzUwRUxTRUdPVE85MABkAJhMQzYsWTo_IiAiOkZPUkk9MFRPMjU6W0ldPTA6TkVYVDpIPTU6Rk9SST0wVE80OkE9WzI2K0ldOko9QT1bMzErSV06SD1ILUo6WzM2K0ldPUoqMjpbQV09W0FdKyFKOk5FWFQ6Rk9SST0wVE80OkE9WzMxK0ldOko9WzM2K0ldOklGIUpBTkRbQV1bQV09W0FdLTE6Sj0xAABuACZYPTgrMypJOkxDWCxZLTE6SUZKP0NIUiQoIzg4LCM4QywjODQpAAB4ACpMQ1gsWTpJRko_Q0hSJCgjOEEsWzMxK0ldKzY1K0ovMiojQTEsIzg1KQAAggAeTENYLFkrMTpJRko_Q0hSJCgjODIsIzgzLCM4MSkAAIwAQk5FWFQ6Uj1SKzE6SUZISUZSPDZHT1RPMzBFTFNFRk9SST0wVE80Oj9DSFIkKFsyNitJXSs2NSk7Ok5FWFQ6PyIiAACWAARFTkQAAKAANEdTQjE4MDpRPVEtKEM9MzQpOklGIVFBTkQ2NDxDQU5EQzw5MVtQK05dPUMtNjU6Tj1OKzEAqgAUSUZDPTkzUlRORUxTRUdPVE8xNjAAtAAkQz1JTktFWSgpOkM9Qy0zMiooOTY8Q0FOREM8IzdCKTpSVE4)

このプログラムは、[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) でライセンスする。
このプログラム (改造したものを含む) を公開の場で利用する際は、出典を示していただけると嬉しい。
これは、Qiitaの利用規約に基づくプログラムの利用を禁止するものではない。

# 実行結果例

ゲーム開始。

![ゲーム開始](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/877e68b1-60ac-a689-3f4b-300715229fe3.png)

ゲームを進めていく。
入力した単語が存在しないと判定されると、右側にハテナが表示される。

![ハテナ表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/6a27920c-8ad8-7561-0d8a-5bee6caa9159.png)

単語を入力して確定する前の状態。
当たるかな…？

![確定前](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/bde2459d-5d31-f73c-d6d3-789c7922ca95.png)

違った！

![違った](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/56848975-6a70-fbb2-e72e-fa2224b2456f.png)

6回の制限ギリギリで、なんとか正解にたどり着くことができた。

![正解](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/942b8847-2289-8e4c-d99a-cc68ebe1b7d9.png)

別のゲーム。今度は6回入力せずに正解にたどり着けた。

![別のゲーム](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ef0422c3-d431-53e3-be1b-7720f83bfd66.png)

さらに別のゲーム。残り1文字でハマってしまい、正解を出すことができなかった。

![別のゲーム 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/77418750-2489-22d7-4a7e-387f862f4838.png)

# 解説

## 配列の割り当て

今回のプログラムでは、配列の各領域を以下のように割り当てている。

* 0～25：文字種ごとの出現数のカウント
* 26～30：正解の英単語
* 31～35：入力の英単語
* 36～40：正解との一致状況
* 41～：入力の英単語と似ているとして取得した英単語

## 10行目：タイトル

> ```
> 10 'Wordle
> ```

`FILES` 対応のタイトルである。

## 180行目：入力取得・正規化サブルーチン

> ```
> 180 C=INKEY():C=C-32*(96<CANDC<#7B):RTN
> ```

キー (もしくはUART) の入力を変数 `C` に取得し、アルファベット小文字であれば大文字に変換して返す。

## 160～170行目：JSON簡易パースサブルーチン

`"` の数のみに基づいて、JSON から文字列を取り出す。
以下の変数を設定した状態で呼び出す。

* `P` : 結果の格納を開始する配列の添字
* `N` : ゼロ (結果の文字数)
* `Q` : `"` がいくつあった場所を結果とみなすか

戻る際、変数 `N` に読み込んだ文字数が格納される。

> ```
> 160 GSB180
> ```

入力を取得する。

> ```
> Q=Q-(C=34)
> ```

入力された文字が `"` であれば、カウントを更新する。

> ```
> IF!QAND64<CANDC<91[P+N]=C-65:N=N+1
> ```

`"` の数が要求と一致しており、かつ入力がアルファベットであれば、配列に「何番目のアルファベットか」を格納し、文字数を更新する。
「入力がアルファベットであれば」のチェックが無いと、`"` もここに混ざってしまう。
アルファベット小文字は180行目のサブルーチンで大文字に変換しているので、ここでは大文字のみを考慮すればよい。

> ```
> 170 IFC=93RTNELSEGOTO160
> ```

入力が `]` であれば、入力の終わりとみなす。そうでなければ、入力の処理を続行する。

## 20行目：画面の初期化と正解の設定

> ```
> 20 CLS:FORI=0TO207:POKE#710+I,~PEEK(520+I):NEXT
> ```

画面を初期化し、アルファベット大文字のフォントを白黒反転させたものを用意する。

> ```
> R=0:LC0,-1:P=26:Q=1:N=0
> ```

入力の回数 `R` を初期化する。
リクエスト送信用に、画面への出力を無効化する。
160行目のルーチン用のパラメータを設定する。

> ```
> ?CHR$(10);"MJ GETS random-word-api.vercel.app/api?length=5"
> ```

MixJuice にリクエストを送信する。
これまでの(`LC` を表す特殊文字などの)出力の影響を避けるため、最初に改行を出力する。

> ```
> GSB160:IFN-5END
> ```

レスポンスを処理する。
得られた単語が5文字でなければ、エラーなので終了する。

## 30～70行目：入力の受付と描画

> ```
> 30 Y=3*R+1:LC6,Y:?CHR$(#E1):L=0
> ```

入力回数 `R` に基づき、描画行 `Y` を設定する。
入力位置を表す矢印を描画する。
入力文字数 `L` を初期化する。

> ```
> 40 GSB180
> ```

入力を読み込む。

> ```
> 50 IFC=8ANDL:L=L-1:LC9+3*L,Y:?" "
> ```

BackSpace が入力され、それまでに入力がある場合、1文字消す。

> ```
> 60 IFL<5AND64<CANDC<91:LC9+3*L,Y:?CHR$(C):[31+L]=C-65:L=L+1
> ```

アルファベットが入力され、それまでの入力が5文字未満の場合、入力された文字を保存する。

> ```
> 70 IFC-10ORL<5GOTO40
> ```

LF (Enterキー) が入力され、かつ5文字入力されている場合、次の処理に進む。
そうでない場合、入力処理を続行する。

## 80行目～90行目：入力された英単語の存在判定

> ```
> 80 LC0,-1:P=41:Q=3:N=0:
> ```

画面への出力を無効化し、160行目のサブルーチンのパラメータを設定する。

> ```
> ?CHR$(10);"MJ GETS api.datamuse.com/words?max=1&sp=";:FORI=0TO4:?CHR$([31+I]+97);:NEXT:?""
> ```

MixJuice にクエリを送信する。
URL の固定部分に続き、入力された英単語を送信する。

> ```
> GSB160:V=N=5:FORI=0TO4:V=VAND[41+I]=[31+I]:NEXT

入力を処理する。
存在判定の結果を変数 `V` に格納する。
APIから返ってきた英単語が5文字でない場合、入力の英単語は存在しないと判定する。
入力とレスポンスの英単語が一致しない場合も、入力の英単語は存在しないと判定する。

> ```
> IFVGOTO100ELSELC23,Y:?"?"
> ```

入力の英単語が存在すると判定した場合、正解との比較に進む。
そうでない場合、`?` を表示する。

> ```
> 90 GSB180:IFCLC23,Y:?" ":GOTO50ELSEGOTO90
> ```

何か入力があるまで、`?` を表示したままにする。
入力があったら、`?` を消し、入力の処理に進む。

## 100行目：入力と正解の比較

> ```
> 100 LC6,Y:?" ":FORI=0TO25:[I]=0:NEXT:H=5
> ```

入力位置を表す矢印の表示を消す。
各文字種のカウントを初期化する。
正解と一致しなかった文字の数 `H` を初期化する。

> ```
> FORI=0TO4:A=[26+I]:J=A=[31+I]:H=H-J:[36+I]=J*2:[A]=[A]+!J:NEXT
> ```

一致の判定を行う。

* `A=[26+I]`
  * 正解の文字を取り出し、`A` に格納する。
* `J=A=[31+I]`
  * 取り出した文字と対応する位置の入力の文字が一致するかを `J` に格納する。
* `H=H-J`
  * 入力と正解が一致した場合、不一致カウント `H` を1減らす。
* `[36+I]=J*2`
  * 判定結果 (2：一致 または 0：なし) を配列に格納する。
* `[A]=[A]+!J`
  * 文字を一致により消費しなかった場合、文字のカウントを1増やす。

> ```
> FORI=0TO4:A=[31+I]:J=[36+I]:IF!JAND[A][A]=[A]-1:J=1
> ```

場所違いの判定を行う。

* `A=[31+I]`
  * 入力の文字を取り出し、`A` に格納する。
* `J=[36+I]`
  * 判定結果を取り出し、`J` に格納する。
* `IF!JAND[A][A]=[A]-1:J=1`
  * 判定結果が「一致」ではなく、入力の文字と対応する正解の文字でまだ消費されていないものが残っている場合、正解の文字を消費して判定を「場所違い」にする。

## 110～150行目：一致状況の描画

> ```
> 110 X=8+3*I:LCX,Y-1:IFJ?CHR$(#88,#8C,#84)
> ```

何文字目を描画しているか `I` に応じて、描画列 `X` を設定する。
判定が「一致」または「場所違い」(`J` が非零) の場合、文字の上部を白く塗る。

> ```
> 120 LCX,Y:IFJ?CHR$(#8A,[31+I]+65+J/2*#A1,#85)
> ```

判定が「一致」または「場所違い」の場合、文字の左右を白く塗る。
判定が「一致」(`J=2`) の場合は白黒反転した文字を描画し、そうでない場合は通常の文字を描画する。

> ```
> 130 LCX,Y+1:IFJ?CHR$(#82,#83,#81)
> ```

判定が「一致」または「場所違い」の場合、文字の下を白く塗る。

> ```
> 140 NEXT:R=R+1:IFHIFR<6GOTO30ELSEFORI=0TO4:?CHR$([26+I]+65);:NEXT:?""
> 150 END
> ```

判定と描画の完了後、入力数 `R` を更新する。
`H` が零、すなわち5文字すべてが「一致」となった場合、クリアなのでゲームを終了する。
そうでなく、`R` が6未満の場合、まだ入力できる回数が残っているので、次の入力の受付に進む。
`H` が非零で `R` が6の場合、ゲームオーバーなので、正解の英単語を出力して終了する。