Wordle Investigation with Python TDD and the Game (Part 1)
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:
- Be created with the word to test - which must be five letters.
- 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.
- 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).
- 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
- The code for this on Github