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:

  1. Hard code some unknowns for each simultaneous board

  2. Hard code initial board states (e.g. has this board been solved)

  3. Hard code some guesses representing each round of Quordle game

  4. Iterate over those guesses and perform a comparison of the guess vs the unknown word for each board

  5. Display the returned game states

  6. Update the finished state of each game

  7. If 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 staticmethods 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
stairstairstairstair
hellohellohellohelloboundboundbound

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.')
stairstairstairstair
hellohellohellohelloboundboundboundworksworksworksvaluevaluevaluetweettweet┃     
     ┃usurpusurp┃     
     ┃worldworld┃     
     ┃     ┃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!