stefafafan の fa は3つです

"すてにゃん" こと id:stefafafan のブログです

Wordleの候補を教えてくれるスクリプト書いた

先日はこういう記事を投稿していました。
blog.stenyan.jp

ここで導き出した初手の候補をTwitterとかで検索したら他にも同じ単語に辿りついている人々がいて面白かったです。一方でTwitterもう少し検索していたらこういう記事も見つけました。

qiita.com

いいなと思ったので自分も似たものを作ることにしました。

作戦

どういう風に候補を絞り込んでいくかコード書く前に一瞬考えたところ、これがいいかなと思いました。

  1. 単語リストをファイルから読み込んで配列に入れる
  2. 単語リストの中のアルファベットの登場回数を、アルファベットごとのスコアとする
  3. スコアの高い単語を候補として提示する
  4. ユーザが選んだ単語と解答結果(どの文字が当たっていてどの文字が外れている等)を受け取り、その条件に合わない単語を弾いて、単語リストを更新
  5. 2-4 を繰り返す

以前の記事ではあらかじめ全体の単語リストから最善の初手と2つめ、3つめの手を導き出して、「最初に入れる三つの単語はこれ!」と決めていましたが、今回はユーザからの入力を元に2つめ以降の手も都度探索して良い候補を出すことにしました。

あと前回はGolangで書いたのでそれを一部流用してそのまま拡張していくことにしました。

注意: ここからしたは一部ネタバレがあります。

結果

ここに公開しています(単語リストはリポジトリに入れていません)。
github.com

単語を入れるたびに、「答えの候補」と「絞り込むのにおすすめの単語集」を出力してくれます。

実際に試してみましょう。例えば、 proxy が答えとします。

$ go run main.go --dictionary five-letter-words.txt
-----Try number 1-----
There are 14849 candidates. Here are the first 10 candidates:
[aahed aalii aargh aaron abaca abaci aback abada abaff abaft]

Here are the first 10 recommendations to help narrow down the word:
[{arose 30949} {seora 30949} {aries 30623} {aesir 30623} {raise 30623} {arise 30623} {serai 30623} {osela 30089} {alose 30089} {solea 30089}]

Enter your guess:

実行するとまずこのように、単語の候補の数と最初の10件の候補、それとレコメンドも10件点数つきで表示されます。私が使ってる辞書では初手は大体 arose になるようです。なので arose と入れます。

Enter your guess: arose
Enter the result (b: blank, y: yellow, g: green):

と聞かれるので、空欄の場合は b、黄色の場合はy、緑の場合はgと続けて5文字分入れます。proxy にたいして arosebggbb になります。

上記を繰り返したらこういう結果になります。

-----Try number 1-----
There are 14849 candidates. Here are the first 10 candidates:
[aahed aalii aargh aaron abaca abaci aback abada abaff abaft]

Here are the first 10 recommendations to help narrow down the word:
[{arose 30949} {seora 30949} {aries 30623} {aesir 30623} {raise 30623} {arise 30623} {serai 30623} {osela 30089} {alose 30089} {solea 30089}]

Enter your guess: arose
Enter the result (b: blank, y: yellow, g: green): bggbb
Guess: arose, Result: bggbb

-----Try number 2-----
There are 104 candidates. Here are the first 10 candidates:
[broch brock brogh broid broil broll bromo bronc bronk bronx]

Here are the first 10 recommendations to help narrow down the word:
[{tronc 324} {contr 324} {gront 317} {croft 316} {dropt 316} {front 314} {trock 312} {crout 311} {turco 311} {court 311}]

Enter your guess: tronc
Enter the result (b: blank, y: yellow, g: green): bggbb
Guess: tronc, Result: bggbb

-----Try number 3-----
There are 43 candidates. Here are the first 10 candidates:
[brogh broid broil broll bromo brood brook brool broom browd]

Here are the first 10 recommendations to help narrow down the word:
[{world 141} {browd 140} {lourd 140} {droyl 140} {lordy 140} {gourd 139} {proud 139} {bourd 139} {droil 138} {borid 137}]

Enter your guess: world
Enter the result (b: blank, y: yellow, g: green): byybb
Guess: world, Result: byybb

-----Try number 4-----
There are 19 candidates. Here are the first 10 candidates:
[brogh bromo brook broom froom frory groff groof groom groop]

Here are the first 10 recommendations to help narrow down the word:
[{porgy 69} {group 68} {grouf 68} {formy 67} {rompy 67} {pirog 67} {rompu 66} {forge 66} {forgo 66} {prong 66}]

Enter your guess: porgy
Enter the result (b: blank, y: yellow, g: green): gyybg
Guess: porgy, Result: gyybg

-----Try number 5-----
There is 1 candidate. This is probably the answer: proxy

Enter your guess: proxy
Enter the result (b: blank, y: yellow, g: green): ggggg
Guess: proxy, Result: ggggg

GJ!
Game over.

5/6 で正解しました。意外とパソコンの力をそのまま借りても爆速で解答できるわけでもないようですね……。

実装のポイント

サクッと書けたと思ったら上手くいかないケースがいくつかありました。

毎回単語リストを更新した上で、その単語リストを元にレコメンドのリストまで絞り込むのはもったいなさそう

1.4万単語くらいある中で、初手絞り込むと arose が上位にあがる。

arose を入れたとして、正解の候補は確かにここから絞り込むと良いが、解答の候補(レコメンドする解答)については絞り込まない方が良い。ということを実装の途中で気づきました。

5文字の単語のうち最後の1文字が緑だったのでその文字をキープしたまま残りの4文字を考え続けるのではなく、次の解答時は前回使わなかった文字をフルに使って解答したほうがヒントが増えて良いということですね。

過去の黄色や緑の結果を常に保持しておくべき

最初の実装では絞り込む関数の内部で毎回単語リストから違うものを排除し続けるというのをやっていました。空欄の文字を含む単語についてはこれで事足りてました。

ですが、「次の解答時は前回使わなかった文字をフルに使って解答したほうがヒントが増えて良い」みたいな戦略を取る場合、「最初はAが緑だった、次の解答ではOが黄色だった、なので正解はAとOは絶対含む」と考えるはずなのでこの変数もグローバルな感じで持つべきでした。

同じ文字を繰り返すことを考慮すべき

黄色や緑の情報を持つのに、最初の実装では map[string]int のような、1つの文字列に対して1つのindexを持つという構造で保持させていました。

つまり arose にたいして bbybb が結果の場合、 アルファベットのoが黄色なので、 o:2 みたいなのを保持すれば良いという感じで作っていました。(これをどう使うのかというと、絞り込むときに 2 のindexに o がある単語は不正解、逆に 0, 1, 3, 4 の位置に o がある単語は正解候補、という感じです)。

がよく考えたら何回も同じ文字だけど別の位置で挑戦するかもしれないし、正解の単語も bloom みたいに一部の文字を繰り返している場合がある。最終的に map[string][]int を持つ形に変更しました。

感想

最終的なコードは素朴なforとif文を繰り返していて意外と過酷な感じになってしまいました。最初にデータ構造とかをもう少し考えておくべきでしたね。

それはそうとして、コンピュータにこういうことやらせるのはコード書いている間は面白いですが、今後のWordleは作業みが出てきそうなのでそんなにはおすすめしません。

こちらからは以上です。