The Urn Logo

Guess the number

In order to get more into the spirit of Urn here, let’s create the traditional “guess the number” game: the computer will generate a random number and you’ve got to guess what it is!

Well then, let’s create a new file, guess.lisp and start writing some code!

Totally random, dude!

First off, we’ll want to generate some random numbers. To do that, we need to import an external library. Let’s import the random function from the lua/math library.

(import lua/math (random))

A digression on imports: The import expression takes one argument, and several optional ones. The first argument is the library to import, in this case lua/math. By default all symbols in lua/math will be imported into the current scope prefixed with lua/math/: for instance, you’d reference the random function as lua/math/random.

If you specify a symbol as the second argument, all symbols will be prefixed with that instead. For example:

(import lua/math math)
(math/random 1 10)

If you only need a few symbols, you can specify a list of required symbols instead: this will import those variables directly into the scope. This is what we’re doing here.

So now that we’ve got the ability to generate a couple of random numbers, let’s generate a couple and print them to the terminal. As we want to generate integers (and not decimals) we’ll pass two numbers: the lower and upper bounds to generate between.

(print! (random 1 10))

We can copy and paste this line a few more times to get some more random numbers, but that gets tedious rather quickly. Let’s use a loop instead. As we just want to do something a set number of times, we’ll use the for looping construct.

(for i 1 10 1
  (print! (random 1 10)))

So what’s going on here? for is a rather complex construct so we’ll go though this slowly. Firstly, let’s consider the “signature” of for:

(for ctr start end step &body)

for will create a new variable, ctr, and set it to every value between start and end, adding step and executing all expressions in body each time. In our code above, we will set i to every number between 1 and 10 inclusive and print a random number.

What’s this &body doing? When & is in front of a variable name, it means it takes a variable number of arguments. Any additional arguments you give the function will get bundled into a list and assigned to this argument.

Of course, we only really want one random number, so instead let’s generate it and store it to a variable. There are two kinds of variables in Urn: top level definitions (like those created by defun) and “lexically scoped” variables (such as function arguments). Top level definitions are accessible anywhere in your program, lexically scoped variables are only available in the current block.

In this case, we want to create a lexically scoped variable. We’ll use a with construct to do that:

(with (my-number (random 1 10))
  (print! my-number))

The first argument here is a little odd: it is another list. It defines a variable to define (my-number) and a value to store in this variable. You can then use my-number anywhere inside the body of the with construct. This variable doesn’t exist outside of the block though: you’ll get an error if you try to access it:

(with (my-number (random 1 10))
  (print! my-number)) ;; Works OK: you're inside the `with` block

(print! my-number) ;; [ERROR] Cannot find variable my-number

Now that we’ve got a number, let’s ask the player what number they think it is.

Reading user input

The easiest way to read and write data from the terminal is to use Lua’s io library, namely the read and write functions. So first off, let’s import these into the scope:

(import lua/io (read write))

Let’s start off with displaying a prompt to the user and just echoing what they typed in. We’ll create a helper function to do this:

(defun read-input ()
  (write "Enter a number> ")
  (with (res (read "*l"))
    (print! (.. "You entered " res))))

This is just a combination of things you’ve seen before, but with a couple of new functions: we write some text, read a line from the console (hence the "*l" argument to read), store it to a variable and print it!. The .. function simply takes a list of strings and concatenates them together (just like Lua’s .. operator).

Of course, we’re interested in numbers, not strings. So we’ll need to convert it to a number, and ask them again if what the user enters isn’t valid. To do this, we’ll need to use a conditional.

The simplest conditional in Urn is if. This takes three arguments: the condition to test on, an expression to execute if this condition true and an expression to execute otherwise. We can simply attempt to convert our string to a number, print a message if it fails or return the number if it works.

(defun read-input ()
  (write "Enter a number> ")
  (with (res (read "*l"))
    (with (num (string->number res))
      (if num
        num
        (progn
          (print! "That's not a valid number. Try again!")
          (read-input))))))

Well, we’ve got a couple of new things here. Firstly let’s talk about progn. In Lisp there isn’t a distinction between “statements” and “expressions”: everything is an expression. For instance, it is possible to use the result of an if expression in a computation (like a more powerful ternary operator). However, sometimes you need to execute multiple expressions. Consider defun and with: these both accept multiple expressions, each one being executed and the last one being the “result” of the whole thing. progn is another such construct, executing a series of expressions in order.

You can see this all in action here: our if will return num if it is a number, or otherwise will execute all expressions in progn. progn executes the print! call, and returns its last expression, calling read-input again. This is an example of tail-recursion. Instead of a manual loop construct, like for above, we can simply call the function again and return its value instead. In fact, behind the scenes, this is how all looping is implemented.

You can now call this function, should you so fancy.

(print! (.. "You entered: " (number->string (read-input))))

Note the number->string function. It isn’t strictly required, but it is considered good form to add these cast functions rather than allowing Urn to implicitly convert between types.

Comparing numbers

So now that we can read user input and have a random number, let’s compare the two. We’re going to print out a message telling the user if they are too large, too small or just right. We’ll also return if it was correct: so we know when to stop our program. As we’re going to be doing multiple comparisons, our best choice is to use cond, a construct similar to an if - elseif block in Lua.

(defun compare (input expected)
  (cond
    [(< input expected)
     (print! "Too small!")
     false]
    [(= input expected)
     (print! "You win!")
     true]
    [(> input expected)
     (print! "Too large!")
     false]))

Each element of cond is composed of two parts. The first is a condition which, if it evaluates to true, will execute the remaining parts of this element.

It is worth noting that each cond case is executed in order, stopping on the first truthy element. If nothing is truthy then an error occurs: you should be careful to add a true block if you want to handle all cases. In the above case this isn’t needed as one of the conditions has to be true.

A word on brackets: You might have noticed those square brackets ([, ]) in the above code. This doesn’t have any special meaning: sometimes we use different sets of brackets in order to make it clearer what brackets line up.

Putting it all together

Well, we’ve got all the components, let’s merge everything together! We want to loop, prompting for input and comparing it to the expected value. If it is equal, then we want to exit the program, otherwise prompt again. We could write a tail-recursive function but, just for fun, let’s use a while loop instead.

(with (my-number (random 1 10))
  (with (found false)
    (while (not found)
      (set! found (compare (read-input) my-number)))))

Here we use the not function to check if we haven’t found a match yet, and loop until we do.

This is also our first introduction to set!. This takes a symbol and sets it’s value to that given. When compare returns true, found will be set to true and so (not found) is false, resulting in the loop terminating.

You should now be able to run your code (lua bin/urn.lua guess.lisp --run) and play the game.

Polishing things up

Now that we’ve got everything working, let’s clean up the code a little. Firstly, you may notice that running the program multiple times always results in the same random number being generated. In order to fix this, you’ll need to “seed” the random number generator. We’ll use the current time as our seed.

Let’s add randomseed to our lua/math import list, as well as importing time from lua/os:

(import lua/math (random randomseed))
(import lua/os (time))

(randomseed (time))

Now running our program multiple times should mean we get different numbers. Much better!

The next issues are more stylistic, but still good to fix. First let’s look at our read-input function and our main loop: both have nested with bindings. This gets a little ugly when you’ve got a lot of variables, so let’s polish this up using let*:

(let* [(my-number (random 1 10))
       (found false)]
  (while (not found)
    (set! found (compare (read-input) my-number))))

let* can be thought of as multiple nested withs bundled into one: each declared variable is accessible in all successive bindings. If you fancy, why not apply the same transformation to read-input?

Lastly, let’s add some documentation to our code. Documentation is given by a string at the top of the program or function. Firstly we’ll document the entire module, giving an explanation of what the entire program does. Just place a string at the very top of the program.

"A simple implementation of a number guessing game.

 This generates a random number and prompts for a number, giving feedback on whether
 it is too large or too small."

(import lua/math (random randomseed)
;; And the rest of your module

A word on strings: Strings can span multiple lines. Multi-line strings are aligned to the start quote character, making it easier to format your strings correctly. For instance,

(print! "Hello
         World")

results in

Hello
World

We’ll also want to document our read-input and compare methods.

(defun read-input ()
  "Read a number from the terminal, prompting until a valid number is entered"
  (write "Enter a number> ")
  ;; Etc...
  )

(defun compare (input expected)
  "Compare INPUT to EXPECTED, returning whether it is too large, small or correct"
  ;; Etc...
  )

Doc strings allow for several formatting codes:

  • SYMBOLS which are upper case are read as arguments to the function.
  • [[random]] will link to the documentation of another symbol in scope.
  • Any markdown code, including GitHub’s multi-line code blocks.

We can then generate documentation for this module, by running the compiler with the --docs .: this will produce a guess.md file in the current directory. You can also view the documentation of symbols in the REPL using the :doc command. For example:

> :doc with
(lib/base/with var &body)
Bind the single variable VAR, then evaluate BODY.