Недавно я играл в головоломку Wordle, параллельно думая, как бы её могла решать программа.

[Прим. пер.: Wordle — игра в отгадывание слов, напоминающая «быки и коровы». Правила достаточно ясны по скриншоту выше.]

Первым делом я извлёк списки слов с сайта Wordle. Любопытно, что существует «целевой» список из 2315 слов, которые могут быть ответами, но и дополнительный список из 10657 возможных догадок — вариантов, которые могут вводить пользователи, но которые никогда не будут ответом. Если вам нужны эти списки, то в репозитории ниже есть пара set в формате Python.

Первым делом я подумал, что для управления моей стратегией угадываний стоит использовать частотность букв английского языка. Однако потом я осознал, что есть способ получше: использовать частотность букв в целевом списке! Ведь это самое важное? Никаких мне etaoin shrdlu!

Также у меня возникла мысль, что я могу замерить частотность букв в конкретных позициях. Допустим, для первой позиции пятибуквенного слова частотность согласных будет больше, чем гласных? Или, возможно, я ошибаюсь.

Затем я подумал: почему бы не измерять эту частотность для возможных целевых слов на основании предыдущих догадок, отсекая ненужные варианты после каждой новой догадки?

Всё это я превратил в программу, выполняющую следующие шаги:

  1. Вычисляем частотность букв в позициях 1-5 для целевого списка слов.
  2. Выбираем слово, которое с наибольшей вероятностью даст новые зелёные буквы на основании частотности слов в каждой позиции.
  3. Используем результаты новой догадки, чтобы оставить в целевом списке только те слова, которые возможны с учётом всех предыдущих догадок.
  4. Повторяем.

По ходу дела у меня также возникла мысль, что в качестве списка потенциальных догадок можно использовать расширенный список, в котором есть все целевые слова и возможные догадки. Это увеличит возможность получения зелёной буквы. Однако, как вы можете увидеть в коде, я провёл этот эксперимент, но ответ при таком решении на самом деле оказался хуже! Потом я подумал, что есть смысл использовать расширенный список для первых одной-двух догадок, а затем пользоваться только целевыми словами. Это решение тоже было хуже, чем просто использование целевых слов.

Кроме того, сначала я не реализовал логику, гласящую, что когда жёлтая буква находится на той же позиции в слове, то это слово не является ответом. Добавление одной только этой логики позволило снизить среднее количество догадок с 4,5 до 3,6.

Программа может и проигрывать, в 14 случаях из 2315! Эти слова и догадки любопытны:

wound: [slate: 00000, crony: 00120, hound: 02222, bound: 02222, pound: 02222, mound: 02222]
shave: [slate: 20202, share: 22202, shame: 22202, shape: 22202, shake: 22202, shade: 22202]
vaunt: [slate: 00110, taunt: 02222, jaunt: 02222, haunt: 02222, daunt: 02222, gaunt: 02222]
found: [slate: 00000, crony: 00120, hound: 02222, bound: 02222, pound: 02222, mound: 02222]
boxer: [slate: 00001, fever: 00022, rider: 00022, cower: 02022, poker: 02022, homer: 02022]
ratty: [slate: 00120, fatty: 02222, catty: 02222, batty: 02222, patty: 02222, tatty: 02222]
catch: [slate: 00110, taunt: 12000, patch: 02222, match: 02222, hatch: 02222, batch: 02222]
stamp: [slate: 20210, start: 22200, staid: 22200, stank: 22200, staff: 22200, stash: 22200]
baste: [slate: 10122, paste: 02222, caste: 02222, haste: 02222, taste: 02222, waste: 02222]
watch: [slate: 00110, taunt: 12000, patch: 02222, match: 02222, hatch: 02222, batch: 02222]
goner: [slate: 00001, fever: 00022, rider: 00022, cower: 02022, poker: 02022, homer: 02022]
fight: [slate: 00010, tight: 02222, might: 02222, wight: 02222, right: 02222, night: 02222]
willy: [slate: 01000, golly: 00222, billy: 02222, hilly: 02222, dilly: 02222, filly: 02222]

Иногда программа выполняется в другом порядке, вероятно, это как-то связано с работой множеств в Python? Простите меня за код, я программист-самоучка с дипломом философа.

Код


https://github.com/tomlockwood/wordle-solve

(Множества слов находятся в ./lib/words.py)

Что дальше


Часть кода в репозитории поддерживает различные типы игр, и я продолжил работу, написав несколько альтернативных стратегий игры. Самая многообещающая выполнялась всю ночь на моём слабом ноутбуке, а потом у неё закончилась память. Чтобы решить эту проблему, я начал кэшировать на диск часть результатов, и эта база данных (sqlite) разрослась до размера 50 ГБ.

Я решил исследовать возможность реализации программы на Rust, и пока то, что занимало на Python 1 ГБ ОЗУ на Rust в буквальном смысле занимает 1 МБ! Я пытаюсь уместить программу в 8 бит, но не занимался ничем подобным раньше. Возможно, появится ещё один пост! Думаю, вполне возможно выигрывать каждую игру в среднем меньше чем за 3,5 догадки!

Мой текущий солвер предназначен для сложного режима игры и я считаю, что солвер для лёгкого режима проявит себя лучше. Похоже, наблюдение за решениями программы немного улучшили мои навыки игры в Wordle!

Комментарии (9)


  1. vesper-bot
    24.01.2022 12:05
    +4

    Если посмотреть внимательно на «проигрыши», становится очевидно, что в базе есть несколько слов, отличающихся на одну букву в целевой позиции, скажем, «wound»-«hound»-«bound»-«pound» etc, что нормально для английского языка, у них там такого вагон. По мне, правильно будет ввести второй вариант «угадайки» такого вида: при обнаружении ответа с 1 нулем и 4 двойками собрать из базы все возможные ответы и не перебирать, а запулить «слово» из различных букв этих слов, т.е. в примере с wound послать «hmbpw», получение ответа в 5 нулей выкинет из множества 5 слов, получение единицы или двойки оставит одно. Повторять до победного.
    Формально этот же вариант можно использовать для состояния 2 нуля 3 двойки («goner/boxer», «stamp/stash»), удачный подбор может исключить *до* 5 слов, но придется проверять отдельно на наличие нескольких единиц/двоек, скажем паттерн 02022 для goner запулить pmhbn (желательно пихать в паттерн те буквы, которые в оставшемся множестве слов занимают обе позиции в разных словах, причем их предпочтительнее пихать именно на них, так больше инфы получится из 1 или 2 в этих местах, в случае нехватки одной буквы в этом нет проблем) или что-то подобное. А основной алгоритм подобрал красивое начальное слово для 6 букв :)
    Апд: потрогал wordle — а она не дает (по крайней мере через веб) посылать «не-слова» — обидно! Придется извращаться, и отправлять вместо hmbpw скажем thumb (3 из 5 букв выбиваются), если вообще даст отправить подобное, мол нет уже t в искомом слове, не пытайтесь.


    1. kryvichh
      24.01.2022 15:10

      Даст отправить слово с повторной буквой. Но да, произвольные символы не вобьёшь.


  1. StjarnornasFred
    24.01.2022 12:54
    +1

    Для "ручной" победы лучшая тактика - первые 3 слова ввести так, чтобы они не имели общих букв. Тем самым максимизируется знание о том, что есть и чего нет в остальных словах.


    1. savostin
      24.01.2022 19:19

      Даже можно найти эти 3 слова заранее ;)


  1. gwisp
    24.01.2022 14:11
    +1

    А что если попробовать построить решающее дерево [используемое в машинном обучении]?

    В качестве вопроса выбрать слово, а в качестве эффективности разделения что-нибудь стандартное, например, gini index или information gain.


  1. sophist
    24.01.2022 15:32

    Я написал свой алгоритм: имея список из допустимых (=удовлетворяющих всем ответам системы) слов, считаем для каждого слова энтропию (средневзвешенное количество информации, которую можно получить, задав это слово), выбираем слово с наибольшей энтропией. После ответа системы пересчитываем множество допустимых слов.


    1. vesper-bot
      24.01.2022 17:38

      Сразу вопрос: уперлись вы в *ound, или в *atty (ещё хуже), осталось 4-7 слов, на 2-4 попытки. Как действовать будете?


      1. sophist
        24.01.2022 21:50

        Сначала хотел ответить, что алгоритм не попадёт в такую ситуацию :).

        Но вы правы, я допустил оплошность: выбирать надо из ВСЕХ слов словаря, (а вот энтропию для них считать только по допустимым). (Что характерно, на стадии придумывания алгоритма я так и планировал, а реализовал почему-то с ошибкой).


    1. BurhanUlzen
      27.01.2022 09:39

      Тоже сделал через энтропию, прогнал его по всем словам, выходит разгадывает за 3,47 попытки в среднем, так что похоже автор был прав, рубеж в 3,5 попыток преодолим.