Homework 5: Wordle

Wordle is a indie word-guessed game-turned social phenomena. The game is quite simple:

Guess the WORDLE in six tries.

Each guess must be a valid five-letter word. Hit the enter button to submit.

After each guess, the color of the tiles will change to show how close your guess was to the word.

In this demonstration exercise, we will use our string/array/pointer manipulation skills to implement a command-line version of Wordle that is as faithful as possible to the original! Cloning an existing application is an excellent starting point for a project that can help you build up programming skills, learn about a new area of discipline, and serve as a basis for a larger endeavour that might appear in your eventual portfolio for job applications!

You should implement your clone of Wordle in a file called wordle.c and upload it to Gradescope when you are done.

The Basic Wordle Game

Many application have a simple core engine that is simple to describe and implement. However, what frequently makes the application interesting are the features built on top of this engine which add complexity and depth. Consequently, when we go to implement an application, it is useful to begin with the core engine and build outwards. Not only does this allow us to structure or architect our program in a natural fashion, it also is a strong confidence builder getting a minimum viable product up and running as quickly as possible.

The core of Wordle is a simple loop. Given a five-letter target word:

  • For each of the six turns of the game.
    • The user guesses a word.
    • The game checks to see if the guess matches the target word. If so, the user wins and the game is over.
    • Otherwise the game gives a report of how close the guess was to the target word and the turn ends.

In this sense, Wordle is merely a simple guessing game. However, Wordle's reports at the end of each turn are what make it unique. For each character in the guess, Wordle will:

  • Highlight the character green if the character matches the corresponding character in the target word.
  • Highlight the character yellow if the character does not match but the character appears somewhere in the target word.

Otherwise, the character is not in the target word, so Wordle displays the character without any additional highlighting.

For example, suppose that the target word is trove and the user guesses the word tread, then the game would:

  • Highlight t green because it is in the correct position.
  • Highlight r green because it is in the correct position.
  • Highlight e yellow because it is in the wrong position but in the target word.

And it would not highlight a and d because they are not in the target word.

Part 1: Getting input from the user

From the game description above, we can see there are three key operations that we must implement to complete the Wordle core engine:

  1. Getting a five-letter word from the user.
  2. Computing a report comparing the user's guess against the target word.
  3. Displaying the report using colors.

Let's start with getting input from the user. Write a function called void get_guess(const int turn, char *guess) that prompts the user for a guess and populates the char buffer pointed to by guess with that guess. The function prompts the user by displaying the text Guess <turn>: where turn is the turn number the game is currently on. A pre-condition of the function is that the character buffer pointed to by guess is at least six characters. For example, if we call get_guess(2, guess), then with the following interaction:

Guess 2: slate

Where the system prints "Guess 2: " and the user responds with slate, then the characters s, l, a, t, e, and the null terminator \0 are all stored in guess.

For now, we will assume the user enters in an appropriate five-letter word and not worry about any kind of error handling and re-prompting that might be required. This means that we can use the stdio.h function char getchar(void) which fetches a single character from the user to build up their guess one character at a time. Make sure to account for the fact that the user will need to enter a newline to send their text to stdin!

Part 2: Computing the report

Assuming we have received the guess from the user, now let's compute the report of how close their guess is to the actual word. While we can combine the computation of the report with the actual reporting, it is beneficial to break up the two steps into separate functions. That way, we can test and debug them independently.

Write a function int check_guess(const char *guess, const char *target, char result []) that takes two strings, the guess and the target, compares them according to the rules of Wordle, and then returns the number of characters that matched exactly. If check_guess returns 5, that means that the guess is equal to the target. As a precondition of the function, guess and target must both be strings of length 5.

result is assumed to be a char array of size five that check_guess populates with the per-character results of the check.

  • If the ith character of guess matches target exactly, then the ith character of result will be g (for "green").
  • If the ith character of guess does not match target but the ith character appears somewhere inside of target, then the ith character of result will be y (for "yellow");
  • Otherwise, the ith character of guess is x (for "no match").

For example, if we call check_guess("tread", "trove", result) then the function returns 2 since t and t matches and result will contain the characters c, c, y, x, and x in sequence.

Part 3: Printing the report

After computing the report and generating the result string, we now need to echo back the guess with appropriate highlighting as described by the report string. Write a function void print_report(const char *guess, const char result []) which prints guess followed by a newline. However, for each character c of guess:

  • If the corresponding character of result is g, c should be printed with a green background.
  • If the corresponding character of result is y, c should be printed with a yellow background.
  • Otherwise, c is printed normally.

To print individual characters, you can use the %c format specifier, taking care to ensure that what you pass to printf has type char rather than char*. To print with colored backgrounds, you will need to take advantage of special ANSI color escape sequences which causes the terminal to print colored text. These character sequences have the form \033[...m where the ... are a series of semicolon separated parameters. The effect of "printing" one of these sequences is to cause the terminal to print subsequent characters with the specified color.

In particular:

  • \033[1;37;42m renders subsequent text with a green background.
  • \033[1;37;43m renders subsequent text with a yellow background.
  • \033[0m resets the colors back to the default.

For example, the following calls to printf:

printf("\033[1;37;42mgreen\033[0m\n");
printf("\033[1;37;43myellow\033[0m\n");

Print green and yellow to the console with green and yellow backgrounds, respectively.

Part 4: Putting it all together

With your three functions in-hand, write a function called void play_game(const char *target) that targets a five-letter target word as input and plays a complete game of Wordle on the command-line.

Part 5: Choosing random words from a dictionary

Finally, we can write a main function that calls play_game to play a Wordle game. The only catch is that we need a dictionary of words to draw from. Let's solve the problem of loading a dictionary from a file in a second step. For now, let's hard code a dictionary in main, an array of test words, randomly choose one of those words, and then pass that word to play_game.

As far as your hard-coded dictionary goes, you should create a small dictionary of strings, e.g.,

char *dictionary [] = { "hello", "slate", ... }

Include a number of five-letter strings so you can adequately test the program before you try to load words from a larger data source.

To randomly choose a word from this array, you will need to use the rand() function from stdlib.h. int rand(void) generates a random number from in the range 0 to RAND_MAX which is usually the largest value that int can take on. This is likely much larger than the length of the array, but since you know the length of the array, you can use the modulus (%) operator to "squish" the random number into a range appropriate for indexing into the array.

rand() is an example of a pseudo-random number generator. It is a deterministic algorithm for generating seemingly (but not actually) random numbers. Pseudo-random number generators need a seed value which determines the start of the "random" sequence they will generate. If you start two generators utilizing the same algorithm with the same seed value, they should produce the same sequence of int values.

The function void srand(int seed) sets the seed for rand. To produce a truly random result, the seed ought to be draw from some "truly random" source of data. Frequently, the current time since 1 January 1970, i.e., the Unix Epoch, is used for this purpose. The time(time_t *result) function from the time.h header returns this value. The argument passed to time is a redundant result pointer that will be loaded with the result of the call to time. Since it is unnecessary, we can pass NULL, the pointer to nothing, to the function.

All that being said, to generate random numbers in our C program, include the following setup in your code:

#include <stdlib.h>
#include <time.h>

int main(int argv, char* argv []) {
    srand(time(NULL));  // set rand's seed to be the current time
    // go on to use rand() to generate random numbers...
}

Part 6: Read in a Complete Dictionary of Words

Finally, instead of a hard-coded dictionary, read the set of possible words from the following file:

This file contains five-letter words extracted from Collins Scrabble Words dictionary. Your program can assume that words5.txt exists in the same directory as the program. However, your program should report a sensible error and exit gracefully if words5.txt does not exist.

Currently, your dictionary is hard-coded to a finite set of words using an array literal. Now, you will need to create a list of strings of the appropriate size to hold all the words found in words5.txt. To do this, you can adapt the array-based list code from a previous lab to store these strings.

With this in mind, you can use fopen and fclose from stdio.h to open the file and then use fgetc to read individual characters of the file. You may assume for the purposes of this that each entry in the file is of the form of five characters and then a newline (\n) character. Note that you will need to also dynamically allocate memory for each string so that your dictionary is a list of pointers to heap-allocated strings that it owns after the dictionary has been loaded!

Extra Bits

At this point, the core of your Wordle game is roughly complete! However, there are many more things that you can add to it. While not required, here are some additions to your Wordle program for both functionality and fun that you can consider. If you include any of these features or others, please make sure to include a comment at the top of the file indicating what you have done.

Feature 1: forgiving prompt

Right now get_guess assumes that the user does "the right thing" and enters exactly five characters and a newline character for their guess. Relax this restriction so that the program re-prompts the user for a five-letter word if they enter in a word that is not exactly five characters long. To do this, you will need to correctly instrument get_guess with appropriate error checking, detecting the situations in which the user has not entered a valid input. You should then re-prompt the user, giving them an optional error message

While seemingly simple, this is fairly tricky functionality to get 100% right. Keep in mind that when entering input, the user will fill the console pipe with a number of characters ending in a newline character. You need to make sure to cover all the cases where execution exhausts part or all of this input, ensuring that subsequent prompts work as expected.

Feature 2: summary report

A memorable feature of Wordle is the summary of the game that the player can share with their friends. The summary consists of a number of rows, one for each guess the player made. Each row is simply the results of print_report for that guess, i.e., the guess information for each character---correct g, out of position y, or not in the target word x. Each character of this row should be colored as with print_report although if you went to copy-and-paste this text to share with a friend only the rows of gs, ys, and xs would remain.