Previous |
| Next |
Did you know that you can download pictures of playing cards from the Wikimedia Commons project? Neither did I until I went looking for them.
If we can get a free set of cards images to play with, it would almost be a crime to not use them to make a game.
A solitaire version of Concentration is pretty easy to write. We can write this game using buttons and images like we did for Tchuka Ruma in lesson 15.
This lesson will show you how to do that.
You can find a set of card images here, but you can download a zip file of the cards, and a card back and a blank card by clicking here.
Now that you've downloaded the card images and unpacked the zip file, you'll want to load them into a Tcl/Tk program and see how they look.
The card images in the zip file are named following a pattern:
.gif
to let us know that
these are Graphic Interchange Format image files.
To load image in a file named s_a.gif
(the Ace of spaces) and
create an image named s_a
, we'd use a command
like this:
|
With the Tchuka Ruma game, there were just 9 images to load, and it wasn't too unpleasant to type in the 9 image commands.
Now we've got 54 images to load (counting a card back and blank card).
Typing 54 image create
commands is almost as boring
as writing "I will let a computer do the boring stuff" 10,000 times.
The last time we had to do something over and over, we used a for
loop. We could use a for
loop to count the cards from 2
to 10, but how do we count from j to a?
You might have guessed that this isn't the first time a computer programmer has had this problem.
The solution to this problem is the foreach
command. The
foreach
command loops through a list of things, rather
than counting a number of loops.
The foreach
command has lots of variations on how it
will loop. We'll look at the trickier ones later.
For now, we just need to run through the list of cards, and that's simple.
One way the foreach
command works is to loop through a
list, one element at a time. We give the foreach
command a variable to use for the loops (like the counter variable in
for
commands), a list of things to loop through, and a
body to evaluate on each pass through the loop.
Here's some code that will step through a loop and put up a message box for each word in a sentence. Try typing it into Komodo Edit and see what it does.
|
The sentence in that example is a simple list. In Tcl, the list is an important type of variable. We use a list when we want to have collection of values that we want to keep in a particular order. Each word in a list is a separate list element, that we can access or change without touching the rest of the list.
Here's some code that will read 3 image files from the disk and create
Tcl/Tk images in a program. The images will be named the same thing as
the original file, the Ace of Spades image will be named
s_a.gif
, the King of Spades image will be named
s_k.gif
, and so forth.
|
To load all the cards, we'd need to make a list with every file name in it. That's 54 files. I'd rather type 54 words than 54 lines, but I'd rather type less. The less I type, the fewer errors I mkae.
There is a Tcl/Tk command to look at files on the disk and return what
it finds. The command is named glob
for reasons that made
sense at the time.
The way the glob
command works is that you give it a pattern
for a file name, and it gives you a list of all the files that match that
pattern. (Notice that word - list - this is one of the places where
the list data type gets used.)
The rules for the pattern are pretty simple
|
To get a list of all the .gif
image files in a folder, we
could use a line like this:
|
The star (*
) is called a wildcard. It will match any set
of letters or numbers.
If we want to loop through those files and make images, we can do it with code like this:
|
That's a whole lot better than typeing 52 lines of image create
commands, and even better than typing 52 file names.
The idea of naming all the images with the same name as the file,
including the .gif
extension is a bit kludgey, though.
We can do better.
A list is a bunch of words separated by spaces, and sometimes grouped with curly braces if a list element has spaces in it. But sometimes we get a bunch of stuff that we want to treat as a list that's not separated by spaces.
For instance, a file name is two parts - the unique name part and the suffix part, separated by a period. It would be nice to be able to treat those two parts separately.
If we could convert s_a.gif
to two words - s_a
and gif
it would be a list. There's lots of commands for
working with lists.
Tcl/Tk has a way to split strings of characters into lists. We call it
the split
command. We give the split
command
a string and one or more characters to split on, and it gives us a list
that starts a new list element at each location where there was a
character to split the string on.
For example, try this:
|
Try replacing the "." in the split command with a "_", or "a" and see what you get for a new list.
We don't really want a list though, what we want is the unique name part of the file name.
The first element in the list will be the unique part of the
file name, something like s_a
.
We can loop through the elements in a list with the foreach
command, but that doesn't help us much in this case.
There are other commands to work with lists. One of these is the
lindex
command. The lindex
command
returns the list element at a given location.
You use the lindex
command by typing the lindex
command and providing two arguments:
Note that the first list element is element 0, the second is 1, etc. This isn't what you might expect, but computer counting usually starts at 0 instead of 1.
Here's some code code will split the file name and put the first part
of the file name into a variable named card
.
|
This gets us closer to a procedure to load all the card images. The last trick is that we'll want to save a list of all the cards.
It's time for another command. This time the command is
lappend
. The lappend
command is like the
append
we first saw in lesson
10.
As you recall, the append
command appends new characters
to the end of whatever is stored in a variable. The
lappend
appends one or more new list elements onto a list
that's stored in a variable. If the variable hasn't been used yet both
append
and lappend
will create it.
Syntax:lappend listName element1 element2 element3
listName |
The name of the variable that contains the list to have data appended to it. |
element* |
Elements to append to the list |
The lappend
command is the only list command that changes
the contents of a list. All the other list commands leave the original
list like it was and return a new list.
Try this code to see how the lappend
command creates a
list.
|
Here's some sample code for loading bunch of files and making images with the same name as the first part of the file name, and saving all the names in a list.
|
Once you've typed that code into Komodo Edit and run it, you've created the card images, but they aren't being displayed anyplace.
We can put each image onto a button to check that the program is working by typing the code below into Komodo Edit. This will make 52 labels in 7 rows and 8 columns The last row will only have 4 cards.
|
Here's an old joke: an optimist thinks a glass is half-full while a pessimist thinks it's half empty. An engineer thinks the glass is too big.
If you typed in the last two snippets of code, you noticed that we've got one half-empty row (or half-full, if you're an optimist). Computer programmers are like engineers - we think there's the wrong number of cards to fit evenly in the number of rows we're using.
If we got rid of 4 cards, the cards would fit in 6 even rows. So, lets toss out all the Aces. We can do that by looking at each card as we create the images to see if it's an ace. If it's an ace, we won't load it.
We can check to see if the card is an Ace with the ne
operator for the if
command, and using the AND
(&&
) operator to combine 4 tests into one. The code
below will check to see if a fileName is one of the Aces.
|
That's enough commands for the moment, here's a procedure for loading the cards, creating the images, and storing the cards in a list.
As usual, we'll have a global variable that's an array to hold the
information we need for this game. We'll name that variable
concentration
. We'll need list of all the cards. It
makes a program easier to read if we use names for our variables
that describe what they hold, so we'll
store the names of the card images in concentration(cards)
.
|
If we deal the cards out into the rows and columns the way we tested to see if the images got loaded OK, we'd always get the cards in the same order. This might be a fun game once, but it would get boring pretty quickly.
We need to shuffle the cards.
If you are shuffling a real deck of cards, you probably know of several ways -
For people, it's easiest to shuffle the cards by doing something with a deck. For a computer, it's easiest to do stuff one card at a time. Remember, the computer is very fast at doing simple things in a loop, but not so good at doing complex stuff with a lot of things.
We could shuffle real cards by taking one card at random from the deck and putting it in the another pile until all the cards are in the second pile.
This is a slow way for people to shuffle cards, but it's a good way for a computer.
For the number guessing games, we made up secret numbers using the
rand()
function of the expr
command. The
rand()
function returns a fraction between 0 and .999. We
can convert that to a number between 0 and N by multiplying the
fraction by N, and can convert that to a whole number with the
int()
function.
To find a random card in the card list, we could use the lindex
command and a random position with code like this:
|
That finds a random card in the deck (or a random image in the list if you prefer) and puts it onto the new stack of cards (OK, it really appends it to the list of images).
Unlike moving a physical card from one deck to the other, we just made a new card to put in the new list, and didn't remove the original card. Copying a card without destroying the original would be a neat trick in real life (I'd like to shuffle the money in my wallet like that), but it's not what we want to do in this computer program.
What we need to do for a computer is to append the name of the card image in the new list and then delete it from the original list.
Tcl/Tk doesn't exactly have a "delete element in a list" command. What Tcl/Tk has is a "replace an element in the list" command, that lets you replace one or more list elements with new values. If the command doesn't provide enough new values to be put into the list, Tcl/Tk replaces those list elements with nothing. Putting nothing where a list element was deletes the list element.
You give the lreplace
command:
Note that the lreplace
command doesn't change your
original list. This command creates a new list with the new elements
you requested.
Here's an example of using the lreplace
command.
|
Try typing that code into Komodo Edit and see what comes out in the message box.
The new list you see should be a b 1 2 3 g h i j
. We
told the lreplace
command to replace the characters
between the c and the f (including the c and f)
with the numbers 1, 2, and 3. We took out 4 letters
(c d e f) and put in 3 numbers (1 2 3).
Here's the syntax for the lreplace
command.
|
We have one more problem to solve before we can write a procedure to shuffle the card images. Each time we move a random image from the original list into the new list (taking one card out of the deck and putting it in the pile on the table), there are fewer cards in the deck.
We know how to calculate a random number between 0 and N. But, every time we take a card out of the deck (remove an image from the list) the value of N gets one smaller.
We usually write our for
loops to count up from 0 to N, but
we can also write a for
to count down from N to 0.
We know we've got 48 cards, so we can set the initial value of the loop counter to 48, and count down until there are 0 cards left in the original pile.
We could make a shuffleCards
procedure that knows about
the global concentration
array and which index
(cards
) holds the list of cards, how many cards there
are and stuff like that. This would be a perfectly good procedure
for this program.
But we could only use that procedure in this concentration game.
We'll want to randomize the list of cards for any card game we write. We'll even want to randomize lists of other things in other games. Even different card games might have different numbers of cards in the deck (think of writing a pinochle or euchre game).
This is a good procedure to write so that we can use it again in a different program. We call this code reuse. One of the things that makes some programmers better than others is how many reusable procedures they have available to plunk into a new program. Not having to write and test a new procedure saves a lot of time.
We still need to know how many elements are in the list. The way to
learn that is another list command. This one is llength
.
You give llength
a list, and it returns the number of
list elements.
This sample shows how you might use the llength
command.
|
That's all the new commands we need to shuffle the deck of cards. We
call this procedure randomizeList
, rather than
shuffleCards
, since we can use it to change the order of
any list of things.
Take a look at this code. We pass the randomizeList
a
list of stuff, and it returns a new list, with all the same stuff
as the original, but in a different order.
|
Ok, that loads the card images and shuffles the deck. We're on the home stretch now.
In order to make a replayable game, we need to have 4 procedures that work together:
makeGameBoard
. This procedure is called once, when the
game starts to create the all widgets
startGame
. This procedure is called each time we start
a game. It initializes the global variables and configures the buttons.
playerTurn
. This is called each time the player
takes a turn. In this case, each time the player clicks a button.
endGame
. This is called once at the end to clean
up any loose ends, report the final score, and ask the player if they
want to play again. It will call startGame
if the player
wants to keep playing.
I added one more procedure to this list for this game -
checkForFinished
. This procedure gets called from
playerTurn
to see if the player has found the last card
yet. The code that does the check is very small (a single if
statement), but by putting this into a separate procedure it will be
easier to change the rules for winning the game if we want to.
Look at the command that's given to the buttons. We call the
playerTurn
proceudure with the position of this card in
the list. The first button relates to the first card, the second
button relates to the second card, etc.
The list of cards will not change while we play a game of concentration,
so we can identify a card by either the image name (s_q
) or
it's position in the list (the card at position 5 might be the Queen of
Spades).
Here's the complete code for this game. Copy it into Komodo Edit and see how it plays.
|
Type or copy/paste that code into Komodo Edit and try playing a couple of games.
Notice the update idle
command and the after
command. We looked at these in lesson 13. The
update idle
tells Tcl/Tk to update the display right now. The
after
causes the game to pause for a second so you can see
the cards before it turns them over again.
Try changing the endGame
procedure to tell you if you've
got a new best score. You'll need to add a label
or
tk_messageBox
to the endGame
to tell the user
what the best score was. You'll need to add a new index to the
concentration
array to remember what the previous best
score was.
Try clicking the same button twice. There's a way to cheat at this game!
Try adding another test to playerTurn
to prevent people
from cheating.
This lesson introduced a lot of new ideas. The big concept is the idea of storing data as a list. The list and the associative array are the two important types of variables in a Tcl/Tk program. We use these types of variables to organize data, not just to store it. As your programs get more complex, organizing the data becomes more important than writing the commands.
The important points in this lesson are:
lindex
command will return the list element at a location
lindex {1 2 3 4} 2
will return 3
llength
command returns the number of elements in a list.set numberOfElements [llength $myList]
lreplace
command returns a new list with some elements changed. The original list is not modified.
set newList [lreplace $oldList 0 1 newFirstElement newSecondElement]
glob
command will return a list of file names that match a pattern.
foreach
command will loop through a list, one loop for each list element.
Previous | Next |