Game: Infinite Treasure

Game: Infinite Treasure #

Infinite Treasure is a game about opening treasure chests and getting loot.

Basic Game Loop #

We’ll start off with the same basic structure used in the Don’t Steve example, with some small changes.

Save this code in a file named infinite-treasure.lua

print("Welcome to Infinite Treasure")

game_over = false

while not game_over do
  print("Enter command: ")

  player_command = string.lower(io.read())
  if player_command == "quit" then
    game_over = true
  end
end

Here we’ve got out welcome message and basic game loop controlled by the game_over variable.

In the previous example, anything the player typed was treated the same way, either they won or lost depending on whether the entered text was “steve”.

In this game we want the player to be able to perform various actions, so each thing the player enters will be treated as a different command.

To make this work, we store what the player has entered into the variable player_command, using string.lower() to convert it to lower-case in the process, to account for the player entering different capitalizations.

If player_command is equal to the string "quit" then game_over is set to true and causing the while loop to stop looping the next time it’s condition (not game_over) is evaluated.

Newlines and io.write #

Of particular importance for text based programs is the newline character, which functions like pressing the Enter key on your keyboard. Newlines move the cursor down to the next line. Up to this point, we’ve been relying on print to add a newline for us automatically at the end of every line, but that isn’t always what we want.

For example, it would look nicer if the player could enter their command to the right of the Enter command: text, instead of on the next line. Since print is always going to output a new line automatically, we’ll have to use something else.

Fortunately we can use io.write – the opposite of io.read – to output text to the screen without the automatic newline at the end. All we have to do is change the line :

  print("Enter command: ")

to read:

  io.write("Enter command: ")

Now the cursor is ready and waiting at the end of our prompt, instead of down on the next line.

print is what you might consider a convenience function, it’s provided by the language to be useful in most basic situations, but it’s not perfect for every situation. We can use io.write to have a bit more control over what gets printed.

Printing newlines #

What if we want to print some more newlines? It would be nice to have some blank lines once in a while to break up the text on the screen and make it easier to read.

For example, we could add print("") after our welcome message, to add a blank line:

print("Welcome to Infinite Treasure")
print("")

Another option, is to use the escape code \n in our string. Instead of a second print we could use:

print("Welcome to Infinite Treasure\n")

Note the \n near the end. When Lua sees a \ inside a string, it causes the next character to have a special meaning. When it’s followed by an n, it means print a new line. Because print also prints its own newline as well, we get a blank line.

If you wanted, you could print each word on a new line, like so:

print("Welcome\nto\nInfinite\nTreasure\n")

Which will look like this:

Welcome
to
Infinite
Treasure

Other escape characters #

There are other escape characters available as well. A couple of useful ones are \t which prints a tab character, and \" which prints a " character.

\" might not seem super useful as first, but think about how you might print Welcome to "Infinite Treasure", including the quotes.

This won’t work:

print("Welcome to "Infinite Treasure"\n")

And you’ll get an error like ..\bin\lua53: infinite-treasure.lua:1: ')' expected near 'Infinite'.

This is because we’re already using " to indicate the beginning and end of our string. Lua’s rules say the second " ends the string, and suddenly Infinite Treasure isn’t part of the string any more.

This is where the \" escape code comes in.

print("Welcome to \"Infinite Treasure\"\n")

The \ tells Lua this isn’t a normal ", so don’t end the string, instead include the " as part of the text to be printed. The final " doesn’t have a \ before it and ends the string as usual.

More Info

You can see a full list of escape codes at Programming in Lua - Strings

Code So far
print("Welcome to \"Infinite Treasure\"\n")

game_over = false

while not game_over do
  io.write("Enter command: ")

  player_command = string.lower(io.read())
  if player_command == "quit" then
    game_over = true
  end
end

Writing Functions #

We’ve already been using a few functions, we need to in order to do much of anything. print, io.read, and io.write are all functions that are provided by Lua.

As our programs get more complicated, it’s going to be a good idea to organize our own code into functions. We can declare a function using the function keyword, and giving it a name:

function main_loop()

end

This declares an empty function named main_loop that takes no parameters – the () mark where the parameters go, but we don’t have any right now, so they’re empty.

An empty function isn’t so useful, so let’s move everything in our game so far inside the main_loop function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    end
  end
end

If you run this program you should see…. nothing.

When code is inside a function, it doesn’t run automatically. You have to call the function to make the code inside of it run.

We can call our function the same way we call print, or io.read, by typing its name followed by (). Add this line to the end of our program, and we should be back in business:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    end
  end
end

main_loop()

Note that the first appearance of main_loop is where we are declaring the function, that is we’re giving it a name, and writing the code that the function contains, but it doesn’t run yet. It’s the function call at the end of the program that tells Lua call the function main_loop() and run the code inside.

Let’s add another function called loot_treasure and call this new function if the player enters the command loot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    end
  end
end

function loot_treasure()
  print("Looting treasure chest...")
end

main_loop()

If you run the game and enter the command loot you should see the message Looting treasure chest....

Take note of the position of the function within the file. loot_treasure is declared after main_loop is declared, but before main_loop is called. You are mostly free to declare the functions in your program in any order, however they need to be declared before they are used at runtime!

We could swap our two functions around, and our program will still work fine:

function loot_treasure()
  print("Looting treasure chest...")
end

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    end
  end
end

main_loop()

As both functions still get declared, and then main_loop runs.

What happens if we put loot_treasure at the end of our program?

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    end
  end
end

main_loop()

function loot_treasure()
  print("Looting treasure chest...")
end

The program should start ok, but if we type loot we’ll get a runtime error:

Welcome to "Infinite Treasure"

Enter command: loot
..\bin\lua53: infinite-treasure.lua:14: attempt to call a nil value (global 'loot_treasure')
stack traceback:
        infinite-treasure.lua:14: in function 'main_loop'
        infinite-treasure.lua:19: in main chunk
        [C]: in ?

Our code that calls loot_treasure() is inside main_loop() which is still running until the player quits out of the while loop. So the program hasn’t proceeded far enough along to see the declaration of loot_treasure(), and as far as Lua is concerned, it doesn’t exist yet.

If we keep our call to main_loop() at the bottom of our file, we can be sure that all of the functions we write have been properly declared and are ready to be called.

Code So far
function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    end
  end
end

function loot_treasure()
  print("Looting treasure chest...")
end

main_loop()

Looting Gold Coins #

Of course we want to find some actual treasure when we loot a chest. We can add gold coins easily, similar to how we kept score in Don’t Steve. We’ll declare a new variable to store how many coins we’ve found so far, and add to it each time we open a chest.

At the top of our program, declare the variable and start it off with a value of 0.

coins = 0

Then we can change the loot_treasure function to tell us how many coins we found, as well as adding them to our total.

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = 1
  print("You found", found_coins, "gold coins!")

  coins = coins + found_coins
end

If you run the program at this point, you should see that each time you loot, you find one coin. It’s not nothing, but it’s not especially exciting, and also the formatting is a bit awkward, due to how print formats multiple parameters.

Enter command: loot
Looting treasure chest...
You found       1       gold coins!

We can improve how this looks by using the .. operator. .. is is the string concatenation operator, which just means it joins the strings on either side together into a new string.

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = 1
  print("You found" .. found_coins .. "gold coins!")

  coins = coins + found_coins
end

Here we’ve joined "You found" with found_coins and "gold coins!" all into a single string before passing it to printf as a single parameter.

This gives us…

You found1gold coins!

What happened here?

Before when we’ve using print to print multiple strings, print will automatically add tabs between them, but now we’re using .. to join strings together, and .. doesn’t add any extra spaces or tabs automatically, it just joins strings as they are.

We’ll need to add our own spaces into our strings, after the word found and before the word gold.

  print("You found " .. found_coins .. " gold coins!")

Now we get the nicely formatted output:

You found 1 gold coins!

And we should probably add a way to see how many gold coins you’ve collected so far. What’s the point of collecting all that gold if you aren’t going to gloat about how much you have?

We’ll add a new command check in our main_loop, and a new function check_inventory() to go along with it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
coins = 0

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    elseif player_command == "check" then
      check_inventory()
    end
  end
end

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = 1
  print("You found " .. found_coins .. " gold coins!")

  coins = coins + found_coins
end

function check_inventory()
  print("You have " .. coins .. " gold coins!")
end

main_loop()

Random Coins #

Getting a single gold coin each time you open a treasure chest isn’t that engaging. Games often use randomness to add variation to the player’s experience. Let’s add a random amount of gold coins to our chests.

Lua provides a simple random function math.random(). Let’s try it out and see what we get:

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = math.random()
  print("You found " .. found_coins .. " gold coins!")

  coins = coins + found_coins
end
Looting treasure chest...
You found 0.56356811523438 gold coins!

As it happens, math.random() returns decimal values between 0 and 1 by default. This is more useful than it might sound, as you can think of this as being numbers between 0% to 100%, just expressed as a decimal number instead of a percentage. 0.1 is 10%, 0.53 is 53% etc…

We probably don’t want to deal with fractions of gold coins, though. There’s another way to call math.random which is to pass in a maximum value. This will generate an integer value (whole number) between 1 and the specified maximum value.

For example, if we wanted the maximum number of coins you could find in a treasure chest to be 500, we could write:

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = math.random(500)
  print("You found " .. found_coins .. " gold coins!")

  coins = coins + found_coins
end

Now we have sensible whole numbers of gold coins in each chest:

Looting treasure chest...
You found 96 gold coins!
Code So far
coins = 0

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  game_over = false

  while not game_over do
    io.write("Enter command: ")

    player_command = string.lower(io.read())
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    elseif player_command == "check" then
      check_inventory()
    end
  end
end

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = math.random(500)
  print("You found " .. found_coins .. " gold coins!")

  coins = coins + found_coins
end

function check_inventory()
  print("You have " .. coins .. " gold coins!")
end

main_loop()

Pseudo-Random Coins #

You may have noticed a couple of odd things going on with our so-called “random numbers”. The same sequence of random numbers (ie. the number of gold coins) comes up each time we run the game, and the first one is also likely to be very small each time. Suspiciously not very random.

It turns out that it’s actually quite hard to generate true random numbers on a computer, so what you typically find are pseudo-random number generators. These generators use a seed – a starting value – which all of the subsequent results are based on.

The thing is, if you use the same seed, you’ll get the same results every time. This is what you’ll see if you run the game multiple times. Lua uses the same default seed every time, so the same sequence of not-very-random numbers is generated.

There are whole fields of study into generating really good random numbers which are critical for computer security, but all we need right now are numbers that aren’t obviously the same every time.

This can be accomplished by setting the seed to a different value each time the game is run. And an easy and common way to get a different seed, is to use the current time. If you run the game at exactly the same time on two computers, you’ll get exactly the same sequence of results, but that’s probably good enough for our purposes.

We can call os.time() to get a number representing the current time on the computer, and pass the result to math.randomseed() to set the starting seed for the random number generator.

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  math.randomseed( os.time() )
  game_over = false

We only do this once, at the beginning of the program, and every call to math.random() after that will be based on that seed.

Now each time you run the program, all of the numbers generated should appear sufficiently different.

If you want you can print out the seed each time to verify that it is actually different:

  time = os.time()
  print("Seed:", time)
  math.randomseed(time)
Final Code: Infinite Treasure
coins = 0

function main_loop()
  print("Welcome to \"Infinite Treasure\"\n")

  math.randomseed( os.time() )
  game_over = false

  while not game_over do
    io.write("\nEnter command: ")

    player_command = io.read()
    if player_command == "quit" then
      game_over = true
    elseif player_command == "loot" then
      loot_treasure()
    elseif player_command == "check" then
      check_inventory()
    end
  end
end

function loot_treasure()
  print("Looting treasure chest...")

  found_coins = math.random(500)
  print("You found " .. found_coins .. " gold coins!")

  coins = coins + found_coins
end

function check_inventory()
  print("You have " .. coins .. " gold coins!")
end

main_loop()

Challenges #

  • Add a help command, that prints a nicely formatted list of commands that the game accepts, along with what they do. For example:
Welcome to "Infinite Treasure"


Enter command: help
Commands:
        loot    Loot a treasure chest
        check   Check how many coins you have
        help    Show this message
        quit    Quit the game
  • If the player enters something that is not a known command (like askhfg), then print a warning message like Sorry, I don't understand "askhfg". Then print the same help message as if they had typed help. Avoid duplicating the message in the code by writing a new function you can call in both cases.

  • When looting, print the new total number of coins along with the number that were looted. For example:

Enter command: loot
Looting treasure chest...
You found 69 gold coins, for a total of 420!

Next - Infinite Treasure II