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).
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-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.
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
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
Image: Wikipedia
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: 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.
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 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.
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-fa23-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.