Inspired by a tweet from Victoria Cohen I have been doing something about what the average score for a Wordle could be over time. There is no perfect way to score this, but it has inspired me to write a class and some tests as a way to do some analysis of the game. The first part of this is to write the initial class to play the game. As part of this, I thought it might be interested to take a Test Driven Approach to building the class.

I laid out (in my mind) the main things I wanted the class to do:

  1. Be created with the word to test - which must be five letters.
  2. Be able to limit the number of guesses to 6 (as that is the wordle rule) and also track if the game has been solved and hold the guess number.
  3. Return a report for each guess that contains an indication of exact, misplaced or not in the word (green, yellow or black in the wordle game).
  4. Raise a specific exception to indicate what the issue was.

Initial setup of the class

Inside Pycharm I set up a new project and set things up as below:

(wordle-2YrnC8QD-py3.9) thomas@Thomass-MacBook-Pro wordle % tree
.
├── poetry.lock
├── pyproject.toml
├── wordle
│ ├── __init__.py
│ ├── shortlist
│ ├── test_wordle.py
│ ├── word_tracker.py
│ └── wordle.py
├── wordle_analysis.py
└── words

I am using poetry inside my repo (obviously). The shortlist file and words files are for use in testing and other calculations. (I have taken the wordlist from Github)

class Wordle():
    locked = False
    solved = False
    attempt = 0

    def __init__(self, word):
        if len(word) != 5:
            raise WordTooLong
        self.answer = word

This is then validated with the following test:

import unittest

from wordle import Wordle, WordTooLong, GameSolved, TooManyAttempts

class TestWordle(unittest.TestCase):
    def test_correct_length(self):
        self.assertRaises(WordTooLong, Wordle, "abcdef")

This means we can now instantiate a game inside python with the code

game = Wordle(<word>)

Guess a word, get a response

The next thing is to implement the check for the word. What I want is for each guess to tell me how close the answer is. What I therefore will do for each guess is to return an array of five integers with the following values.

value square colour meaning
0 Black character is not in the word
1 Yellow character is in the word
2 Green character in the right place

Note that if a letter appears twice (or more) in the guess word but only once in the target word unless one of the letters is in the correct position the first letter is yellow (or one) and the remainder are black (or zero).

This also leads to the specific case that if the guess is correct we get the array [2, 2, 2, 2, 2] as the return from the function.

Testing the guess function

I try to account for this by having the following tests. These tests all run against a game with the word train as the solution unless a test specific game is required (which it is for the test_non_unique_letters) test.


    def setUp(self):
        self.game = Wordle("train")
        
    def test_basics(self):
        self.assertEqual(self.game.answer, "train")
        self.assertFalse(self.game.locked)
        self.assertFalse(self.game.solved)

    def test_guess_1(self):
        self.assertEqual(self.game.guess("trade"), [2, 2, 2, 0, 0])
        self.assertEqual(self.game.attempt, 1)

    def test_guess_2(self):
        self.assertEqual(self.game.guess("raise"), [1, 1, 1, 0, 0])
        self.assertEqual(self.game.attempt, 1)

    def test_correct_answer(self):
        self.assertEqual(self.game.guess("train"), [2,2,2,2,2])
        self.assertTrue(self.game.solved)
        self.assertTrue(self.game.locked)
        self.assertEqual(self.game.attempt, 1)


    def test_correct_locks_game(self):
        self.game.guess("train")
        self.assertRaises(GameSolved, self.game.guess, "nikau")

    def test_non_unique_letters(self):
        my_game = Wordle("sweet")
        self.assertEqual(my_game.guess("event"), [1, 0, 2, 0, 2])
        self.assertEqual(my_game.guess("ivory"), [0, 0, 0, 0, 0])
        self.assertEqual(my_game.guess("women"), [1, 0, 0, 2, 0])
        self.assertEqual(my_game.guess("eette"), [1, 1, 1, 0, 0])

    def test_too_many_guesses(self):
        wordlist = ["women", "nikau", "swack","feens", "fyles", "poled", "clags"]

        def myfunc():
            for word in wordlist:

                self.game.guess(word)
                print(f"Guessing {word} {self.game.attempt}")
        self.assertRaises(TooManyAttempts, myfunc)

This is the code that it runs on.

    def guess(self, guess):
        if self.attempt >= 6:
            raise TooManyAttempts
        if self.solved:
            raise GameSolved

        self.attempt += 1
        report = [0, 0, 0, 0, 0]
        if guess == self.answer:
            self.locked = True
            self.solved = True
            return [2, 2, 2, 2, 2]

        # first see the matching letters
        notinplace = ""
        for i in range(5):
            if guess[i] == self.answer[i]:
                report[i] = 2
            else:
                notinplace = notinplace + self.answer[i]

            # print(f"Not in place: {notinplace} for {guess} {self.answer} {report}")

        # Now to find the letters not correctly placed
        for i in range(5):
            if report[i] == 2:
                continue
            # print(f"checking if {guess[i]} in {notinplace} ", end="")
            if guess[i] in notinplace:
                report[i] = 1
                notinplace = notinplace.replace(guess[i], "", 1)
            # print(report)

        return report

Links