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.
stubs.gameLoop : Nat
-> stubs.Target
-> [[stubs.Result]]
->{IO, Exception, Ask (Set Text)} ()Load the dictionary file
Thus far we've been assuming that a bunch of five letter words represented by a Set of Text will 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 the words 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 the main function
Your main function is what the ucm run command 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 Discord channel so the Unison crew and others can procrastinate play it! π
stubs.gameLoopstubs.gameLoopstubs.gameLoop : Nat
-> stubs.Target
-> [[stubs.Result]]
->{IO, Exception, Ask (Set Text)} ()Imagine we call stubs.gameLoop with the following named arguments: stubs.gameLoop turns target results.
turnsis aNatrepresenting the number of guesses a user has hadtargetis the word of the dayresultsis the history of the user's guesses, for rendering purposes- The
Askability 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.
IO abilityIO abilityTo perform a file read we'll need readFileUtf8 function from base.
readFileUtf8 : FilePath ->{IO, Exception} TextFilePathis the type representing directories and files. It's constructed from aTextvalue representing the path:FilePath "myFile.txt"- The
readFileUtf8function reads the contents of a file as aTextvalue but you can read bytes withFilePath.readFile.
Ask abilityAsk abilityThe handler for Ask is called provide.
When functions call ask for a value in the environment, it makes sense that the value they should get is supplied by provide.
The second argument to provide is a delayed computation.
Note that provide 42 '(myAskFunction a b) is different from provide 42 'myAskFunction a b. Read more about this difference here.
To share your Unison source code, push your code to your Unison Share and set its visibility to public in the Share UI.
wordle/main> pushThe push command with no arguments will push the current namespace to a correspondingly named remote repository on Unison Share.
You can also create a standalone executable file from your main function for other folks to run with the compile command in the UCM. Read more about standalone binaries here
Resources
Solutions
basic.gameLoop implementationbasic.gameLoop implementationbasic.gameLoop is 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 :
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 resultsbasic.loadDict implementationbasic.loadDict implementationstubs.loadDict loads the Text from 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 = xml.lib.base.Text.split ?\n text |> Set.fromList
printLine "π Dictionary created."
t
else
Exception.raise
(Generic.failure "Dictionary file not found" filePath)basic.readFile is the function which handles the file reading as described above. Inside basic.readFile is a recursive function calling getBytes in 4096 byte chunks at a time (the size of a page of memory in most OS architectures).
basic.readFile : FilePath -> '{IO, Exception} Textbasic.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 _ = FilePath.open filePath FileMode.Read
do bracket fileHandle Handle.close (file -> read file |> fromUtf8)basic.main function implementationbasic.main function implementationbasic.main is 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 the Ask ability. The handler for Ask is provide, which provides the set of five letter words as its first argument to the basic.gameLoop function which requires it.
basic.main : '{IO, Exception} ()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 do
basic.gameLoop 0 (basic.Target.fromText target) [[]]