Previous

Play it again, Sam - Making games that can be replayed.

Next

The games so far have all been play-once games. You get to the end, the game exits, and you have to run it again.

Most games will ask you if you want to play again, and then give you some sort of new game - new secret locations a new board, new characters, or something.

In the last lesson, we calculated a secret X and a secret Y location, and then built each button with a command to say how to get to the secret X and Y location.

In order to replay the game, we need to calculate a new pair of secrets, and then make a whole new set of buttons.

That sounds like a lot of work. There must be a better way.

Of course, computer programmers ran into this problem years ago, and looked for a way to make it easier.

Since so much of what computers do is a reflection of what people do, they thought about how people handle situations where there are a lot of things to do.

What humans do is to list all the steps in order and then make up a short phrase that means "do this stuff".

For example, at some point, your mom sits down with you and says "Here are your chores..." Now, when your mom says "Do your chores", you know she means "Collect the plates from the table, stack them by the sink. Feed the dog and fill the dog's water bowl. Go out to the mailbox and bring in the mail." (At least, you better know what she means, or you'll be in trouble.)

If you play (or watch) football, you know that when the quarterback calls the "Statue Of Liberty Play" he means that he will will fake a pass while he actually hands the football to a player behind him. The quarterback just says "Statue of Liberty", and his team knows what to do.

In computer programs we call these descriptions of what to do a procedure. Some languages call these a function or a subroutine, but in Tcl they are procedures.

A procedure has a single word name (like "chores") and a set of commands we call the body. Most procedures also have a special set of variables so we can give the procedure different details each time we run the procedure.

For example, in the "chores" procedure, your mom might tell you which table to clean off. In the "StatueOfLiberty" procedure the quarterback needs to specify which player should run behind him to get the ball.

In Tcl/Tk, we have to define our procedures before we use them, just like your mom has to tell you what your chores are before she can expect you to do them.

We define procedures with a command named proc. The proc creates a new procedure for us. To do this, it needs three arguments:

A simple procedure looks like this:


# This defines  a procedure 
#  The first line gives us the name and arguments
#  The procedure name is showMessageBox
#  The procedure takes two arguments:
#    type    - A type of message box to show
#    message - The message to show in the message box
#  The last curly brace says that the body is on the next lines.
proc showMessageBox {type message} {
  # This is the body of this procedure.  It runs the
  #  tk_messageBox command
  tk_messageBox -type $type -message $message
}

That defines a procedure, but it doesn't do anything. It's as useful as your mom telling what your chores are but never telling you to do them. (This may not be a bad thing, but it doesn't get the chores done.)

Once we define a procedure in Tcl/Tk we have to tell Tcl/Tk to run it.

When we define a new procedure, it's as if we added a new command to the Tcl/Tk language. The new command behaves the same way as the original Tcl/Tk commands like for and if. We tell the Tcl/Tk interpreter run our procedure by typing the name of the procedure and its arguments on a single line.

This code defines the procedure and then runs it:


proc showMessageBox {type message} {
  tk_messageBox -type $type -message $message
}
showMessageBox yesno "Did this work?"

Try typing that into Komodo and run it. Try running the procedure with different types like ok and okcancel and try changing the message.

That's pretty cool, but lets do something useful with these procedures.

For starters, instead of making buttons that each have their own commands to tell the player whether or not they've guessed right, lets make the all the buttons call the same procedure, and that procedure will figure out what to do.

It's not obvious, but this means that we don't need to know what the secret is when we create the buttons. Our new procedure can compare the player's guess to the secret when they click a button.

Checking the player's actions when they do them them, instead of coding all the events once before the game starts is one of the tricks to making a game that's re-playable. Now we can change the secret without rebuilding all the buttons!

So, what do we need in this procedure? We need to know what button the user clicked, and what the secret is.

Lets make a simple game - two buttons, and a secret number - sort of like the "Guess which hand I've got the candy in" game you played as a kid.

We can assign a different number to each button and then pass that value to the the procedure as the argument. The secret value won't change until we start a new game. We don't need to pass that value.

So, the start of our procedure will look like this:


# Define the checkGuess procedure with one argument variable.
# guess: this turns guess.
proc checkGuess {guess} {

Note that we have a curly brace at the end of that line. That brace tells Tcl/Tk that we're starting a set of code, and it should keep reading lines of code until it gets to the matching close brace.

Now we get to a tricky part.

The body of a procedure is like a computer program all by itself. We can put variables and commands inside the body of the procedure, and they only get used when we run that procedure, just like the commands and data in a computer program only get used when we tell the operating system to run that program.

One nice thing about this is that we can have variables that only exist within the procedure body. For instance, we can have a loop where the loop variable is named counter, and within that loop we can call a procedure that contains a variable named counter. The two variables have the same name, but they are two different variables. The loop variable named counter exists outside the procedure, while the other variable named counter exists inside the procedure.

In computer science terms, we call this a variable's scope. The variables we've used so far exist in the global scope. This means they get created when the program starts, and they exist until the program exits.

Variables inside a procedure exist in the local scope. These variables don't get created until the procedure is run, and when the procedure is finished the memory that was used for these variables is returned to the operating system so other programs can use it.

Take a glance at this code, and try to guess what the message in the message box will be. Try copying it into Komodo and see if you're right.


proc addOne {value} {
  set var [expr $value + 1]
}
set var 2
addOne $var
tk_messageBox -type ok -text "Variable var has the value $var"

If you guessed that $var would still have the value 2 stored in it, you were right. The variable var in the addOne procedure is in the local scope. The local scope var is just a variable that happens to have the same name as a variable in the global scope.

In fact, this addOne procedure doesn't do much of anything. It creates a local variable named var, sets the value of that variable, then it's done and the variable is destroyed.

There is another command we can use in a procedure called return. This command tells the Tcl/Tk interpreter that we're done with this procedure and it should return to the line of our program just after we called the procedure.

We can give the return command an argument, and then it will return that value. When we tell our procedures to return a value they behave the same way as Tcl/Tk commands like expr that return a value. We can put the call to a procedure in square brackets, and Tcl/Tk let us use the returned value.

Take a look at this code and guess what var2 will be.


proc addOne {value} {
  set var [expr $value + 1]
  return $var
}
set var 2
set var2 [addOne $var]
tk_messageBox -type ok -text "var2 has the value $var2. var has the value $var"

The most common way to use procedures is to pass the procedure some values, do calculations with local variables, and finally use the return command to return a result to the code that called the procedure.

The division between between local and global scope is a good thing. It lets us write code in little pieces that only interact with each other when we want them to. Think of how hard it would be to keep track of all the variables if you had a program 40,000 lines long with a thousand variables, and you had to make sure you never changed the value of the wrong variable at the wrong time.

But, all good things have exceptions. Sometimes, we need to access a global variable from inside a procedure.

For instance, the variable with a game's secret will be created in global scope. We need it to last for as long as the game runs. At the same time, the procedure that checks to see if the player guessed the secret needs to access that variable from inside the procedure body.

The command that lets us tell the Tcl/Tk interpreter that we want to access a global scope variable from inside a procedure is the global command.

We use the global command by putting a list of the variables we want to share after the word global.

Here's a small guess-a-number game using a procedure to compare a guessed value to the secret value. The procedure will create a tk_messageBox to tell us if the guess matches the secret.

The secret value is in the global scope. We put the secret in the global scope because we need the variable to exist for as long as our program is running, and we need to let other procedures gain access to the variable when they need it.


proc checkGuess {guess} {
  global secret
  if {$guess == $secret} {
    tk_messageBox -type ok -message "You Got It!"
  } 

  if {$guess != $secret} {
    tk_messageBox -type ok -message "That's Not It."
  } 
}
set secret 2
button .b1 -text 1 -command "checkGuess 1"
button .b2 -text 2 -command "checkGuess 2"
grid .b1 
grid .b2

We can add another procedure to reset the secret number, and a button to invoke it like this:



################################################################
# proc setSecret {}--
#    set the Secret value
# Arguments
#   None
# 
# Results
#   Assigns a random value of 1 or 2 to the global variable secret 
# 
proc setSecret {} {
  global secret
  set secret [expr int(rand() * 2) + 1]
}

################################################################
# proc checkGuess {guess}--
#    Compares a player's guess to the secret.
# Arguments
#   guess	The number a player is guessing
# 
# Results
#   Displays a tk_messageBox.  The message is set for whether the
#   guess is correct or not.
# 
proc checkGuess {guess} {
  global secret
  if {$guess == $secret} {
    tk_messageBox -type ok -message "You Got It!"
  } 

  if {$guess != $secret} {
    tk_messageBox -type ok -message "That's Not It."
  } 
}

# This is global scope code that sets up the game.
set secret [setSecret]
button .b1 -text 1 -command "checkGuess 1"
button .b2 -text 2 -command "checkGuess 2"
grid .b1 
grid .b2
button .reset -text "change Secret" -command setSecret
grid .reset

Those are the two procedures we really need in order to write a program that can be restarted with new values. One to set up the game and one to process a player's turn. In this case, we set up the game by creating a secret number and process a player's turn by comparing their guess (which button they pressed) to the secret and reporting the results to the player.

There is a block of comments just before each procedure. It's not required, but it's a very a good idea to put comments in front of your procedures. When you write the code, it's very obvious what it does, and you could never forget it. Next week, it won't be so obvious, and in a month, you'll have forgotten what it did and why you ever wrote such silly code.

The Arguments part of the comment will help you remember how to use this procedure. The Results part of the comment tells you what this procedure does that happens in the global scope. In large programs (like a Pac-Man game), the biggest source of bugs is having procedures do something in the global scope (like change the value of a variable) that nobody remembered this procedure did.


Try playing with the 2 number guessing game in the previous code. Modify it to build 10 buttons using a for loop, and then change the checkGuess command to make message boxes for too high and too low.


Here's the important things in this lesson:

The next lesson will introduce some ways to keep track of the score.

Previous

Next


Copyright 2007 Clif Flynt