🕹 Game loop state and dictionary

Keep track of game state

There are a few final features to complete our Wordle clone. We need to limit the user to six guesses and read a file representing the dictionary.

First try to write a function,stubs.gameLoop,which does the following:

  • Gets the user's guess from the console
  • Renders the result of the guess with the user's previous guesses
  • Lets the user know if they have guessed successfully or run out of attempts.

Load the dictionary file

Thus far we've been assuming that a bunch of five letter words represented by aSetofTextwill be provided as an argument to our functions. We still need to write the function which generates this data. To do that we'll read in a file.

Try to write a function,stubs.loadDict,which returns the words in thewords file.Each five letter word is separated by a newline character""For the sake of convenience you might want to place it in the same folder where you're calling the UCM from.

Write themainfunction

Yourmainfunction is what the ucmruncommand will execute. It should do the following:

  • Create the target word (hard-coding it is fine for now)
  • Read the dictionary file
  • Kick off the game loop 🍾

When you're done post your wordle in the#beginner-friendly slack channelso the Unison crew and others canprocrastinateplay it! 😁

See a breakdown of each of the arguments instubs.gameLoop
See a breakdown of each of the arguments instubs.gameLoop

Imagine we callstubs.gameLoopwith the following named arguments:stubs.gameLoop turns target results.

  • turnsis aNatrepresenting the number of guesses a user has had
  • targetis the word of the day
  • resultsis the history of the user's guesses, for rendering purposes
  • TheAskability requirement can be thought of as an environment which provides the dictionary

Think about which of these are tracking state which should change based on the user's input.

📚How to read files using theIOability
📚How to read files using theIOability

To perform a file read we'll need theFilePath.exists,open,getBytes,andHandle.closefunctions frombase,in roughly that order.

  • FilePathis the type representing directories and files.
  • FileModedescribes the possible interactions we can have with the file—here we'll probably just need theReadmode.
    • When you open a file, the reference that is created for you is aHandle(file handle).

To write thestubs.loadDictfunction, you'll want to pay special attention to what happens when we callgetByteson the same file handle multiple times. Additionally, consider what happens to the file handle if reading the file fails.

UnderstandgetByteswith a simple test! 🧪
UnderstandgetByteswith a simple test! 🧪

Create a file called "test" in your working directory. "test" should contain one ten letter word. (I like to use "strawberry" 🍓)

ThefileTestfunction calls getBytes exactly twice, each time taking 5 bytes (half the length of the word).

fileTest : '{IO, Exception} ()
fileTest _ =
  filePath = FilePath "test"
  fileHandle = open filePath Read
  performRead fileHandle =
    use Text ++
    first = getBytes fileHandle 5
    second = getBytes fileHandle 5
    text = first |> fromUtf8 ++ (second |> fromUtf8)
    printLine text
  bracket 'fileHandle Handle.close performRead

runthefileTestfunction to see what gets printed to the console.

📚Handling theAskability
📚Handling theAskability

The handler forAskis calledprovide.

provide : a -> '{g, Ask a} r ->{g} r

When functions callaskfor a value in the environment, it makes sense that the value they should get is supplied byprovide.

The second argument toprovideis a delayed computation.

Note thatprovide 42 '(myAskFunction a b)is different fromprovide 42 'myAskFunction a b.Read more about this difference here.

📚Packaging and sharing Unison code
📚Packaging and sharing Unison code

To quickly share your Unison source code, you can use thegistcommand.Gist pushes everything in a given namespace to a remote repository and returns a pull''command representing that namespace to share with other people.

.> cd my.desired.namespace
my.desired.namespace> gist remoteRepo

🎥Gist command walk through

You can also create a standalone executable file from your main function for other folks to run with thecompilecommand in the UCM.Read more about standalone binaries here

Resources

Solutions

🔑basic.gameLoopimplementation
🔑basic.gameLoopimplementation

basic.gameLoopis a recursive function which determines if the user has won, keeps track of the state of the user's turn, and prints the user's past guesses.

basic.gameLoop :
  Nat
  -> basic.Target
  -> [[basic.Result]]
  ->{IO, Exception, Ask (Set Text)} ()
basic.gameLoop turn target pastGuesses =
  use List :+
  use Nat + >=
  if turn >= 6 then printLine "Alas! Better luck next time! 🤡"
  else
    guess = !basic.getUserInput
    result = basic.Guess.score guess target
    results = pastGuesses :+ result
    printLine (basic.renderGuesses results)
    if basic.isVictory result then printLine "🎉 Yay! 🎊"
    else basic.gameLoop (turn + 1) target results
🔑basic.loadDictimplementation
🔑basic.loadDictimplementation

stubs.loadDictloads theTextfrom the dictionary file as raw text, then splits it on the new line character.

basic.loadDict : FilePath -> '{IO, Exception} Set Text
basic.loadDict filePath _ =
  if FilePath.exists filePath then
    printLine "📚 Dictionary found! This may take a second... ⏰"
    text = ((<|) basic.readFile filePath ())
    t = lib.base.Text.split ?\n text |> lib.base.data.Set.fromList
    printLine "📗 Dictionary created."
    t
  else
    Exception.raise
      (Generic.failure "Dictionary file not found" filePath)

basic.readFileis the function which handles the file reading as described above. Insidebasic.readFileis a recursive function callinggetBytesin 4096 byte chunks at a time (the size of a page of memory in most OS architectures).

basic.readFile : FilePath -> '{IO, Exception} Text
basic.readFile filePath =
  read : Handle ->{IO, Exception} Bytes
  read fileHandle =
    go acc =
      use Bytes ++
      use Nat <
      bs = getBytes fileHandle 4096
      if Bytes.size bs < 4096 then acc ++ bs else go (acc ++ bs)
    go Bytes.empty
  fileHandle : '{IO, Exception} Handle
  fileHandle _ = open filePath Read
  '(bracket fileHandle Handle.close (file -> read file |> fromUtf8))
🔑basic.mainfunction implementation
🔑basic.mainfunction implementation

basic.mainis our "edge of the world 🗺" function. We've hard-coded the file path to a dictionary file, and hard-coded the target word for this iteration of the code.

It's here that we provide a handler for theAskability. The handler forAskisprovide,which provides the set of five letter words as its first argument to thebasic.gameLoopfunction which requires it.

basic.main : '{IO, Exception} ()
basic.main _ =
  printLine "🥳 Welcome to WORDLE!"
  filePath = FilePath "dict"
  dict = basic.loadDict filePath ()
  target = "party"
  if Boolean.not (Text.size target === 5) then
    Exception.raise
      (Generic.failure
        "The word of the day must be 5 letters long" target)
  else
    printLine "Guess a five letter word"
    provide dict '(basic.gameLoop
        0 (basic.Target.fromText target) [[]])

Next steps

👉Check out the challenge task for this lab

👉Explore other ways to extend this code