Hello Again, Wordle!

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:

  1. Refactor your previous implementation to make use of datatypes.

  2. Implement a richer user interface that displays

    1. green, yellow, and gray colors to label letters of committed guesses,
    2. the current guess directly on the board (rather than at a separate prompt), and
    3. a colored display of all letters summarizing the guesses played so far.
  3. Add support for words with repeat letters.

1. Refactoring

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.

2. User Interface

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.)

2a. Colors

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.

2b. Current Guess

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.

If NoBuffering isn’t working on Windows, see here and here for potential workarounds.

2c. Visual Summary

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"]

3. Repeat Letters

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 Es), and yellow labels are assigned from left to right (see Ts).

Creativity (Optional)

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

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-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.

Cabal

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.

Coloring Letters

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 Cases

Some 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)

Sanity Check

Once you are finished, check that you added, committed, and pushed 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

Grading

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):