Python で Wordle を実装する

## Wordleとは

https://www.powerlanguage.co.uk/wordle/


## モチベーション

何かコードを書きたかったので、最近流行りのWordleを実装した。

## 成果物

実装時間: 1〜2時間
単語リストは[こちら](https://github.com/splch/slc.is/blob/main/posts/data/wordles.txt)より拝借
（日本語の単語リストを作れば日本語対応も可能）

![スクリーンショット 2022-01-28 18.31.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/ec4c1237-9507-fb8e-3e13-e46efbd52027.png)

## 感想

プログラミングさわりたての人にちょうどいいくらいの実装難易度と感じた。

入力の判定部分（`Wordle.evalueate`）が地味に面倒で、もう少しいい書き方がありそう。

ソルバー（`Solver`, `StupidSolver`）を簡単に書いて満足したので、続きを書くことがあればいい感じのソルバーを書きたい。


## コード全文

- Python 3.8.2

```python
from __future__ import annotations

import os
from logging import getLogger, basicConfig, INFO
import random
from collections import Counter

basicConfig(level=INFO)
logger = getLogger(__name__)


class Color:
    BLACK = '\033[30m'
    bgGREEN = '\033[42m'
    bgYELLOW = '\033[43m'
    END = '\033[0m'

    @classmethod
    def green(cls, word: str) -> str:
        return cls.bgGREEN + cls.BLACK + word + cls.END

    @classmethod
    def yellow(cls, word: str) -> str:
        return cls.bgYELLOW + cls.BLACK + word + cls.END


class Solver:
    def choice(self) -> str:
        return input('>> ').lower()

    def update(self, guess: str, score: list[int]) -> None:
        pass


class StupidSolver(Solver):
    def __init__(self) -> None:
        self.guess_list = Wordle.load_dict()

    def update(self, guess: str, score: list[int]):
        # TODO: fix bug
        inclusion = [g for g, s in zip(guess, score) if s == 2]
        exclusion = [g for g, s in zip(
            guess, score) if s == 0 and g not in inclusion]

        self.guess_list = list(filter(lambda x: not any(
            set(exclusion) & set(x)), self.guess_list))

    def choice(self):
        return random.choice(self.guess_list)


class Wordle:
    END_TURN = 6
    WORD_LENGTH = 5

    def __init__(self, seed: int | None = None) -> None:
        self.dict_ = self.load_dict()
        self.turn = 0

        random.seed(seed)
        self.answer = random.choice(self.dict_)
        assert len(self.answer) == self.WORD_LENGTH

    # ゲーム終了判定
    def is_done(self) -> bool:
        return self.END_TURN <= self.turn

    # 使用辞書のロード
    @staticmethod
    def load_dict(path: str = 'wordles.txt'):
        path = os.path.join(os.path.dirname(__file__), path)
        if not os.path.exists(path):
            import requests
            url = 'https://slc.is/data/wordles.txt'
            logger.info(f'[Download] wordles dictionary from "{url}"')
            res = requests.get(url)
            content = res.content
            with open(path, 'wb') as f:
                f.write(content)
        else:
            with open(path, 'rb') as f:
                content = f.read()
        content = content.decode('utf-8')
        return content.splitlines()

    def evalueate(self, guess: str):
        item_list = sum([[g, a]
                        for g, a in zip(guess, self.answer) if g != a], [])

        c1, c2 = Counter(item_list[::2]), Counter(item_list[1::2])
        items: list[str] = []
        for e in set(item_list[::2]) & set(item_list[1::2]):
            for _ in range(min(c1[e], c2[e])):
                items.append(e)

        buf: list[int] = []
        for g, a in zip(guess, self.answer):
            if g == a:
                buf.append(2)
            elif g in items:
                items.remove(g)
                buf.append(1)
            else:
                buf.append(0)
        return buf

    def predict(self, word: str) -> str:
        if len(word) != self.WORD_LENGTH or word not in self.dict_:
            return ''

        self.turn += 1
        if self.answer == word:
            self.turn += self.END_TURN
            return Color.green(self.answer)

        buf = ''
        for i, e in enumerate(self.evalueate(word)):
            if e == 2:
                buf += Color.green(word[i])
            elif e == 1:
                buf += Color.yellow(word[i])
            else:
                buf += '*'
        return buf

    def start(self, solver: Solver):
        while not self.is_done():
            print(f'[Round {self.turn+1}]')
            guess = solver.choice()
            print('[Solver]:', guess)
            print(self.predict(guess))

            score = self.evalueate(guess)
            solver.update(guess, score)

        if self.turn == self.END_TURN:
            print('Answer: ', self.answer)


if __name__ == '__main__':
    Wordle(0).start(Solver())

    # Wordle().start(StupidSolver())

```