POSTS

Guess the Number, Part 5 - letting the player guess with cond and keywords

In this final part, we’ll finish the game by adding a second game mode which lets the player guess a number that the computer has thought of. We’ll also allow the player to choose between the two game modes. Along the way, we’ll do some reorganising of our code and learn a few new forms.

Code cleanup

So far, we’ve done all our work in the -main function of core.clj. To make it easier to add new features, such as the second game mode, let’s extract the code into separate functions:

(ns guessnumber.core)

(defn get-input []
  (print "> ")
  (flush)
  (Integer/parseInt (read-line)))

(defn pc-guess []
  (println "Think of a number between 0 to 99 and I'll try to guess it!")
  (loop [minimum 0
         maximum 99]
    (let [guess (int (+ minimum (/ (- maximum minimum) 2)))]
      (println "My guess is:" guess)
      (println "1. Bigger")
      (println "2. Smaller")
      (println "3. That's correct!")
      (condp = (get-input)
        1 (recur guess maximum)
        2 (recur minimum guess)
        3 (println "All right!!!")
        (do (println "I didn't understand your answer...")
            (recur minimum maximum))))))

(defn -main []
  (pc-guess))

Basically, we’ve taken all the code in -main and placed it in a new function called pc-guess. Then, we’ve extracted the code that queries the player for some input, calling the function get-input. We then call pc-guess from our -main function to start the game.

Now we’re ready to tackle the new game mode.

Deciding on the number

Our first step is to have the computer come up with a number for the player to guess. A simple way to do this is to generate a random number. Clojure offers a simple function, rand-int, that allows us to produce a random number anywhere from 0 to a given maximum. In order to set the maximum to 99, we have to pass in 100 as the parameter:

> (rand-int 100)
9
> (rand-int 100)
49
>

Handling different conditions with cond

In an earlier tutorial, we made use of the condp form for handling different choices from the player. To recap, condp allows us to provide a predicate function, as well as a set of clauses. Each clause has a test expression which will be passed into the predicate to decide whether the result expression will be executed.

This time round, the player’s input will no longer be in the form of menu choices. Instead, the player will try to guess the actual number the computer is thinking of, in the range of 0 to 99. To accept this input, we’ll need to handle three different situations using separate predicate functions:

  1. Player’s guess is less than answer, determined with the < function.
  2. Player’s guess is greater than answer, determined with the > function.
  3. Player’s guess is the right answer, determined with the = function.

Since the condp form only accepts a single predicate function, we’ll need to use the more flexible cond form:

cond
clojure.core

  (cond & clauses)

Takes a set of test/expr pairs. It evaluates each test one at a time. If a test
returns logical true, cond evaluates and returns the value of the corresponding
expr and doesn't evaluate any of the other tests or exprs. (cond) returns nil.

Let’s try it out on the REPL:

> (cond
   (> 1 2) "1 is greater than 2"
   (< 4 2) "4 is lesser than 2"
   :else "Your computer seems to be working fine")
"Your computer seems to be working fine"

As mentioned in the documentation, cond evaluates the test form in each clause. In this case, the test forms are (> 1 2), (< 4 2) and :else. The first two forms are obviously false, but why does the REPL return the result of the third expression, :else? And why is there a : in front of else anyway?

A little more about keywords

:else is a keyword. In Clojure, keywords are identifiers which evaluate to themselves:

> :my-keyword
:my-keyword
> :another-keyword
:another-keyword
> :else
:else

Keywords are used throughout Clojure and Clojure libraries in place of strings whenever efficient comparisons are needed. This StackOverflow thread has some more explanation on the difference between keywords and symbols.

Let’s get back to the cond statement. The reason that cond returns the result associated with :else is that :else is considered a true value by Clojure. Technically, we could use :catchall or any other keyword or even a string in place of :else since they are all considered true, but :else is the convention in Clojure for a catch-all test condition.

Finishing the second game mode

We can place all the code needed for our second game mode in a new function, player-guess, which we’ll add to core.clj:

(defn player-guess []
  (let [answer (rand-int 100)]
    (println "I've thought of a number between 0 to 99... can you guess it?")
    (loop [guess (get-input)]
      (cond
       (< answer guess) (do (println "Smaller!") (recur (get-input)))
       (> answer guess) (do (println "Bigger!") (recur (get-input)))
       (= answer guess) (println "You got it!")))))

Then, modify the -main method to call player-guess instead:

(defn -main []
  (player-guess))

Note that you need to make sure player-guess appears above -main in your code, since any symbol, including function names, must exist before you can refer to it.

Taking a look at player-guess:

line 2: We store our randomly generated answer in the top-level form, since it’s value stays constant throughout the game.

line 4: We begin our loop by binding the output of get-input, which returns a player-supplied integer, into guess.

lines 6 and 7: If player does not get the answer, we give a hint and rerun the loop again with a fresh guess from the player.

line 8: Upon hitting the right answer, print a congratulatory message and exit the loop.

The game is almost complete! As a final step, let’s allow the player to choose between the two game modes without having to change the -main function.

Creating the Main Menu

Once again, we’ll add our new functionality within a new function, this time called menu:

(defn menu []
  (println "Let's Play Guess the Number!")
  (println "1. PC will decide the number")
  (println "2. You will decide the number")
  (println "3. Quit")
  (condp = (get-input)
    1 (player-guess)
    2 (pc-guess)
    3 (System/exit 0)
    (println "That's not a valid choice, sorry.")))

There’s nothing here that you haven’t seen before, except for System/exit. System/exit is a call to Java that allows you to exit the entire program. The reason why we need to explicitly exit the program is because we’re about to make a special modification to -main:

(defn -main []
  (while true
    (menu)))

We’ve changed -main to call our menu function as expected, but we’ve also wrapped it in a while form:

while
clojure.core

    (while test & body)

Repeatedly executes body while test expression is true. Presumes
some side-effect will cause test to become false/nil. Returns nil

while will repeatedly execute it’s body clauses while the test expression returns a truth value. Since we’re using “true” in our while statement, the -main function will keep calling menu since test is always true. Hence, without providing an option to explicitly exit the program, our player will be stuck playing rounds of Guess the Number indefinitely…

Further Improvements

Thanks for sticking with the tutorial! Although we’ve finished the game, there are still some optional improvements that could be made:

  • Error handling. If the player enters non-numerical input, the game will crash. We could handle this by catching the exception and prompting the player for input again.

  • Adding more randomness. When the computer guesses, it will always follow the “perfect” strategy of selecting the middle number between the minimum and maximum range. We could vary the guess by a little in order to make the guessing less mechanical, perhaps by adding or subtracting from the guess by a random amount. We would also need to ensure that the guess still stays within the allowed range of 0 to 99.

  • App distribution. You might like to send your game to a friend after you’re done improving it. By running leiningen uberjar on the command line, you can generate a JAR file which your friend can run, but you’ll need to make modifications to project.clj first.