Loading [MathJax]/extensions/tex2jax.js

2022年2月19日土曜日

Wordleを攻略する

最近巷で流行っているゲームです。

Wordle - The New York Times 

入力された単語から「その文字は含まれない」「その文字は含まれるが別の場所に入る」「その文字はその場所に含まれる」の3通りの判定結果が出るので、それをもとにお題の英単語を当てるというゲームです。

やってみると意外と難しくて、2, 3個の単語を入れた後にすべての条件を満たす単語をいろいろと考えてもなかなか思いつきません。辞書を使っても絶妙に単語が見つけられないくらいのゲームバランスは何といっても絶妙です。

また、日替わりで全員が同じお題でやるのですが、3色の文字色を使ったSNS共有も印象的です。私もTwitterでWordleのツイートが流れてきたのを見て知ったくちです。

候補の単語を知る

さて、攻略をしようとすると 真っ先に必要なのは候補の単語です。実は単語リストは 公開されています。

Wordle 単語リスト

12,972語あります。この中から条件を満たす単語を抽出することができればゲームをスムーズに進められるようになります。チートですが。

NominateCandidates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static IReadOnlySet<string> NominateCandidates(IEnumerable<IWordHint> wordhints)
{
    var original = WordList.All;
 
    // 正しい場所の文字を抽出
    var exactpos = wordhints
        .SelectMany(p => p.Characters.Select((p, i) => (character: p, pos: i)).Where(p => p.character.Hint == HintState.ExactPosition))
        .Select(p => (character: char.ToLower(p.character.Character), pos: p.pos))
        .Distinct()
        .ToArray();
 
    if(exactpos.Length > 0) {
        var filtered = new SortedSet<string>();
        foreach(var w in original) {
            if(exactpos.All(ep => w[ep.pos] == ep.character))
                filtered.Add(w);
        }
        original = filtered;
    }
 
    // 含まれていない文字を除外
    var notcontained = wordhints
        .SelectMany(p => p.Characters)
        .Where(p => p.Hint == HintState.NotContained)
        .Select(p => char.ToLower(p.Character))
        .Distinct()
        .ToArray();
 
    if(notcontained.Length > 0) {
        var filtered = new SortedSet<string>();
        foreach(var w in original) {
            if(notcontained.All(nc => !w.Contains(nc)))
                filtered.Add(w);
        }
        original = filtered;
    }
 
    // 異なる場所の文字を抽出
    var notinpos = wordhints
        .SelectMany(p => p.Characters.Select((p, i) => (character: p, pos: i)).Where(p => p.character.Hint == HintState.NotInPosition))
        .Select(p => (character: char.ToLower(p.character.Character), pos: p.pos))
        .Distinct()
        .ToArray();
 
    if(notinpos.Length > 0) {
        var filtered = new SortedSet<string>();
        foreach(var w in original) {
            if(notinpos.All(nip => w[nip.pos] != nip.character && w.Contains(nip.character)))
                filtered.Add(w);
        }
        original = filtered;
    }
 
    return original;
}

Wordleに単語を入力すると、それぞれの文字が3種類の判定結果になるので、3パターンの検索をします。

正しい場所の文字を抽出→含まれていない文字を除外→異なる場所の文字を抽出の順で処理をしているのは、数がグッと減る検索を先にやったほうが効率が良いと考えたからです。本当にこの順で数がグッと減るかどうかはわかりませんが。

ちなみに、上のコードに出てくるインターフェースや列挙体などは以下の通りです。

Interfaces
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IWordHint
{
    IReadOnlyList<ICharacterHint> Characters { get; }
}
 
public interface ICharacterHint
{
    char Character { get; }
    HintState Hint { get; }
}
 
public enum HintState
{
    NotContained,
    NotInPosition,
    ExactPosition,
}

あとはIWordHintを入力するUIを実装してやればいいだけです。

ここに置いておきます。

WordleAssistant.zip

次に何の単語を入れたらいいか考える

さて、じゃあ肝心の「次に入力する単語」はどうやって選べば良いでしょうか。

これを体系化して導くのはおそらく一筋縄にいきません。また機会があったら(めどが立ったら)記事にしてみようかと思います。