Wordleの最初の一手を考える

# Wordleとは

最近、Wordleというゲームにハマっています。
https://www.powerlanguage.co.uk/wordle/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/e325e6c9-522b-5e11-aede-a96a9142e7b1.png)

５文字の英単語を当てるゲームです。ルールをざっと説明すると以下の通り。

 - 入力できる文字列は英単語だけ（無意味な文字列は入力できない）
 - 文字列を入力できるチャンスは６回のみ
 - 文字列入力後、各文字について、
    - 外れた文字は**灰色**でハイライトされる
    - 位置は異なるが含まれている文字は**黄色**でハイライトされる
    - 当たった文字は**緑色**でハイライトされる
 - すべての文字を緑色にすれば勝ち

ついでに、以下のような仕様がSNSでバズっている理由な気がします。

 - 一日ごとに問題が変わる
 - 同じ端末で解けるのは一日に一回
 - Twitterに結果を簡単にコピペできる（文字列を伏せたまま）

正解がまったく聞いたこともない単語であることもままあり、かなり英語の語彙力を試されます。
楽しく英語を勉強したい方にはうってつけのゲームです。

# 初手で情報量を増やしたい
さて、Wordleに挑むときに悩ましい問題のひとつが、**最初に何を入力するか**です。
一手目はノーヒントなわけですから、いきなり正解するのはまず無理でしょう。
（SNSで一手目で正解した人を見かけたとしても、99％別の端末で先に解いて答えを知ってる人です）

その代わり、一手目ではより多くのヒント、より多くの**情報量**が得られるような文字列を入力したくなります。
この考えを推し進めて、もう少し**理論的に定式化**してみましょう。
情報についての理論ということで、**情報理論**の考え方を用います。

この考え方を説明するために、いったんWordleから離れて、もっと簡単な問題を考えてみましょう。
ここで例に出すのは、**「人当てゲーム」**です。
出題者が思い浮かべている特定の人物を、「YES/NO」で答えられる質問だけで当てていくゲームです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/0e503047-f6a1-fa5e-7329-fd12b1306ec8.png)
（似たようなゲームで有名なものだと、ランプの魔人、[Akinator](https://jp.akinator.com/game)がありますね。
こちらの場合、プレイヤーは質問される側ですが。）

まず、「人当てゲーム」における最初の一手を考えてみましょう。

例えば、「あなたが思い浮かべている人物は**[草刈正雄](https://ja.m.wikipedia.org/wiki/%E8%8D%89%E5%88%88%E6%AD%A3%E9%9B%84)**ですか？」のように、いきなり個人名を出してしまうことが悪手だというのは、なんとなくわかることかと思います。

むしろ「あなたが思い浮かべている人物は**男性**ですか？」のように、最初の質問ではそこそこ多くの人に当てはまる属性を聞いていくのが**スジ**だという気がします。

逆に言うと、「**草刈正雄**ですか？」がよくないのは、「**草刈正雄ではない**」というのが**ほとんどの人間に当てはまる**から、**NOの答えが返ってきて当然**だから、ということです。

このように「ある属性（質問の答え）が集団の中で偏っていること」を、情報理論では「エントロピーが低い＝平均情報量が少ない」と言います。

わかりやすいように、図にまとめておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/4cfb3047-dab8-ef5c-d44a-5c75b44a02fe.png)

さて、同じ考えをWordleに応用してみましょう。
単語当てゲームであるところのWordleは、実は、人当てゲームとルールがよく似ています。
違いは、**一度に交わされる質問と答えの数が多い**という点だけです。

Wordleにおける１つの質問は、１文字の入力に相当します。
つまり、Wordleでは、**５つの質問を同時にしている**ことになります。
そして、それぞれの質問に対し、YES/NOではなく、**灰/黄/緑の３通りの答え**が返って来るわけです。

人当てゲームでは、一回の質問によって、候補がYES/NOの2つの集団に分かれました。
Wordleでは、一回の質問によって、候補が**3^5=243の集団**に分かれることになります。

しかし、数が増えただけで、基本的な戦略は人当てゲームと同じように考えることができるでしょう。
良い文字列とは、**3^5=243の集団において答えの偏りが少ない＝エントロピーが高い＝情報量が多い**文字列なのです。

再度、わかりやすいように、図にまとめておきます。
（5階のテンソルを視覚的に表示するのが大変なので、2文字目までの情報、つまり3^2=9の集団だけ以下には記しています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/0bde786c-5870-88d1-e175-6649adbfcf07.png)

以上をまとめると、**「Wordleの初手の最善手」**とは、**「最もエントロピーが高い文字列を出す」**ということになります。
（これはあくまでもナイーブな情報理論に基づいた考え方です。他の戦略もありえるでしょう）

最後に一応、情報理論的エントロピー＝平均情報量を数式でちゃんと表しておきましょう。

分割数を$n$、$i$番目の分割された集団のサイズを$N_i$としたとき、エントロピー$H$は次のように定義されます。

```math

H=\sum_{i=1}^n P_i (-\log P_i)  \\

\text{where } P_i=\frac{N_i}{\sum_{i=1}^n N_i}

```

もちろん、ここで$P_i$は$i$番目の集団が選択される確率を意味します。
そして、$(-\log P_i)$がその事象の情報量（自己情報量、選択情報量）と呼ばれるものです。
情報量は$0\le P_i\le 1$に対して対数をとってからマイナスを掛けているので、必ず非負の値を取ります。
この情報量の集団平均を取ったものがエントロピー$H$ですから、**平均情報量**と呼ばれるのも納得です。

# 実装しよう、そうしよう

必要な理論は揃ったので、**最もエントロピーが高い文字列を探すコード**をPythonで実装しましょう。
Jupyter-labでの実行結果も適宜載せていきます。

## フリー英和辞書のダウンロード

まずは、フリーの英和辞書をダウンロードします。
今回の目的上、日本語の部分はなくてもいいのですが、英単語の意味もわかると勉強になって楽しいです。

https://kujirahand.com/web-tools/EJDictFreeDL.php

## 必要なライブラリ

データ解析に使う基本的なライブラリに加えて、scipyからエントロピーを計算する関数entropyをインポートします。
（この関数、合計が１になるように規格化されてないリストを与えても、ちゃんとエントロピーを計算してくれます）

あとは、並列計算用のjoblibも入れておきます。

```python
import pandas as pd
import re
import numpy as np
from scipy.stats import entropy
from joblib import Parallel,delayed
```

## Wordle用5文字辞書の作成

普通の辞書は5文字以外の単語とか、記号も含めた単語とか、見出しに大文字小文字が混ざった状態だったりするので、これらをWordle用に整えていきます。

```python
eng_dict = pd.read_table("ejdict-hand-utf8.txt",header=None).rename(columns={0:"key",1:"value"})
eng_dict = eng_dict[[len(str(key))==5 for key in eng_dict["key"]]]
eng_dict = eng_dict[[True if re.match(r'[a-zA-Z]{5}', str(key)) else False for key in eng_dict["key"]]]
eng_dict["key"] = eng_dict["key"].str.upper()
eng_dict.drop_duplicates(subset="key",inplace=True)
eng_dict.set_index("key",inplace=True)
eng_dict
```

できあがった中身はこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/87a4721c-c4bb-2119-0b87-a4a53a9a0f67.png)

## 一文字ずつに分割

以降の解析をしやすいように、文字列を一文字ずつに分割して異なる列に収めたデータフレームを用意しておきます。

```python
df_char = pd.DataFrame([list(key) for key in eng_dict.index],columns=range(1,6))
df_char.index=eng_dict.index
df_char
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/c00d6755-683c-2461-cadf-8fe2596abe1d.png)

## エントロピー計算

ここでようやく肝心のエントロピー計算です。エントロピー計算そのものは簡単なのですが、与えられた文字列に対して結果を3^5通りに分類するコードの部分が結構長くなってしまったので、表示を省略しておきます。
（もっと短く書く方法があったら教えて欲しい）

<details><summary>**エントロピー計算のコード （やや長いですが見たい方はここをクリック）**</summary><div>

```python
def calc_cluster_entropy(df_char, input_word:str):
    cluster_size = np.zeros(np.repeat(3,5))
    Green = pd.DataFrame()
    Yellow = pd.DataFrame()
    Gray = pd.DataFrame()

    for pos, char in enumerate(list(input_word)):
        Green[pos] = (df_char[pos+1] == char)
        Yellow[pos] = (~Green[pos])&(df_char.index.str.contains(char))
        Gray[pos] = (~Yellow[pos]) & (~Green[pos])

    for i in range(3):
        if i==0:
            TF_i = Gray[0]
        elif i==1:
            TF_i = Yellow[0]
        else:
            TF_i = Green[0]
            
        for j in range(3):
            if j==0:
                TF_j = Gray[1]
            elif j==1:
                TF_j = Yellow[1]
            else:
                TF_j = Green[1]
                
            for k in range(3):
                if k==0:
                    TF_k = Gray[2]
                elif k==1:
                    TF_k = Yellow[2]
                else:
                    TF_k = Green[2]
                    
                for l in range(3):
                    if l==0:
                        TF_l = Gray[3]
                    elif l==1:
                        TF_l = Yellow[3]
                    else:
                        TF_l = Green[3]
                        
                    for m in range(3):
                        if m==0:
                            TF_m = Gray[4]
                        elif m==1:
                            TF_m = Yellow[4]
                        else:
                            TF_m = Green[4]
                            
                        cluster_size[i,j,k,l,m] = (TF_i&TF_j&TF_k&TF_l&TF_m).sum()

    cluster_size = np.int64(cluster_size.ravel())
    return entropy(cluster_size, base=2)

```

</div></details>

上記で作成した、「与えられた単語が作る分割集団のエントロピーを計算する関数」 **calc_cluster_entropy** を使って、「与えられた単語のリストをエントロピー順にソートして、日本語の意味を表示する関数」 **order_by_entropy** も作っておきます。
時間がかかるので、joblibで並列化しておきます。

```python
def order_by_entropy(df_temp):
    df_entropy = pd.DataFrame(Parallel(n_jobs=-1)(delayed(calc_cluster_entropy)(df_temp, index) for index in df_temp.index),
                              index=df_temp.index, columns=["entropy"])
    df_entropy.sort_values("entropy",ascending=False,inplace=True)
    df_entropy["meaning"]=eng_dict.loc[df_entropy.index,"value"]
    return df_entropy

order_by_entropy(df_char)
```

**order_by_entropy** に、先ほど作成した１文字ずつの辞書を与えた結果がこちらになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/9677ae4c-6e0a-af4a-b0ed-6f2aef9a93d8.png)

# Wordleの初手の最善手

というわけで、Wordleの初手の最善手は**"RAISE"**であることがわかりました。
誰でも知っている英単語ですね。忘れる心配はなさそうです。
私は今後、Wordleの初手は**RAISE**で決め打ちすることにします！

ところで、推理小説の愛好家なら、**「英文では"E"が一番多く使われている」**というネタをよくご存知のことかと思います。
実は、今回作成した５文字辞書でも同じことが言えます。アルファベットを出現頻度順に並べるコードは以下の通り。

```python
char2freq = pd.Series(df_char.values.ravel()).value_counts().to_dict()
char2freq
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/44846bc5-6877-86e1-d1fb-9480ecb25b2f.png)

上位６文字の"O"以外の文字から構成された単語が最善手というのは、**できるだけ黄色のヒットを当てる**という戦略に立つとしても、もっともな結果に思えます。２位以降の**ARISE**や**SLATE**も、似たようなものですね。

一方、最悪手は**"UHHUH"**という聞いたこともない単語ですが、残念なことにこちらはWordleの辞書には登録されていないようで、答えとして認められません。

ということで、Wordle内で最悪手の単語は**FUZZY**ということになります。
**「はっきりしない」**というその単語の意味の通り、これを入力してもヒントが与えられる可能性は少なく、**答えがはっきりしないまま**になるわけです。

（オチがつきましたね。よかったよかった。）

……その上にさらに、日本語由来のKUDZU（葛）があるのも、ちょっと日本人としては趣きを感じます。
なぜこんな単語が辞書に載っているのだろうと思ったのですが、どうやら葛は米国では侵略的外来種として増え過ぎてしまい、深刻な環境問題になっているらしいです。ウケる。

https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%A1%E3%83%AA%E3%82%AB%E5%90%88%E8%A1%86%E5%9B%BD%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%82%AF%E3%82%BA

UHHUHにしろFUZZYにしろKUDZUにしろ、**出現頻度の低い文字から構成されている**ことに加えて、**同じ文字が重なっている**と悪手になる、というのは、情報理論など知らなくてもなんとなくわかりそうです。

# ２手目以降は？

さて、以上では「初手の最善手」を考えるために情報理論を導入したわけですが、実際のところ、この**エントロピー戦略**は別に初手に限らず、２手目以降も有効だと考えられます。

order_by_entropyに与える単語リストを全単語にするのではなく、ヒントに合致する単語だけに絞っていけばよいだけです。
以下のようなコードで、与えられたヒントから単語のリストを絞ることができます（使い方は後述）。

```python
def make_df(df_char, fixed=dict(), contains=dict(), not_contains=""):
    intersection = set(df_char.index)
    for key in fixed.keys():
        intersection = intersection & set(df_char.index[df_char[key]==fixed[key]])
    for key in contains.keys():
        intersection = intersection & set(df_char.index[df_char[key]!=contains[key]])
        TF_contains = [False]*df_char.shape[0]
        for position in {1,2,3,4,5} - set(fixed.keys()) - {key}:
            TF_contains = TF_contains|(df_char[position]==contains[key])
        intersection = intersection & set(df_char.index[TF_contains])
    for char in list(not_contains):
        TF_not_contains = [True]*df_char.shape[0]
        for position in {1,2,3,4,5} - set(fixed.keys()):
            TF_not_contains = TF_not_contains&(df_char[position]!=char)
        intersection = intersection & set(df_char.index[TF_not_contains])
    return df_char.loc[intersection]
```

……ただ、新たなヒントが得られるたびに、エントロピーを計算しなおす必要があり、**それは流石にチート**という気がします。
ですが折角なので、**最後までエントロピー戦略を取ること**がどれくらい有効なのかを試してみたくなりました。

## エントロピー戦略 vs Absurdle ～南海の大決闘～

本家のWordleは一日一回しか解けないので、代わりに激ムズ版Wordleである**Absurdle**でエントロピー戦略を試してみます。

https://qntm.org/files/wordle/

Absurdleは、**答えの単語が予め決まっておらず、できるだけプレイヤーにヒントを与えないように、毎回答えの単語をこっそり変化させていきます**。ずるいですね。
代わりに、本家の６回という制限は取り払われて、無限回文字列を入力できるようになっています。
もはやWordleとは別ゲームなのですが、その難しさはそれはそれで味わい深いので、一度お試しあれ。
実際に自分でやってみると**永遠に終わらなくてイライラします。**

さて、これまで作った関数を以下のように使えば、ヒントが与えられた状態でのエントロピー戦略を実行できます。

```python
order_by_entropy(
    make_df(df_char, fixed={2:"U",5:"Y"}, contains={}, not_contains="RAISECLOTHDMP")
)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/66859aa5-9ac9-bc30-7d79-5c448fcf9545.png)

そして、徹頭徹尾エントロピー戦略にAbsurdleに挑んだ結果がこちら。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/694a2b91-e2ad-fe9b-878a-c97a65bab004.png)

本家の制限である６手以内の、５手でどうにかクリアすることが出来ました！
**親の仇を取った**ような気分で感無量です。

……ところが、Absurdleのヘルプには**最短４手で終わる**とありました。なんと、１手届きません！

まあ、エントロピー戦略は**答えの単語がランダムに選ばれる**ことを暗に想定しているのに対し、Absurdleの中身はまったくランダムではありませんので、致し方ありません。
Absurdleを攻略するためには、「Absurdleが動く方向を予め想定して、罠に誘い込む」ような、より高度な戦略が必要なのかもしれません。
もしどなたか攻略法をご存じの方がいらっしゃったら、教えて欲しいところです。

以上。

# 追記1

[Absrudleを4手で攻略した人のTweet](https://twitter.com/zwegner/status/1480110927752175618)を見つけました。

# 追記2

[Gigazineの記事](https://gigazine.net/news/20220129-the-best-strategies-for-wordle/)を読んでWordle公式が使ってる正解単語リストの存在を知ったので、再度エントロピー計算をしてみましたが、上位も下位もあまり変わらなかったです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240908/d461921d-a112-a7d0-f3ec-dc9d4ea40869.png)

最下位のMAMMAように一部の単語は手持ちの辞書に意味が乗っていませんでした。もちろんこれは文字通りマンマ、お母さんという意味ですが。

なお、上記の記事によると、最短で解く戦略では**SALET**が最適とのこと。
エントロピー戦略２位の**SLATE**のアナグラムになっていますが、戦略が違うことで微妙に最適な単語が変わるのは面白いですね。

# 追記3
ヒントが得られた後の手について、もしかしたら助けになるかもしれない記事を書きました。
[Wordleのチートシートを作ったよ](/items/2c45817a254658a63a61)