Interactive Play

As you consider interactive play, you’ll need to make a few additions to the program. Right now the game only returns the final score, but an interactive game should report progress as the game progresses. Also, right now you’re choosing the word to guess, but we really need to let the program choose a random word so it’s a mystery to a human player.

Let’s tackle the random word first. Included in the hangman project is a file words.txt, which contains about 4000 words that you can use as a word bank. First, read those words into a data structure.

 (​defn​ valid-letter? [c]
  (<= (int ​a​) (int c) (int ​z​)))
 
 (defonce available-words
  (with-open [r (jio/reader ​"words.txt"​)]
  (->> (line-seq r)
  (filter #(every? valid-letter? %))
  vec)))

The clojure.java.io namespace (aliased here to jio) has a number of helpful functions for interacting with the Java I/O library. Java has several I/O abstractions for different purposes. For example, streams represent binary streams of data and readers and writers are used for reading and writing character-based data. The jio/reader function will coerce many input sources into a Java reader.

Once you have the reader, you can break it into lines with line-seq, filter to keep only those that contain valid letters (omitting those with punctuation), and finally leave the final result in a vector. This is a typical sequence processing pipeline, tied together with the ->> thread-last operator.

Note that defonce is used here. defonce is a special wrapper for def that will prevent re-execution if this namespace is reloaded. This change avoids re-reading the word file, which is expensive. This mostly helps during development.

Now that you have a vector of valid words, you can easily pick a random one with rand-nth:

 (​defn​ rand-word []
  (rand-nth available-words))

Try it out:

 (repeatedly 5 rand-word)
 -> (​"sophisticated"​ ​"humor"​ ​"proclaim"​ ​"threshold"​ ​"obtain"​)

Next, let’s revisit our game function and add some printing to reveal the game progress. It’s common to add optional keyword arguments to the end of an invocation, so define a new :verbose option. Clojure supports destructuring the varargs sequential arguments as if they were a map for this purpose. It’s also good practice to declare defaults using the :or destructuring syntax.

Within the game loop, add a call to report progress when the verbose flag is set:

 (​defn​ game
  [word player & {:keys [verbose] :or {verbose false}}]
  (when verbose
  (println ​"You are guessing a word with"​ (count word) ​"letters"​))
  (loop [progress (new-progress word), guesses 1]
  (​let​ [guess (next-guess player progress)
  progress' (update-progress progress word guess)]
  (when verbose (report progress guess progress'))
  (​if​ (complete? progress' word)
  guesses
  (recur progress' (inc guesses))))))

Calling out to a function here keeps the reporting out of the main loop and makes the core loop code easier to read. The progress reporting looks like this:

 (​defn​ report [begin-progress guess end-progress]
  (println)
  (println ​"You guessed:"​ guess)
  (​if​ (= begin-progress end-progress)
  (​if​ (some #{guess} end-progress)
  (println ​"Sorry, you already guessed:"​ guess)
  (println ​"Sorry, the word does not contain:"​ guess))
  (println ​"The letter"​ guess ​"is in the word!"​))
  (println ​"Progress so far:"​ (apply str end-progress)))

And finally, you need to create the interactive player, which will accept guesses interactively from the player:

 (​def​ interactive-player
  (reify Player
  (next-guess [_ progress] (take-guess))))

Like the random-player, no state is being used here, so you can just define a single interactive-player to use that does nothing more than defer to a function take-guess that interacts with the console.

Clojure provides access to the stdin input stream via the *in* dynamic variable, which will be an instance of java.io.Reader. Here’s the full code:

 (​defn​ take-guess []
  (println)
  (print ​"Enter a letter: "​)
  (flush)
  (​let​ [input (.readLine *in*)
  line (str/trim input)]
  (​cond
  (str/blank? line) (recur)
  (valid-letter? (first line)) (first line)
  :else (do
  (println ​"That is not a valid letter!"​)
  (recur)))))

Start by printing the instructions to the user using print, which will not print a newline character at the end but will instead wait for the user’s response. Next, call flush to force the buffered output stream to print so the user will see it. Then you’re ready to read the user’s input from the input stream—just call readLine via Java interop.

Once the user hits enter, you can consider their response. If the line is blank, you can recur back to the top of the function (remember that functions serve as implicit loop targets) to try again. If the response starts with a valid letter, return that. And if the letter is invalid, notify the user and try again. Now you can put all this together and play a game yourself.

 (game (rand-word) interactive-player :verbose true)
 
 You are guessing a word with 4 letters
 
 Enter a letter​:​ a
 The player guessed​:​ a
 The letter a is in the word!
 Progress so far​:​ _a__
 
 Enter a letter​:​ e
 The player guessed​:​ e
 The letter e is in the word!
 Progress so far​:​ ea__
 
 Enter a letter​:​ c
 The player guessed​:​ c
 Sorry, the word does not contain​:​ c
 Progress so far​:​ ea__
 
 Enter a letter​:​ s
 The player guessed​:​ s
 The letter s is in the word!
 Progress so far​:​ eas_
 
 Enter a letter​:​ t
 The player guessed​:​ t
 Sorry, the word does not contain​:​ t
 Progress so far​:​ eas_
 
 Enter a letter​:​ y
 The player guessed​:​ y
 The letter y is in the word!
 Progress so far​:​ easy
 -> 6

Seems like the interactive player works. Next let’s consider how we can use specs to document and test the program.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.228.99