Practice Python

Beginner Python exercises

12 February 2022

A Python Wordle Clone

Written by Michele Pratusevich. When learning how to program, I always recommend having a project or application in mind. For me, it makes it easier to concentrate and learn specific skills and flex the critical thinking muscle. What better project to attempt than the latest game craze, Wordle? In this post-essay, I’ll take you through the steps required to write your own version of Wordle on the command-line without a GUI in Python.

If you’d rather, feel free to take this as an exercise / challenge yourself (definitely something like 7 chilis ), then come back here to see how I did it. Alternatively, use this as a guide to code along, as you see my thought process. Note that the way I’ve chosen to structure the code is just one way this could have been implemented. If you go a different route, let’s discuss in the comments. Feel free to skip around!

Requirements

Before starting any programming project, it is important to make a list of all the features you want it to have. This accomplishes two things: (1) gives you a “stopping point” to know when you’re done, and (2) lets you organize your thoughts to make sure your choices for how to implement cover all your use cases.

In our case, for a Python command-line clone of Wordle, here is our requirements and features list:

Gameplay

Rules

Basic Design

For any command-line game, the basic program structure is a large infinite while loop that keeps “playing the game” until an end condition is met. According to our requirements list, the end conditions are:

  1. The player guesses the word correctly.
  2. The player exits by pressing CTRL-C.

So the first step is to structure our program with this basic scaffold (in a __main__ block). First we tackle the first condition. We don’t deal with selecting the word or processing guesses, so we create two dummy variables (WORD and GUESS), set them to specific strings, and continue adding on later.

if __name__ == '__main__':
  WORD = "TESTS"
  while True:
    GUESS = "PHONE"
    if WORD == GUESS:
      print("You won!")
      break

Now we can add in the second end condition (exit on CTRL-C). This is slightly more complicated, since in Python the way to catch a CTRL-C press is through a KeyboardInterrupt exception, so we wrap the existing loop in a try/catch to catch the CTRL-C.

if __name__ == '__main__':
  WORD = "TESTS"

  try:
    while True:
      GUESS = "PHONE"
      if WORD == GUESS:
        print("You won!")
        break
  except KeyboardInterrupt:
    print("You exited the game.")

Now that we have the structure of the main loop, we can add in the mechanics for player interaction, namely, taking player guesses.

Player Guesses

One feature of the game play is that we need some error checking / handling on a player’s guesses. Because this is a functionality that can be independently tested and verified, processing and getting a user guess is a great candidate for putting inside of a function. The heading of this function will look like this:

def get_user_guess(wordlen: int, wordlist: typing.Set[str]) -> str:
    """Get a user guess input, validate, and return the guess."""

The type annotation is natively supported in Python 3, as seen in the official documentation for the typing library. What our function signature tells us is that the function is going to get the user guess. It takes in two inputs: (1) the length of the expected game word (which is an int), and (2) the list of available guess words as strings. The function will return a single string, which is the player’s guess. Because we return the string of the guess from this function, all the logic around validating a guess, displaying to the player why the guess is in valid, and asking the user to input a new guess, is contained in this function. Because we are potentially in a situation where we need to ask the user multiple times to enter a guess (if for example they keep entering a 4-letter word when the game word is 5 letters), we use another while loop here for the structure.

def get_user_guess(wordlen: int, wordlist: typing.Set[str]) -> str:
    """Get a user guess input, validate, and return the guess."""
    # continue looping until you get a valid guess
    while True:
        guess = input("Guess: ")
        # here we overwrite guess with
        # the filtered guess
        error, guess = validate(guess=guess, wordlen=wordlen,
                                wordlist=wordlist)

        # if there wasn't an error with the guess, we stop asking
        # the user for new guesses
        if error is None:
            break

        # show the input error to the user, as appropriate
        print(error)
    return guess

The choice I’ve made here is to abstract the “validation” logic into yet another function. This is because validating a guess is a repeatable action we take on a word that can be independently tested. Let’s write that validation function now:

def validate(guess: str, wordlen: int,
             wordlist: typing.Set[str]) -> typing.Tuple[str, str]:
    """
    Validate a guess from a user.

    Return tuple of [None if no error or a string containing
    the error message, the guess].
    """

Our validation function header will validate a guess according to any validation rules we want. It takes in a guess as a string (any string), the expected length of the game word, and the list of possible game words. What the validation function returns is a tuple containing the error and the guess word. If there is no error, None is returned for the error. This is again a design choice - some programmers will not return the guess word back, and rely on the fact that if None is returned for the error, the initial guess word is fine. However, the choice here of returning the guess word is a form of filtering & validation. Because we listed in the requirements that guesses can be entered in uppercase or lower case, we want to standardize all the guesses into uppercase. This means that somewhere we will have to convert the guess into all uppercase letters. What better place to do this than the same function where we are validating our guesses?

The choice for why our wordlist variable is a set comes out of this function as well. Inside the validation function, we are checking whether the guess is a member of the valid wordlist - this operation is cheap to do from a set, so we choose to take in our wordlist as a set of strings.

The implementation of the function then looks like this:

def validate(guess: str, wordlen: int,
             wordlist: typing.Set[str]) -> typing.Tuple[str, str]:
    """
    Validate a guess from a user.

    Return tuple of [None if no error or a string containing
    the error message, the guess].
    """
    # make sure the guess is all upper case
    guess_upper = guess.upper()
    # guesses must be the same as the input word
    if len(guess_upper) != wordlen:
        return f"Guess must be of length {wordlen}", guess_upper

    # guesses must also be words from the word list
    if guess_upper not in wordlist:
        return "Guess must be a valid word", guess_upper
    return None, guess_upper

Note that all the return statements return a tuple: the first argument is an error string (or None if we get all the way to the end), and the guess converted into uppercase.

Aside: The syntax in the return statement with the f"Guess must be of length {wordlen}" uses Python 3’s built-in f-strings. They are called f-strings because it stands for “formatted string literals.” It is a special syntax to turn variables easily into strings using the curly braces around the variable name, with lots of formatting options depending on what is desired. The Python documentation has a few examples.

Now that we have a validation function, a user input function, and a main loop, our code so far looks like:

import typing


def validate(guess: str, wordlen: int,
             wordlist: typing.Set[str]) -> typing.Tuple[str, str]:
    """
    Validate a guess from a user.

    Return tuple of [None if no error or a string containing
    the error message, the guess].
    """
    # make sure the guess is all upper case
    guess_upper = guess.upper()
    # guesses must be the same as the input word
    if len(guess_upper) != wordlen:
        return f"Guess must be of length {wordlen}", guess_upper

    # guesses must also be words from the word list
    if guess_upper not in wordlist:
        return "Guess must be a valid word", guess_upper
    return None, guess_upper


def get_user_guess(wordlen: int, wordlist: typing.Set[str]) -> str:
    """Get a user guess input, validate, and return the guess."""
    # continue looping until you get a valid guess
    while True:
        guess = input("Guess: ")
        # here we overwrite guess with
        # the filtered guess
        error, guess = validate(guess=guess, wordlen=wordlen,
                                wordlist=wordlist)

        # if there wasn't an error with the guess, we stop asking
        # the user for new guesses
        if error is None:
            break

        # show the input error to the user, as appropriate
        print(error)
    return guess

if __name__ == '__main__':
  WORD = "TESTS"
  GAME_WORD_LENGTH = len(WORD)
  GUESS_WORDLIST = [WORD, "GAMES", "FRONT", "TILES"]

  try:
    while True:
      GUESS = get_user_guess(GAME_WORD_LENGTH, GUESS_WORDLIST)

      if WORD == GUESS:
        print("You won!")
        break
  except KeyboardInterrupt:
    print("You exited the game.")

At this point, we have a game where player input is continually read and validated! Next up, displaying information back to the player so the game is actually fun!

Displaying Guesses

Now we get to an interesting coding step: we need to take the guess that we have taken from the user, parse it, and display the information back to the user in a way that makes the game fun. There are two types of information that can be displayed back to the user: guessed letters in the correct place, and guessed letters in the incorrect place. We implement these in that order, to make sure we got it right!

Like always, we start with the function signature:

def compare(expected: str, guess: str) -> typing.List[str]:
    """Compare the guess with the expected word and return the output parse."""

What we’ll do is take in the game word (in the expected variable) as a string, and the (parsed and validated) guess word (in the guess variable) as a string. This function will do all the character comparison, and output a list of strings denoting the state of each letter in the guess. Per our requirements set up in the previous section, the characters in the output mean the following: (a) _ is a blank - no information is known about this letter; (b) * is a guessed letter is in the correct place in the game word; (c) - is a guessed letter that is correct but in the incorrect place in the game word.

We return this parsed list with symbols rather than do the printing directly in this function to separate concerns. If we ever want to change how the display is done in the command-line (for example, add emojis, or add colors, etc.) then we can use the returned parsed list to generate an output.

The first step in writing the function is to set up the output, and we will tackle the easier part of answer parsing: characters in the guessed word that are in the correct place in the game word.

Since this is a tricky function in the Wordle implementation, and the one that most directly affects gameplay, let’s start with a set of test cases that we can call on our function to make sure we have all the cases right!

Function call   Expected Output
compare("steer", "stirs")   * * _ - _
compare("steer", "floss")   _ _ _ - _
compare("pains", "stirs")   _ _ * _ *
compare("creep", "enter")   - _ _ * -
compare("crape", "enter")   - _ _ _ -
compare("ennui", "enter")   * * _ _ _

These test cases cover a range of outputs and cases that we expect this function to accomplish, both “normal” behavior and a few edge cases that are tricky. What we’re doing here is a miniature version of a software engineering concept called Test Driven Development (TDD). In TDD, the idea is that before you actually write your code, you think about how you are going to test your code, and oftentimes, write the tests first. That way, the verification for “does my code do what it needs to” is already done.

So, let’s begin! First we implement the logic for checking whether guessed characters are in the correct place. This is done (per the rules requirements above) first before the other characters, and those correctly-guessed letters cannot be double-counted. Here is our implementation:

def compare(expected: str, guess: str) -> typing.List[str]:
    """Compare the guess with the expected word and return the output parse."""
    # the output is assumed to be incorrect to start,
    # and as we progress through the checking, update
    # each position in our output list
    output = ["_"] * len(expected)

    # first we check for expected characters in the correct positions
    # and update the output accordingly
    for index, (expected_char, guess_char) in enumerate(zip(expected, guess)):
        if expected_char == guess_char:
            # a correct character in the correct position
            output[index] = "*"
    # return the list of parses
    return output

At this point if we call this function with our test cases, we will see that we correctly mark the correctly-guessed characters!

Function call   Output so far
compare("steer", "stirs")   * * _ _ _
compare("steer", "floss")   _ _ _ _ _
compare("pains", "stirs")   _ _ * _ *
compare("creep", "enter")   _ _ _ * _
compare("crape", "enter")   _ _ _ _ _
compare("ennui", "enter")   * * _ _ _

Now we move on to the more challenging part: checking for guessed letters that are in the correct place. The way we are going to do this is as follows:

Note that we have to introduce a new structure here to capture the game state: we need to keep track of which characters in the game word have been correctly identified in the game word (whether in the correct or incorrect positions). The reason for this is to take into account the potential for the game word and guess word to both have multiple characters. If there are multiple characters in the guess word, we only count their correct or incorrectness once. That is, if the game word has one “R” but the guess word has two “R”s, then we at most display information back to the user for a single “R” in the guess word (whether that is in the correct or incorrect place). Our test cases listed above cover these gameplay cases.

In this design, we need to write one helper function. Namely, we need to find all the positions of a given letter in the game word. There isn’t any pre-built Python function to do this, so we need to build our own. We base our helper function on the built-in function .find() that returns the first index of the desired character in the string. What we want is ALL the positions, so we wrap this function into a loop, and take advantage of the fact that we can specify where in the target word we can search for a character.

def find_all_char_positions(word: str, char: str) -> typing.List[int]:
    """Given a word and a character, find all the indices of that character."""
    positions = []
    pos = word.find(char)
    while pos != -1:
        positions.append(pos)
        pos = word.find(char, pos + 1)
    return positions

We make sure to return a sorted list of positions in the game word as a list of integers, since this is how we expect to use the output of this function for gameplay. The neat thing here is that we do not need to call sorted() on our output list positions because the way we call .find() is guaranteed to return the positions in increasing order.

So with the find_all_char_positions function built, our code now looks like this:

def compare(expected: str, guess: str) -> typing.List[str]:
    """Compare the guess with the expected word and return the output parse."""
    # the output is assumed to be incorrect to start,
    # and as we progress through the checking, update
    # each position in our output list
    output = ["_"] * len(expected)
    counted_pos = set()

    # first we check for correct words in the correct positions
    # and update the output accordingly
    for index, (expected_char, guess_char) in enumerate(zip(expected, guess)):
        if expected_char == guess_char:
            # a correct character in the correct position
            output[index] = "*"
            counted_pos.add(index)

    # now we check for the remaining letters that are in incorrect
    # positions. in this case, we need to make sure that if the
    # character that this is correct for was already
    # counted as a correct character, we do NOT display
    # this in the double case. e.g. if the correct word
    # is "steer" but we guess "stirs", the second "S"
    # should display "_" and not "-", since the "S" where
    # it belongs was already displayed correctly
    # likewise, if the guess word has two letters in incorrect
    # places, only the first letter is displayed as a "-".
    # e.g. if the guess is "floss" but the game word is "steer"
    # then the output should be "_ _ _ - _"; the second "s" in "floss"
    # is not displayed.
    for index, guess_char in enumerate(guess):
        # if the guessed character is in the correct word,
        # we need to check the other conditions. the easiest
        # one is that if we have not already guessed that
        # letter in the correct place. if we have, don't
        # double-count
        if guess_char in expected and \
                output[index] != "*":
            # first, what are all the positions the guessed
            # character is present in
            positions = find_all_char_positions(word=expected, char=guess_char)
            # have we accounted for all the positions
            for pos in positions:
                # if we have not accounted for the correct
                # position of this letter yet
                if pos not in counted_pos:
                    output[index] = "-"
                    counted_pos.add(pos)
                    # we only count the "correct letter" once,
                    # so we break out of the "for pos in positions" loop
                    break
    # return the list of parses
    return output

We choose a set as the data structure (in the counted_pos variable) for keeping track of the counted positions, since the ordering of the positions doesn’t matter, and the most common operation we do on this structure is checking whether the variable pos is contained in it, which makes a set an appropriate data structure.

Now we can check against our test cases to confirm we got it right:

Function call   Output
compare("steer", "stirs")   * * _ - _
compare("steer", "floss")   _ _ _ - _
compare("pains", "stirs")   _ _ * _ *
compare("creep", "enter")   - _ _ * -
compare("crape", "enter")   - _ _ _ -
compare("ennui", "enter")   * * _ _ _

And there we go! A comparison function for parsing user guesses against a game word. Now we plug this into our main loop for some gameplay!

import typing


def validate(guess: str, wordlen: int,
             wordlist: typing.Set[str]) -> typing.Tuple[str, str]:
    """
    Validate a guess from a user.

    Return tuple of [None if no error or a string containing
    the error message, the guess].
    """
    # make sure the guess is all upper case
    guess_upper = guess.upper()
    # guesses must be the same as the input word
    if len(guess_upper) != wordlen:
        return f"Guess must be of length {wordlen}", guess_upper

    # guesses must also be words from the word list
    if guess_upper not in wordlist:
        return "Guess must be a valid word", guess_upper
    return None, guess_upper


def get_user_guess(wordlen: int, wordlist: typing.Set[str]) -> str:
    """Get a user guess input, validate, and return the guess."""
    # continue looping until you get a valid guess
    while True:
        guess = input("Guess: ")
        # here we overwrite guess with
        # the filtered guess
        error, guess = validate(guess=guess, wordlen=wordlen,
                                wordlist=wordlist)
        if error is None:
            break

        # show the input error to the user, as appropriate
        print(error)
    return guess


def find_all_char_positions(word: str, char: str) -> typing.List[int]:
    """Given a word and a character, find all the indices of that character."""
    positions = []
    pos = word.find(char)
    while pos != -1:
        positions.append(pos)
        pos = word.find(char, pos + 1)
    return positions


def compare(expected: str, guess: str) -> typing.List[str]:
    """Compare the guess with the expected word and return the output parse."""
    # the output is assumed to be incorrect to start,
    # and as we progress through the checking, update
    # each position in our output list
    output = ["_"] * len(expected)
    counted_pos = set()

    # first we check for correct words in the correct positions
    # and update the output accordingly
    for index, (expected_char, guess_char) in enumerate(zip(expected, guess)):
        if expected_char == guess_char:
            # a correct character in the correct position
            output[index] = "*"
            counted_pos.add(index)

    # now we check for the remaining letters that are in incorrect
    # positions. in this case, we need to make sure that if the
    # character that this is correct for was already
    # counted as a correct character, we do NOT display
    # this in the double case. e.g. if the correct word
    # is "steer" but we guess "stirs", the second "S"
    # should display "_" and not "-", since the "S" where
    # it belongs was already displayed correctly
    # likewise, if the guess word has two letters in incorrect
    # places, only the first letter is displayed as a "-".
    # e.g. if the guess is "floss" but the game word is "steer"
    # then the output should be "_ _ _ - _"; the second "s" in "floss"
    # is not displayed.
    for index, guess_char in enumerate(guess):
        # if the guessed character is in the correct word,
        # we need to check the other conditions. the easiest
        # one is that if we have not already guessed that
        # letter in the correct place. if we have, don't
        # double-count
        if guess_char in expected and \
                output[index] != "*":
            # first, what are all the positions the guessed
            # character is present in
            positions = find_all_char_positions(word=expected, char=guess_char)
            # have we accounted for all the positions
            for pos in positions:
                # if we have not accounted for the correct
                # position of this letter yet
                if pos not in counted_pos:
                    output[index] = "-"
                    counted_pos.add(pos)
                    # we only count the "correct letter" once,
                    # so we break out of the "for pos in positions" loop
                    break
    # return the list of parses
    return output


if __name__ == '__main__':
  WORD = "TESTS"
  GAME_WORD_LENGTH = len(WORD)
  GUESS_WORDLIST = [WORD, "GAMES", "FRONT", "TILES"]

  try:
    while True:
      # get the user to guess something
      GUESS = get_user_guess(
          wordlen=GAME_WORD_LENGTH, wordlist=GUESSWORD_WORDLIST)

      # display the guess when compared against the game word
      result = compare(expected=WORD, guess=GUESS)
      print(" ".join(result))

      if WORD == GUESS:
        print("You won!")
        break
  except KeyboardInterrupt:
    print("You exited the game.")

And now we have a playable game!

Generating the Wordlist

Of course, one “trick” we did with our implementation so far is hard-code the game word. This makes for a pretty boring game, so as the next step is to generate some word lists that are more fun!

There is “right way” to generate a word list. Because of a previous exercise on this blog, I had a local copy of SOWPODS, the official Scrabble word list, locally. However, as you know of Scrabble words, some are a bit obscure and not very fun for gameplay. Besides, SOWPODS has words of many lengths, whereas in our version of Wordle we wanted to limit only to 5-letter words.

The additional detail about word lists in Wordle is that there are actually two word lists. One word list for game words (which has fewer words, and contains more “common” words), and a separate word list for validating guesses, which contains the game word list and a bunch of other more “obscure” words. I don’t know for sure how the original implementor of Wordle generated these lists, but I have to assume some amount of hand-curation was done.

We on the other hand don’t need to hand-curate our list (unless you want to for your own implementation). Instead, we look at the page source for Wordle and extract the lists from that code. I won’t go into exactly how I did that here, since I didn’t use Python for it! We save these as two separate word lists (gamewords.txt and guesswords.txt) that we can load into our game. One helper function we will write (since it will be used twice, once for each list of words), is a create_wordlist() helper function that loads the word lists into a list and makes sure all the words are upper case.

We actually implement this as two functions: one for filtering/modifying words, and one for ingesting the word list in the first place. This is because if we ever want to expand the word lists to contain words of multiple lengths, we don’t need to maintain separate word lists for all the lengths; we can simply put them in one list, and filter the random game/guess words in real time during game play.

First we implement a filter_word() function that takes in a word and any filtering criteria. It returns back the word modified (in our case converted to upper case). If we decide to add new functionality into our Wordle implementation (for example, limiting words to only start with “T” or something else), then that functionality would be added into this function.

def filter_word(word: str, length: int) -> str:
    """Filter a word to add it to the wordlist."""
    if len(word.strip()) == length:
        # make sure all the words in our wordlist
        # are stripped of whitespace, and all upper case,
        # for consistency in checking
        return word.strip().upper()
    return None

Now when we implement our create_wordlist() function, we can make use of a Python built-in function called map. I have not yet written an exercise specifically about this function, so I’ll briefly touch on it here. The purpose of map is to apply the same function on any iterable (list, dictionary, set) and return a new version of that iterable with the function applied. In our case this works out perfectly: what we want to do is take in a list of words that we loaded from a text file, and we want to make them all uppercase. More specifically, we want to apply our filter_word function to each element of the word list.

def create_wordlist(fname: str, length: int) -> typing.List[str]:
    """Load and create a wordlist from a filename."""
    with open(fname, "r") as f:
        lines = f.readlines()
    return list(map(lambda word: filter_word(word, length), lines))

The end result is that when we call the create_wordlist() function on our word list, it converts all the words properly to upper case and filters out any undesirable words.

At this point we are ready to put it all together!

Putting it All Together

Now we just add a few bells and whistles to the text of what the user sees during gameplay (the number of guesses taken, for example), and voila! We have a full Wordle implementation in Python!

"""An implementation of Wordle in Python."""
import random
import typing


GAMEWORD_LIST_FNAME = "gamewords.txt"
GUESSWORD_LIST_FNAME = "guesswords.txt"


def filter_word(word: str, length: int) -> str:
    """Filter a word to add it to the wordlist."""
    if len(word.strip()) == length:
        # make sure all the words in our wordlist
        # are stripped of whitespace, and all upper case,
        # for consistency in checking
        return word.strip().upper()
    return None


def create_wordlist(fname: str, length: int) -> typing.List[str]:
    """Load and create a wordlist from a filename."""
    with open(fname, "r") as f:
        lines = f.readlines()
    return list(map(lambda word: filter_word(word, length), lines))


def validate(guess: str, wordlen: int,
             wordlist: typing.Set[str]) -> typing.Tuple[str, str]:
    """
    Validate a guess from a user.

    Return tuple of [None if no error or a string containing
    the error message, the guess].
    """
    # make sure the guess is all upper case
    guess_upper = guess.upper()
    # guesses must be the same as the input word
    if len(guess_upper) != wordlen:
        return f"Guess must be of length {wordlen}", guess_upper

    # guesses must also be words from the word list
    if guess_upper not in wordlist:
        return "Guess must be a valid word", guess_upper
    return None, guess_upper


def get_user_guess(wordlen: int, wordlist: typing.Set[str]) -> str:
    """Get a user guess input, validate, and return the guess."""
    # continue looping until you get a valid guess
    while True:
        guess = input("Guess: ")
        # here we overwrite guess with
        # the filtered guess
        error, guess = validate(guess=guess, wordlen=wordlen,
                                wordlist=wordlist)
        if error is None:
            break

        # show the input error to the user, as appropriate
        print(error)
    return guess


def find_all_char_positions(word: str, char: str) -> typing.List[int]:
    """Given a word and a character, find all the indices of that character."""
    positions = []
    pos = word.find(char)
    while pos != -1:
        positions.append(pos)
        pos = word.find(char, pos + 1)
    return positions


# test cases for find_all_char_positions
# find_all_char_positions("steer", "e") => [2, 3]
# find_all_char_positions("steer", "t") => [1]
# find_all_char_positions("steer", "q") => []


def compare(expected: str, guess: str) -> typing.List[str]:
    """Compare the guess with the expected word and return the output parse."""
    # the output is assumed to be incorrect to start,
    # and as we progress through the checking, update
    # each position in our output list
    output = ["_"] * len(expected)
    counted_pos = set()

    # first we check for correct words in the correct positions
    # and update the output accordingly
    for index, (expected_char, guess_char) in enumerate(zip(expected, guess)):
        if expected_char == guess_char:
            # a correct character in the correct position
            output[index] = "*"
            counted_pos.add(index)

    # now we check for the remaining letters that are in incorrect
    # positions. in this case, we need to make sure that if the
    # character that this is correct for was already
    # counted as a correct character, we do NOT display
    # this in the double case. e.g. if the correct word
    # is "steer" but we guess "stirs", the second "S"
    # should display "_" and not "-", since the "S" where
    # it belongs was already displayed correctly
    # likewise, if the guess word has two letters in incorrect
    # places, only the first letter is displayed as a "-".
    # e.g. if the guess is "floss" but the game word is "steer"
    # then the output should be "_ _ _ - _"; the second "s" in "floss"
    # is not displayed.
    for index, guess_char in enumerate(guess):
        # if the guessed character is in the correct word,
        # we need to check the other conditions. the easiest
        # one is that if we have not already guessed that
        # letter in the correct place. if we have, don't
        # double-count
        if guess_char in expected and \
                output[index] != "*":
            # first, what are all the positions the guessed
            # character is present in
            positions = find_all_char_positions(word=expected, char=guess_char)
            # have we accounted for all the positions
            for pos in positions:
                # if we have not accounted for the correct
                # position of this letter yet
                if pos not in counted_pos:
                    output[index] = "-"
                    counted_pos.add(pos)
                    # we only count the "correct letter" once,
                    # so we break out of the "for pos in positions" loop
                    break
    # return the list of parses
    return output


# test cases for comparing
# compare("steer", "stirs") -> "* * _ - _"
# compare("steer", "floss") -> "_ _ _ - _"
# compare("pains", "stirs") -> "_ _ * _ *"
# compare("creep", "enter") -> "- _ _ * -"
# compare("crape", "enter") -> "- _ _ _ -"
# compare("ennui", "enter") -> "* * _ _ _"


if __name__ == '__main__':
    # the game word is 5 letters
    WORDLEN = 5
    # load the wordlist that we will select words from
    # for the wordle game
    GAMEWORD_WORDLIST = create_wordlist(
        GAMEWORD_LIST_FNAME, length=WORDLEN)

    # load the wordlist for the guesses
    # this needs to be a set, since we pass this into the get_user_guess()
    # function which expects a set
    GUESSWORD_WORDLIST = set(create_wordlist(
        GUESSWORD_LIST_FNAME, length=WORDLEN))

    # select a random word to start with
    WORD = random.choice(GAMEWORD_WORDLIST)
    GAME_WORD_LENGTH = len(WORD)

    # keep track of some game state
    NUM_GUESSES = 0

    # print the game instructions to the user
    print("""
Guess words one at a time to guess the game word.

A * character means a letter was guessed correctly
in the correct position.
A - character means a letter was guessed correctly,
but in the incorrect position.

To quit, press CTRL-C.
""")

    # start of the user name interaction
    print("_ " * GAME_WORD_LENGTH)

    # we use a continuous loop, since there could be a number
    # of different exit conditions from the game if we want
    # to spruce it up.
    try:
        while True:
            # get the user to guess something
            GUESS = get_user_guess(
                wordlen=GAME_WORD_LENGTH, wordlist=GUESSWORD_WORDLIST)
            NUM_GUESSES += 1

            # display the guess when compared against the game word
            result = compare(expected=WORD, guess=GUESS)
            print(" ".join(result))

            if WORD == GUESS:
                print(f"You won! It took you {NUM_GUESSES} guesses.")
                break
    except KeyboardInterrupt:
        print(f"""
You quit - the correct answer was {WORD.upper()}
and you took {NUM_GUESSES} guesses
""")

When you run this code through the terminal, a sample gameplay looks like this:

[mprat@europa] $ python wordle.py

Guess words one at a time to guess the game word.

A * character means a letter was guessed correctly
in the correct position.
A - character means a letter was guessed correctly,
but in the incorrect position.

To quit, press CTRL-C.

_ _ _ _ _ 
Guess: stare
_ _ _ - -
Guess: reign
- - _ _ _
Guess: erode
- - _ _ -
Guess: never
_ - _ * *
Guess: cheer
* * * * *
You won! It took you 5 guesses.

This website contains a few exercises that are relevant to the skills required to code up Wordle. Here are a few, but feel free to get started anywhere!

Enjoying Practice Python?


Explore Yubico
Explore Yubico
comments powered by Disqus