Hello, Wordle!

PA 1 — Due: Fri Oct 6 11pm CT

Recall the academic integrity policy: turn in your own work and cite all sources.

I managed to avoid Wordle for a couple months at first. But then NPR featured it and the New York Times bought it, so I had no alternative left but to check it out. If you haven’t, wriggle into a comfy chair, grab an ergonomic keyboard, and let the game “tickle your lexicon bone”… It’s a worldle of fun!

Your mission (which you are required to accept) is to implement a Wordle clone. Basic gameplay will follow the original, but compared to the nice web version ours will operate through a simple standard input/output user interface.

% ./wordle --how-to-play

HOW TO PLAY

Guess the WORDLE in 6 tries.

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

Examples

  W [e][a][r][y]  The letter W is in the word and in the correct spot.
 [p] i [l][l][s]  The letter I is in the word but in the wrong spot.
 [v][a][g][u][e]  None of the letters are in the word in any spot.

The interactive gameplay loop will start like this:

% ./wordle

###################
##               ##
##               ##
##               ##
##               ##
##               ##
##               ##
###################

Guess the wordle!
>

Then, after several incorrect guesses…

Next guess?
> ...
...

Next guess?
> ...
...

...

… it will look like this upon winning:

Next guess?
> debug

###################
##[c][r][a][t] e ##
##[s][p][o][i][l]##
##[f] u [n][k][y]##
## D  E  B  U [t]##
## D  E  B  U  G ##
##               ##
###################

Great!

The starter code (described below) includes input/output text files that show more detailed example interactions (also described below).

Gameplay

The playing board should consist of six rows, each with five square tiles (one letter per tile).

The user fills in the next empty row by typing characters. Letters should be entered lowercase, such as 'a'. The backspace key should remove the last letter in the current row (see hSetBuffering). The enter key should “submit” the guess.

If a submitted word is invalid (not six alphabetical letters, not in the word list, and so on), display an appropriate message (described below) and allow the current row to continue to be edited.

If a submitted word is valid, then “commit” the word by displaying each of its letters as follows: rather than the original green, yellow, and gray colors, display

Notice that all letters rendered with three characters so that they line up nicely on the board.

A game is over when a committed word matches the goal or when six incorrect words have been committed. In either case, display an appropriate message.

Simplifications

Compared to the original game, we will make two simplifications.

Displaying Messages

There are several messages to show during gameplay:

Messages should be displayed with precedence according to the order above. For example, if the user enters “Haskell” the message Too many letters should be displayed.

Your messages should be identical to these, and displayed in the exact locations shown in the sample interaction logs.

Words

The starter code includes the word list plucked from Wordle. Note that this list includes words with repeat letters. Your program should read in this list from the file words.txt, in the same directory as the executable. See System.IO.readFile :: FilePath -> IO String.

The game number will be a (0-based) index into the full word list.

When run with no command-line arguments, your program should randomly pick a game number corresponding to a word with no repeat letters.

When run with a single command-line argument, treat that argument as the game number. That way, a player (or homework grader) can choose a specific game to play (or test):

% ./wordle 0

If the argument is not a valid (game) number, print an error and exit:

% ./wordle zero
Invalid game number
% ./wordle 123456789
Invalid game number

If the chosen word has repeat letters, print an error and exit:

% ./wordle 2107
CURRY has repeat letters

Command-Line Arguments

In addition to optional game number, support the following optional command-line flag to display gameplay instructions (see top of the page for reference):

% ./wordle --how-to-play

Your program should allow at most one of the optional command-line arguments. Otherwise, print the following usage error message and exit:

% ./wordle 0 --how-to-play
Usage:

  ./wordle                  Play random game
  ./wordle gameNumber       Play specific game
  ./wordle --how-to-play    Display instructions
% ./wordle 1 1 2 3 5
Usage:

  ./wordle                  Play random game
  ./wordle gameNumber       Play specific game
  ./wordle --how-to-play    Display instructions

Starter Code

See Ed for the GitHub Classroom assignment link, which will create a UChicago-PL repository like

https://github.com/UChicago-PL/cs223-fa23-pa-1-hello-wordle-USERNAME

containing several starter files.

Start with Wordle.hs and compile it into an executable program:

% ghc -o wordle Wordle.hs
% ./wordle

(You can also run make. See the Makefile.)

The starter code includes a suggested architecture, described below. Look for occurrences of undefined and replace them with your definitions. You are encouraged to define and use additional helper definitions where appropriate.

Initialization

The entry point, main, will (conditionally) check command-line arguments, read the words file, generate a random number, and kick off the gameplay loop.

main :: IO ()
main = do
  hSetBuffering stdin LineBuffering
  undefined

Notice the command hSetBuffering stdin LineBuffering, which sets up the Backspace key to actually delete the previously entered key from the input buffer.

Command-Line Arguments

Use the following from System.Environment to read command-line arguments:

getArgs :: IO [String]

Errors

Use the following function from System.Exit to print an error message and exit:

die :: String -> IO a

Note: Similar to the type of undefined, the output type forall a. IO a allows this “exception” to be a well-typed expression in any context.

Note: Soon we will learn much better ways to deal with errors besides die-ing or error-ing.

Random Numbers

Use the following function from System.Random to generate random integers within a given range:

randomRIO :: (Int, Int) -> IO Int

Wait, IO… why ?!?

Well, random number generation cannot be implemented as a pure function — there must be some other interaction (“effect”) at play. Clearly the number produced by randomRIO should not be the result of input/output with the user. However, we can generalize how we understand the IO type to be describing interactions with the user or their machine.

Through that lens, we can vaguely imagine how randomRIO — when given access to the file system, system clock, etc. — can produce truly (pseudo) random numbers. We will work with random numbers in much more detail later in the course.

Note: The actual type is more general than what is written above.

Note: You will probably need to install the random package on your machine. Running…

% cabal install random
...
Without flags, the command "cabal install" doesn't expose libraries in a
usable manner. You might have wanted to run "cabal install --lib random".

… may suggest that you run the following instead:

% cabal install --lib random

I/O Loop


Image: Wikipedia

If a successful goal word is obtained during initialization, main should kick off a model-view-controller (MVC) gameplay loop.

Model

A model is everything that needs to be tracked about the current state of an application.

For our Wordle game, that means the word list, the answer word, the board of previous guesses, possibly a caption based on the last guess, and the new guess currently being entered.

Controller

A controller listens for user events and responds by updating the model and/or view.

Define controller to be a recursive function with the following signature:

controller :: [String] -> String -> [String] -> IO ()
controller wordList answer board =
  undefined

Define the following (non-recursive) helper function to update the existing board based on the user’s current guess:

maybeUpdateBoard
  :: [String] -> String -> [String] -> String -> (String, [String])
maybeUpdateBoard wordList answer board guess =
  undefined

The first output value is a (potentially-empty) string caption to display, in response to guess. The second output value is a (maybe) updated board.

You can use the following function from Data.List

elemIndices :: Char -> String -> [Int]

to find all occurrences of a character within a string. (Again, the actual type of the function is more general than what is written above for the purposes of this assignment.)

Although algebraic datatypes (ADTs), such as Maybe, are beyond the scope of this first assignment, if you prefer you could use the related Data.List function…

elemIndex :: Char -> String -> Maybe Int

to check whether a given character appears or not as follows:

> elemIndex 'a' "abc" == Nothing
False
> elemIndex 'A' "abc" == Nothing
True

View

A view is the user interface to display. We have three potential elements to display given the state of the game.

A Prompt

First, a string that prompts the user for a guess:

printPrompt :: String -> IO ()
printPrompt str =
  undefined

A Caption

Second, a (potentially-empty) caption in response to the previous guess.

The Board

Finally, the board:

printBoard :: String -> [String] -> IO ()
printBoard answer board =
  undefined

For this, consider implementing the following helper functions, where board consists of zero to five guesses and each guess consists of five letters:

printGuess :: String -> String -> IO ()
printGuess answer guess =
  undefined

printLetter :: String -> (Int, Char) -> IO ()
printLetter answer (i, letter) =
  undefined

Consider also implementing the following helper function:

mapIO_ :: (a -> IO b) -> [a] -> IO ()
mapIO_ f as =
  undefined

Thought Exercise: Though it may not be needed in this assignment, consider also:

mapIO :: (a -> IO b) -> [a] -> IO [b]
mapIO f as =
  undefined

Notice the similarities and differences between the definitions above, with and without an underscore. Later on, we will see this kind of naming convention in some libraries.

Grading

Make sure your code compiles. The vast majority of your grade will be determined by automated tests for correctness. Subsequent assignments will also be graded for style.

Here is an outline of the grading rubric (10 points total):

Example Interactions

Here are a few example gameplay logs:

command answer (std) input + output just input just output
1 ./wordle 0 cigar input-output-1.txt input-1.txt correct-output-1.txt
2 ./wordle 1898 debug input-output-2.txt input-2.txt correct-output-2.txt
3 ./wordle 748 index input-output-3.txt input-3.txt correct-output-3.txt

For each row N, the input-output-N.txt file shows a complete interaction log. This is exactly what you should see in the terminal when playing your game with the given user inputs.

Each file input-N.txt contains only the input entered by the user, and correct-output-N.txt shows only the output generated in response to the user inputs. These two files can be used to check that your game produces exactly what is expected. For example, run…

% cat testing/input-1.txt | ./wordle 0 > testing/output-1.txt
% diff testing/output-1.txt testing/correct-output-1.txt

… and check that there are no character differences. (You might choose vimdiff or some other tool that displays nicer, more visual diffs.) The starter Makefile defines a few simple abbreviations:

% make test-1
% make test-2
% make test-3

In addition to these three example gameplay logs, here are the remaining interactions described above for situations in which a game is not initiated:

command stdout stderr
4 ./wordle --how-to-play how-to-play.txt
5 ./wordle zero invalid-game-number.txt
6 ./wordle 123456789 invalid-game-number.txt
7 ./wordle 2107 curry.txt
8 ./wordle 0 --how-to-play usage.txt
9 ./wordle 1 1 2 3 5 usage.txt

An example involving standard input:

% ./wordle --how-to-play > testing/output-4.txt
% diff testing/output-4.txt testing/how-to-play.txt

And an example involving standard error; notice that stderr (i.e. file descriptor 2) is being redirected to standard input (file descriptor 1):

% ./wordle zero > testing/output-5.txt 2>&1
% diff testing/output-5.txt testing/invalid-game-number.txt

The Makefile provides a quick-and-dirty way to run all tests above. (Eventally we will do a better job writing tests. For now, let’s be happy writing functional programs and dysfunctional test harnesses.)

% make tests

You can edit Makefile and/or add, commit, and push additional test files if you wish. But only Main.hs will be considered for grading.

Note: All files in testing/ are for testing; your implementation should not depend on them.

Style Guidelines

Sanity Check

Once you are finished, check that you added, committed, and pushed everything — in this case, just Wordle.hs — to your repository before the deadline (or within an eligible late period):

https://github.com/UChicago-PL/cs223-fa23-pa-1-hello-wordle-USERNAME

Miscellaneous