Quordle: Engine#
This week I wanted to revisit a fun project. Specifically, I wanted to try extending the code I wrote to play Wordle to see if I can get it to also play Quordle. For those of who are unfamiliar, Quordle is a similar word game to Wordle in that you guess 1 word per round to try and solve a puzzle. After each round you are provided with feedback per each letter with whether or not the letter appears in the word, appears in the word and is in the correct position, or does not appear in each word. Quordle takes this idea and adds another challenge: you must play 4 simultaneous games of Wordle.
When playing simultaneous games, you must use the same guess across all Wordle boards. For each round, you are provided with the same feedback and if you guess correctly you are finished with that specific board. This extension opens up for new and interesting strategies (which boards do I solve first, how do I go about picking good candidate words) as well as interesting models & maintenance of game state and display.
Original Wordle Code#
Thankfully we can reuse some of the Wordle code I’ve already written. Finding all candidate words and the rules of matching do not change so let’s simply use that same code below.
from enum import Enum
from dataclasses import dataclass
wordlist = []
with open('words') as f:
for line in f:
word = line.strip()
conds = [
len(word) == 5,
word.islower(),
word.isascii(),
word.isalpha(),
not all(l in 'xvci' for l in word)
]
if all(conds):
wordlist.append(word)
class Check(Enum):
CorrectPos = 42
IsInWord = 43
NotInWord = 40
@dataclass
class Status:
letter: str
check: Check
def __str__(self):
return f'\033[97;{self.check.value};1m{self.letter}\033[0m'
def compare(guess, unknown):
for gl, unkl in zip(guess, unknown, strict=True):
if gl == unkl:
yield Status(gl, Check.CorrectPos)
elif gl in unknown:
yield Status(gl, Check.IsInWord)
else:
yield Status(gl, Check.NotInWord)
print(*compare('hello', 'world'), sep='')
hello
The Quordle Extension#
To play this game, I reasoned about the different states I would need to track (mainly, whether a specific game has been solved or not) and progressed from there. I then began writing the main body of my program which parses to these steps:
Hard code some unknowns for each simultaneous board
Hard code initial board states (e.g. has this board been solved)
Hard code some guesses representing each round of Quordle game
Iterate over those guesses and perform a comparison of the guess vs the unknown word for each board
Display the returned game states
Update the
finished
state of each gameIf all boards are solved, declare a winner
The approach by working on my main-loop first and creating ad-hoc helper functions as needed helped to ensure I didn’t code tangent (creating unnecessary features/functions). Additionally by hard-coding all inputs, I was able to focus no the steps themselves rather than the transitions. Once the steps are fleshed out, the simplest transitions start to become apparent.
Note: I wrapped the Quordle related functions into a class with staticmethod
s so that I could reuse the Wordle
functions implemented above. This is not necessary and should only be seen as an organizational convenience.
from IPython.display import clear_output
from itertools import zip_longest
from dataclasses import field
class Quordle:
@staticmethod
def render(results, finished=None):
if finished is None:
finished = [False] * len(results)
buffer = []
for res, fin in zip(results, finished):
if fin is True:
buffer.append(' ' * 5)
else:
buffer.append(''.join(map(str, res)))
return '\N{box drawings heavy vertical}'.join(buffer)
@staticmethod
def compare(guess, unknowns):
return tuple([*compare(guess, unk)] for unk in unknowns)
unknowns = ['hello', 'world', 'tests', 'value']
finished = [False] * len(unknowns)
for guess in ['stair', 'hello', 'bound']:
results = Quordle.compare(guess, unknowns)
print(Quordle.render(results, finished))
for i, res in enumerate(results):
if all(r.check is Check.CorrectPos for r in res):
finished[i] = True
stair┃stair┃stair┃stair
hello┃hello┃hello┃hello
┃bound┃bound┃bound
So far so good! Seems like I have a working game of Quordle. Let’s see if I can hard-code a game to completion! Putting in 9 guesses (where the 4 correct answers are buried in there), let’s see if the game correctly evaluates each guess and exits when the game has been won.
unknowns = ['hello', 'world', 'tests', 'value']
finished = [False] * len(unknowns)
guesses = ['stair', 'hello', 'bound', 'works', 'value', 'tweet', 'usurp', 'world', 'tests']
for guess in guesses:
results = Quordle.compare(guess, unknowns)
print(Quordle.render(results, finished))
for i, res in enumerate(results):
if all(r.check is Check.CorrectPos for r in res):
finished[i] = True
if all(finished):
print('Winner!')
break
else:
print('You lost.')
stair┃stair┃stair┃stair
hello┃hello┃hello┃hello
┃bound┃bound┃bound
┃works┃works┃works
┃value┃value┃value
┃tweet┃tweet┃
┃usurp┃usurp┃
┃world┃world┃
┃ ┃tests┃
Winner!
Looks right to me. Seems like we were able to successfully code & play a game of Quordle with minimal code changes to the original Wordle engine. The trick to tying all of this together was the externalization of state. Access to the finished
state for each board is needed across multiple functions (rendering and win condition), so by ensuring this state is available to the program as a whole I was able to quickly throw together a working prototype.
Wrap Up#
That’s all the time we have for today, implementing this game engine was a lot of fun. Seems like the original Wordle
engine was fairly flexible after all! Stay tuned for next week when I re-implement a within board strategy of smart word choice as well as explore some across board strategies to decide which board to play off for each round in a given game!