PA 2 — Due: Fri Oct 13 11pm CT
Recall the academic integrity policy: turn in your own work and cite all sources.
In this assignment, you will build upon your previous Wordle game to make use of additional Haskell features you have learned and to make the game actually fun(ctional) to play. Specifically, you will:
Refactor your previous implementation to make use of datatypes.
Implement a richer user interface that displays
Add support for words with repeat letters.
Start this assignment with your previous implementation.
Note: Because this is part two in a pair of consecutive assignments, you may not receive detailed feedback on your PA 1 submission before working on this assignment. Sorry about that.
Start by defining new types…
data Model =
... -- fill this in
data Action =
KeyPress Char
and refactor your implementation to fit the following cleaner model-view-controller interface:
main :: IO ()
controller :: Model -> IO Model -- new type signature
update :: Action -> Model -> Model -- previously maybeUpdateBoard
view :: Model -> IO () -- previously printBoard
Note: Defining a single Model
type with all application data, a single Action
type for
all possible user actions and other events (even when there is only one
action, as here), and controller
, update
, and
view
functions with these signatures is one straightforward
architecture for interactive applications in general.
What should go in Model
?
Everything that is needed for update
to update the game
state in response to user input and for view
to draw the
corresponding user interface. Consider all of the arguments that were
passed to maybeUpdateBoard
, printBoard
, and
their helper functions in the original architecture.
As described below, your new user interface will draw letters on the
board immediately upon entry, rather than waiting for an entire line to
be entered via getLine
as before. As you implement that
functionality, assess what additional information needs to be tracked in
the Model
.
Your game should support a user interface with three new characteristics described below.
Unlike the previous assignment, there is no fixed input/output format used for testing and grading. Instead, you are free to design the interface however you like subject to the requirements. Here is an example video of how things could look:
(Here is a similar demo showing dark-on-light but without the required visual summary of letters.)
Use green, yellow, and gray colors to label letters of committed guesses.
Use ANSI
escape codes to change terminal styles — look for codes to set
foreground/background colors, clear the screen, etc. To get the gist of
how these work, check out this handy
gist. (Note: On Windows, you might need to
write something like "\
x1b
"
rather than "\ESC"
.)
(Alternatively, you could choose to use a library, such as System.Console.ANSI
,
to abstract over lower-level details, but this is not necessary.)
Ensure that the user interface is legible both on dark-on-light and light-on-dark terminals.
If it may help with your visual needs, check whether your operating system settings provide more accessible color palettes.
In the previous assignment, the user’s current guess appeared at
Next guess?
prompt underneath the board of previous
guesses.
This time, letters should be added to and removed from the board directly, as in the original game. Allow letters to be typed in lowercase or uppercase. Make sure to ignore non-letter characters, and to limit guesses to five letters.
The visual effect should be that the board is updated in place. However, you can simulate this effect simply by by clearing the screen, or displaying a block of blank lines, on every keystroke. No need to manipulate the cursor position, erase characters, etc. (Though if you really want to implement in-place updates faithfully, you might look into ncurses.)
This simple “flipbook” approach may result in some flickering (as in the demo above). For our purposes, this is fine — our game may be purely functional but not totally functional (or totally functional).
To redraw the interface on every keystroke, you may need to use NoBuffering
instead of LineBuffering
as in the previous assignment.
Check that there is still a way to quit/exit the program.
Your interface should also visually depict a keyboard, as shown at
top-right from the original
game. Unlike in the web-based game, the keys won’t be clickable. But
like the original game, the visual keyboard should serve as a
summary of all words committed so far. For letters that
have been displayed on the main board in multiple colors, pick the
largest one: green > yellow > darkgray. Hint: What type class would be
relevant to compare Color
values?
If you’d like to display an alternative keyboard layout than QWERTY, go ahead and represent. You are also free to choose a non-keyboard design, in which case just make sure that all 26 letters are displayed.
Hint: Use an association list of type [(Char, Color)]
to
track each letter. You can maintain it in the Model
, or
compute it every time it is needed in view
.
Hint: Recall mapIO_
,
which may be handy for iterating over the rows of a keyboard:
qwertyRows = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
How should tiles be colored when the guessed word and/or the goal word have repeated letters? It’s a bit subtle. Consider the following examples stolen from this video:
One more example to consider:
Notice that green takes precedence even when the same letter appears
earlier in the guess (see E
s), and yellow labels are
assigned from left to right (see T
s).
If you want to challenge yourself and have more fun(ctional), spice up your user interface. Consider incorporating some text- and color-based animations. Be creative!
A Light in
the Attic, “Creative”
Shel
Silverstein
See Ed for the GitHub Classroom assignment link, which will create a UChicago-PL repository like
https://github.com/UChicago-PL/cs223-fa23-pa-2-hello-again-wordle-USERNAME
containing several files. You can start either with the given
Wordle.hs
file and then selectively copy pieces of code
from before, or copy the entire previous Wordle.hs
file
into this repository and edit.
This time, the repository does not contain a Makefile
that uses ghc
directly to build the executable. Instead, we
will use a system called Cabal, which is a build system
and package manager for Haskell. (If you didn’t take Cabal for a test
drive before, do it now.)
Your repository contains a wordle.cabal
file that looks
like:
cabal-version: 2.4
name: wordle
version: 0.1.0.0
executable wordle
main-is: Wordle.hs
build-depends: base,
random
hs-source-dirs: .
default-language: GHC2021
This .cabal
file specifies any and all packages that
your program depends on — base
includes Prelude
and other standard libraries, and random
includes System.Random
. If you choose to use additional
libraries (such as System.Console.ANSI
, mentioned above),
add the relevant packages to the build-depends
entry.
Running…
% cabal build
… will generate an executable somewhere deep within a directory like
dist-newstyle/build/
. You can dig around and find it, but
much simpler is to run run
(which will
re-build
if needed):
% cabal run
You can also run
your executable by name…
% cabal run wordle
… and provide command-line arguments to it after the
“--
” characters:
% cabal run wordle -- 1898
Another example (compared to the image above, notice we are invoking the executable via Cabal):
To clean up, run:
% cabal clean
If you decide to use any additional language extensions, such as
those involving record
syntax, you will need to add (and add
and
commit
and push)
an extensions
entry to your wordle.cabal
file.
Check out the Cabal User Guide if and when you need more than the basics above.
You are strongly encouraged to define and use the following helper function:
data Color = None | Gray | Yellow | Green
deriving (Eq, Ord)
colorLetter :: (Char, Color) -> String
This function can be a small helper that takes a letter
(e.g. 'a'
or 'A'
) and handles all aspects of
its rendering on the board — it produces a multi-character string with
(i) all necessary escape codes for styling that (and only that) letter
and (ii) leading and trailing spaces.
getChar
: Funny Corner
CasesSome of our fellow functional programming alumni found an interesting
nit: getChar
can produce more than one
Char
:
> :t getChar
getChar :: IO Char
ghci> getChar -- Type `a` after this action. All is well.
a'a'
ghci> getChar -- Now type left-arrow. Huh? Two characters?
^[[D'\ESC'
I’m not exactly sure what’s going on here, and I don’t care to learn
about the nitty-gritty I/O details at the moment. You don’t have to care
either: simply use isDigit
or isLetter
to
check the resulting Char
from getChar
, and
don’t worry about these funny corner cases; we won’t test them.
But, if you would like to make your game a little more robust
(grandma will certainly mash all the keys), drop in the following
getKey
action in place of getChar
(the return
type is different, so you’ll have to manipulate the result slightly
differently):
getKey :: IO [Char]
getKey = do
str <- getKey' ""
return (reverse str)
where
getKey' chars = do
char <- getChar
more <- hReady stdin
(if more then getKey' else return) (char:chars)
Once you are finished, check that you add
ed,
commit
ted, and push
ed all relevant files to
your repository before the deadline (or within an eligible late
period):
https://github.com/UChicago-PL/cs223-fa23-pa-2-hello-again-wordle-USERNAME
Make sure your code compiles. This assignment will be graded primarily by manually testing your game. Subsequent assignments will also be graded for style.
Here is an outline of the grading rubric (10 points total):
2
Architecture
0.5
Model
type1.5
Required type signatures for
controller
, update
, and view
8
Gameplay
0.5
Command-line arguments1.0
Letters can be added and deleted from board0.5
Input handling: Preventing non-letter characters,
more than 5 letters0.5
Error messages: Not enough letters, not in word
list0.5
Messages are displayed only temporarily0.5
User can win or lose appropriately0.5
Letters are displayed in upper case1.0
Letters are colored appropriately1.5
Repeat letters colored correctly1.5
Visual keyboard colors0
Clean code