cover-img

How to Develop a Wordle Game using TDD in 25 Minutes

6 September, 2022

4

4

1

TL;DR: Implementing a working Wordle in a few minutes

Everybody is playing Wordle these days…
And I love TDD.
So, let's get moving…

TL;DR: With just a few steps we can build a robust Wordle.

Defining a word

The minimum information amount in Wordle is a word.
We can argue that letter is smaller, but we think all needed letter protocol is already defined (we might be wrong).
A word is not a char(5).
A word is not an array.
A word is not a string.
This is a common mistake and a bijection violation.
A word and a string have different responsibilities, though they might intersect.

Mixing (accidental) implementation details with (essential) behavior is a widespread mistake.

So we need to define what is a word.
A word in Wordle is a valid 5-letter word.
Let's start with our happy path:
We assert that prompting for letters in 'valid' returns an array with the letters.

Notice

Word class is not defined yet.

We don't care about letter sorting. That would be a premature optimization and gold plating scenario.

We start with a simple example. No duplicated.

We don't mess with word validation yet (the word might be XXXXX).

We can start with a simpler test just validation word is created. This would violate the test structure that always requires an assertion.

Expected value should always be first.
We get an error:

Error : Class "Wordle\Word" not found

This is good in TDD, We are exploring our domain.

Creating a Word

We need to create a Word with the constructor and the letters() function.

Notice

We don't need anything to do with the constructor parameter.

We hardcode letters function since this is the simplest possible solution up to now. -- Fake it till we make it.

Classes are final to avoid subclassification.
We run all the tests (just 1) and we are OK.

OK (1 test, 1 assertion)

Few Letters

Let's write another test:

Notice

PHPUnit exception raising is not very good. We just declare an exception will be raised.
Test fails…

Failed asserting that exception of type "Wordle\Exception" is thrown.

Changing current implementation

We need to change our implementation in order to make test02 pass (and also test01)

Notice

We just check for a few letters. not for too many since we don't have yet a covering test.

TDD requires full coverage. Adding another check without a test is a technique violation.

We just raise a generic Exception. Creating special exceptions is a code smell that pollutes namespaces. (unless we catch it, but this is not happening right now).

Checking Many Too Letters

Let's check for too many
The test fails as expected. Let's correct it.

Failed asserting that exception of type "Exception" is thrown.

And all tests passed.

OK (3 tests, 3 assertions)

Refactor (or not)

We can now make an (optional) refactor and change the function to assert a range instead of two boundaries. We decide to leave it this way since it is more declarative.
We can also add a test checking for zero words following the Zombie methodology. Let's do it.
it is no surprise test passes since we already have a test covering this scenario. As this test adds no value we should remove it.

Valid Letter

Let's check now what is a valid letter:
… and the test is broken since no assertion is raised.

Failed asserting that exception of type "Exception" is thrown.

We need to correct the code…
And all tests pass since we are clearly hardcoding.

OK (5 tests, 5 assertions)

More Invalid

Let's add more invalid letters and correct the code.

Notice

We didn't write a more generic function (yet) since we cannot correct tests and refactor at the same time (the technique forbids us).

Refactor

All tests are ok. We can refactor. We replace the last two sentences

Notice

The assertion checks only for uppercase letters. Since we are dealing with these examples up to now.

We defer design decisions as much as possible.

We defined a regular expression based on English Letters. We are pretty sure it won't accept Spanish (ñ), German(ë), etc.
As a checkpoint, we have only five letter words from now on.
Lets assert on letters() function. We left it hard coded. TDD Opens many paths. We need to keep track of all of them until we open new ones.

Comparing Words

We need to compare words
And test fails. Let's use the parameter we are sending to them.

Notice

We store the letters and this is enough for object comparison (it might depend on the language).

letters() function is still hardcoded
Tests are OK

OK (8 tests, 8 assertions)

More Words

We add a different word for letters comparison
And test fails.

Failed asserting that two arrays are equal.

It is very important to check for equality/inequality instead of assertTrue() since many IDEs open a comparison tool based on the objects. This is another reason to use IDEs and never text editors.

Refactor

Let's change the letters() function

English Dictionary

Our words are in a bijection with English Wordle words. or not?
This test fails. We are not catching invalid English 5-letter words.
We need to make a decision. According to our bijection, there's an external dictionary asserting valid words.
We can validate with the dictionary upon word creation. But we want the dictionary to store valid wordle words. Not strings.
It is an egg-chicken problem.
We decide to deal with invalid words in the dictionary and not the Wordle word.

Dictionary Tests

We create new tests on our dictionary.
The test fails since we have not defined our Dictionary. We do it:

Notice

We don't do anything with the words yet.

We hardcode the number of words.

We faked it yet again.

Count

We add another case for count 1 if the dictionary has one word.
The test fails as expected

Failed asserting that 0 matches expected 1.

We correct it.

Notice

Dictionary is immutable

No Setters or getters

Including Words

We start with inclusion and get an error.

Error : Call to undefined method Wordle\Dictionary::includesWord()

So we fake it.

Positive Case

We add a positive case. And we need to correct the function instead of hardcoding it.
We have the dictionary working.

Wordle Game

Let's create the game.
Test fails. We need to create the class and the function.

Creating Game Objects

Words tried

We implement words tried. And the simplest solution
Let's try some words. We get

Error : Call to undefined method Wordle\Game::addtry()

We define it.

Notice

We store the trials locally and add the trial and also change wordsTried() real implementation.

Has Lost

We can implement hasLost() if it misses 5 trials. With the simplest implementation as usual.
As always. We stop faking it and decide to make it.

Failed asserting that false is true.

So we change it as below.

Integrating the Dictionary

We have most of the mechanics. Let's add the dictionary and play invalid.
We need to pass the dictionary to fix the tests
Fixed.

Play to Win

Now, we play to win
We need to correct hasWon().

Notice

We use no flags to check if someone has won. We can directly check it.

We make an addParameter refactor with this new element to previous game definitions.

Winner Word

We added winnerWord. We need to assert this word is in the dictionary.

OK (8 tests, 10 assertions)

We have all the mechanics.

Letter Positions

Let's add the letter's positions. We can do it in Word class.
Test passes

OK (10 tests, 10 assertions)

Match

Let's match
Fails.
We need to define it better
We keep running all the tests all the time

OK (23 tests, 25 assertions)

We can add a safety test to be more declarative
Now we need the final steps. Matching in incorrect positions. and always the simplest solution…
A more spicy test case. Let's go for the implementation
We remove from the occurrences the exact matches.

OK (26 tests, 32 assertions)

That's it. We have implemented a very small model with all meaningful rules.

Future Steps

A good model should endure requirements changes.
In the following articles, we will make these enhancements also using TDD:

Manage different languages and characters.

Import the words from a text file.

Add a Visual Engine and host it.

Implement an Absurdle

Develop a machine-learning algorithm to minimize wordle moves.

Repository

You can have all the code (and make pull requests on GitHub)

Conclusion

TDD is an iterative methodology. If you find some missing functionality you can write me on Twitter and we will add it (after a failing test case, of course).
Hope you like this article and enjoy playing Wordle!
More articles are on the way!

test

tutorial

php

code

clean

tdd

driven

development

4

4

1

test

tutorial

php

code

clean

tdd

driven

development

Maxi Contieri

Buenos Aires, Argentina

🎓Learn something new every day.📆 💻CS software engineer 👷coding👨🏽‍🏫teaching ✍🏾writing 🎨Software Design 🏢SOLID 🌉TDD 👴Legacy 💩Code Smells

More Articles

Showwcase is a professional tech network with over 0 users from over 150 countries. We assist tech professionals in showcasing their unique skills through dedicated profiles and connect them with top global companies for career opportunities.

© Copyright 2024. Showcase Creators Inc. All rights reserved.