PA 1 — Due: Fri Jan 13 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).
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
A
”) for in the word and the correct spot, a
”) for in the word but in the wrong spot, and[a]
”) for not in the word in any spot.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.
Compared to the original game, we will make two simplifications.
No Repeat Letters: Ensure that there are no repeated letters in either guessed or goal words.
No Visual Keyboard: The original game displays a keyboard depicted visually, with colors to summarize previous guesses. This feature is not included in this assignment.
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.
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
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
See Ed for the GitHub Classroom assignment link, which will create a UChicago-PL repository like
https://github.com/UChicago-PL/cs223-wi23-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.
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.
Use the following from System.Environment
to read command-line arguments:
getArgs :: IO [String]
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.
Use the following function from System.Random
to generate random integers within a given range:
randomRIO :: (Int, Int) -> IO Int
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
If a successful goal word is obtained during initialization, main
should kick off a model-view-controller (MVC) gameplay loop.
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.
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
A view is the user interface to display. We have three potential elements to display given the state of the game.
First, a str
ing that prompts the user for a guess:
printPrompt :: String -> IO ()
printPrompt str =
undefined
Second, a (potentially-empty) caption in response to the previous guess.
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 guess
es and each guess
consists of five letter
s:
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: Consider a function called mapIO
, without an underscore. What might be its type and definition?
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):
10
Correctness
1
Command-line arguments2
Initialization with and without game number1
User can enter and delete characters, can enter a valid guess2
Gameplay prompt and messages displayed correctly3
Letters on board are annotated correctly1
User can win or lose appropriately00
Clean code
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 input-1.txt | ./wordle 0 > output-1.txt
% diff output-1.txt 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 > output-4.txt
% diff output-4.txt 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 > output-5.txt 2>&1
% diff output-5.txt 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.
sqrt (5 * 2)
not sqrt(5 * 2)
.i
for index, x:xs
for generic list elements, n
for generic number, c
for counter, etc. Abbreviations are fine too, but the rule is a reader should know the meaning of the thing just by reading its name.where
and let
.Once you are finished, check that you add
ed, commit
ted, and push
ed 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-wi23-pa-1-hello-wordle-USERNAME
When formatting long strings (such as the how-to-play instructions), consider using the syntax for multi-line string literals.
In this assignment, we are supporting very simple command-line arguments via getArgs
. In future projects, you might look into libraries such as System.Console.GetOpt
or other command-line option parsers that provide more full-featured support.
Similar to undefined
, the following function from Prelude
, to crash with an error message, may sometimes be useful during development:
error :: {- forall a. -} String -> a
Note: The actual type for error
is more complicated than the above.