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!
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:
*
.-
._
.-
. If the game word is “STEER” and the guess word is “MONEY” then the “E” in “MONEY” is shown as *
.-
.*
and the second copy of the letter is unmarked.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:
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.
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!
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:
-
, add that position to the accounted-for set, and move on to the next guessed character.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!
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!
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!
input
to get info from the user