The concentration game with images on the buttons looks cooler than
buttons with a bunch of words like "Queen Hearts" and "Back of Card".
But, to make really nifty games, we need the canvas widget we discussed
in lesson 16.
Lets see how hard it is to convert the previous concentration
game into one that draws the cards on the canvas instead of buttons.
As with converting Tchuka Ruma
from a button-based game to
a canvas based game, most of our program won't need to be changed.
The procedures to load the images (loadImages
) and to
shuffle the deck (randomizeList
) won't need to be changed at all.
The procedure that makes the board will need to be changed a lot. But
even this procedure can keep some parts the same - we still want to use the
two labels and buttons at the top, for instance.
Here's the code to make a the game board using a canvas. I calculated
the dimensions of the canvas by multiplying the height and width of the
cards and adding some extra because we want to put a little space
between cards. We'll talk about how to find out the size of an image
later in this lesson.
The canvas is gridded with the -columnspan 4
option to
tell the grid command that we want the canvas to use columns 1-4.
This lets us to put the 4 labels above the canvas in columns
1, 2, 3 and 4. We first looked a the -columnspan
option
in lesson 8.
################################################################
# proc makeGameBoard {}--
# Create the game board widgets - canvas and labels.
# Arguments
# NONE
#
# Results
# New GUI widgets are created.
#
proc makeGameBoard {} {
# Create and grid the canvas that will hold the card images
canvas .game -width 718 -height 724 -bg gray
grid .game -row 1 -column 1 -columnspan 4
# Create and grid the labels for turns and score
label .lscoreLabel -text "Score"
label .lscore -textvariable concentration(player,score)
label .lturnLabel -text "Turn"
label .lturn -textvariable concentration(turn)
grid .lscoreLabel -row 0 -column 1 -sticky e
grid .lscore -row 0 -column 2 -sticky w
grid .lturnLabel -row 0 -column 3 -sticky e
grid .lturn -row 0 -column 4 -sticky w
}
|
You may have noticed that we didn't display any cards in the
makeGameBoard
procedure. We'll make all the card
images in the startGame
procedure.
Before we start putting cards on the canvas, we want to make sure
the canvas is empty. The first time we play a game, we know it's
empty, but after that, we might not be so sure.
In lesson 16, we used tags to identify the things we drew on the
canvas. Tcl/Tk also gives us a few tags for free. For instance, the
tag all
means everything on a canvas. So, if we want
to clean up a canvas before we draw on it, we can do this with a command
like this;
When we make games with buttons, we use the grid
command.
The grid
command takes care of arranging the buttons so
they all fit nicely on the screen.
A canvas needs us to tell it the exact location for things we draw on
it.
The grid command makes sure that one widget won't overlap another. We
probably don't want to display one button on top of another.
The canvas will happily let us draw one thing on top of another.
We frequently want to draw one thing on top of another. For
instance, in the Tchuka Ruma
game. we drew the bins first,
and then drew the beans on top of each bin .
Before we tell the Tcl/Tk canvas to display the image of a card,
we need to calculate just where we want the card to go.
To do that, we need to know how big the card images are.
We've used the image create
command to load the images.
Tcl/Tk has several other image
commands that we will
use eventually.
The two that we can use right now are:
Command
| Description
|
image height imageName
| Returns the height of the image in pixels
|
image width imageName
| Returns the width of the image in pixels
|
Since all the cards are the same size, we only need get the height
and width for one card. We can save the height and width to use later
when we calculate where to put the cards.
We don't really want the cards to sit right next to each other. They'll
look better if there's just a little space between cards.
Here's the code that finds the height and width, and then adds 2
pixels to each dimension.
# Save the height and width of the cards to make the code easier
# to read.
set height [image height [lindex $concentration(cards) 0]]
set width [image width [lindex $concentration(cards) 0]]
# Leave spaces between cards.
incr width 2
incr height 2
|
There's one more thing before we can start drawing the cards on the
canvas. We need to tell Tcl/Tk how to react when someone mouse-clicks
a card.
Buttons always do things when we click them. We define what we want the
button to do with the -command
option.
In Tchuka Ruma, we used the bind
to make a canvas
do something when we clicked it, so it would behave like a button.
In this version of concentration, we've only got one canvas, and we
want to do something different with each image we create on it.
As you might expect, we can do this.
Tcl/Tk lets us tell any widget to watch for an event and do something
when that event happens. That's what the bind
command
does.
The Tcl/Tk canvas has a command built into it that lets us tell
every item that we create on the canvas to watch for events and
do something when the event occurs.
In Tchuka Ruma We added a binding to the canvas with
code like this:
# Create and grid a canvas widget
canvas .c_1 -width 80 -height 118 -background blue
grid .c_1
# Tell the canvas named .c_1 to watch for a mouse-button event
# and create a messageBox when the user clicks this canvas.
bind .c_1 "tk_messageBox -type ok -message {One}"
|
We can create an oval on a canvas and add a binding to it with
code like this:
# Create and grid a canvas widget
canvas .c_2 -width 80 -height 118 -background yellow
grid .c_2
# Tell the canvas named .c_2 to watch for a mouse-button event
# when the cursor is over items with the tag "sample".
# Open a message box when a user clicks an item tagged "sample"
.c_2 create oval 20 20 60 60 -tag sample -fill red
.c_2 bind sample "tk_messageBox -type ok -message {Two}"
|
Try typing these two examples into Komodo Edit and see how they behave.
You'll notice that you can click anyplace on the blue canvas to get a
message box with One in it, but you need to click in the red
circle to get a message box in the yellow canvas.
Here's the startGame
procedure. It begins with the same
code we used in the previous game by clearing the indices in the global
array concentration. Then it gets the sizes for the cards and
starts creating the images on the canvas.
Look closely at the code that creates the images. We only use two
coordinates to place an image, rather than the bounding rectangle that
we use for ovals and rectangles.
The reason for this is that the size of an image is fixed. It will be
as big as it is, no bigger, no smaller.
We only need to tell Tcl/Tk where to put the image, not the size of the
image. We can define a location with just two coordinates, an X
coordinate and a Y coordinate. A pair of coordinates specifies a location
on the canvas, but it doesn't quite define where to put the image. The image
covers some area, and you can put the image in a lot of places
and still overlap that X/Y coordinate.
Tcl/Tk centers the image around the coordinate we give it. So, if we
have an image that's 20 pixels wide and 40 pixels tall and we tell
Tcl/Tk to create this image at X location 30 and Y location 50, the
image will cover the rectangle that starts at 20, 30 (X Location 30 -
1/2 the width of the image, Y Location 50-1/2 the height of the image)
to 40,70 (30 + 10, and 50+20).
We can change this behavior with the -anchor
option. We give
the -anchor
one or more sides of the image to put at
the X/Y location. The sides are defined as north, south,
east, and west.
For instance if we create the image with an option like -anchor w
,
Tcl/Tk will put the center of the left (west) edge on that X/Y location.
This procedure uses -anchor nw
to tell Tcl/Tk to put the upper
left (North West) corner of the image at the coordinates.
Also look at how this procedure calculates where to put the images. We
start out 2 pixels down and 2 pixels in from the upper left corner of
the canvas, Each time we create an image, we add the width of the
image to the X location. That gives us a new X location just to the
right of the image we created.
When the X location is more than 8 times the image width, we know we've
put up 8 columns of images, and it's time to step to a new row and reset the
X location back to 2.
Also notice that we use the canvas bind
command to tell
each image to watch for a button release event, and call
playerTurn
with the position of this card in the list
when that event happens.
################################################################
# proc startGame {}--
# Actually start a game running
# Arguments
# NONE
#
# Results
# initializes per-game indices in the global array "concentration"
# The card list is randomized
# The GUI is modified.
#
proc startGame {} {
global concentration
set concentration(player,score) 0
set concentration(turn) 0
set concentration(selected,rank) {}
set concentration(cards) [randomizeList $concentration(cards)]
# Save the height and width of the cards to make the code easier
# to read.
set height [image height [lindex $concentration(cards) 0]]
set width [image width [lindex $concentration(cards) 0]]
# Leave spaces between cards.
incr width 2
incr height 2
# Remove any existing items on the canvas
.game delete all
# Start in the upper left hand corner
set x 2
set y 2
# Step through the list of cards
for {set pos 0} {$pos < [llength $concentration(cards)]} {incr pos} {
# Place the back-of-a-card image on the board
# to simulate a card that is face-down.
.game create image $x $y -image back -anchor nw -tag card_$pos
# Add a binding on the card back to react
# to a player left-clicking the back.
.game bind card_$pos <ButtonRelease-1> "playerTurn $pos"
# Step to the next column (the width of a card)
incr x $width
# If we've put up 8 columns of cards, reset X to the
# far left, and step down one row.
if {$x >= [expr $width * 8] } {
set x 2
incr y $height
}
}
}
|
The playerTurn
procedure also needs a few changes.
The last version of the concentration game used each button's
configure
command to change the image that was being
displayed. Any option that we can define when we create a widget
can be changed later with the configure
command.
We can do the same thing with items on a canvas with the canvas's
itemconfigure
command.
The syntax for the itemconfigure
command is:
canvasName
| The name of the canvas that has this item on it.
|
itemconfigure
| Change the configuration of an item on this canvas.
|
tagOrId
| A Tag or other identifier to specify which item or items to configure
|
-option value
| Pairs of options and a new value to set. You can change multiple
options with a single itemconfigure command.
|
Here's an example that builds a canvas, creates a rectangle on it,
and then changes the color of the rectangle when you click the button.
# Create and grid a canvas widget
canvas .c
grid .c
# Create a red rectangle
.c create rectangle 20 20 100 100 -fill red -tag changeMe
# Create and grid a button that can change the color of
# the rectangle on the canvas
button .b -text "change color" -command ".c itemconfigure changeMe -fill blue"
grid .b
|
Any option that we can define when we create an item on the canvas can
be modified later with the itemconfigure command. We can change the
colors of things we've drawn, change the width of lines, and even
change the image that's displayed when we use the canvas create
image
command.
The new playerTurn
procedure looks like this.
Notice that after a player finds a match, we configure the image to be
blank and use the canvas bind
command to redefine the
action to take when a button up event occurs to be an empty string.
Assigning an empty command means to do nothing. That's the same as
removing the binding.
################################################################
# proc playerTurn {position}--
# Selects a card for comparison, or checks the current
# card against a previous selection.
# Arguments
# position The position of this card in the deck.
#
# Results
# The selection fields of the global array "concentration"
# are modified.
# The GUI is modified.
#
proc playerTurn {position} {
global concentration
set card [lindex $concentration(cards) $position]
.game itemconfigure card_$position -image $card
set rank [lindex [split $card _] 1]
# If concentration(selected,rank) is empty, this is the first
# part of a turn. Mark this card as selected and we're done.
if {{} eq $concentration(selected,rank)} {
# Increment the turn counter
incr concentration(turn)
set concentration(selected,rank) $rank
set concentration(selected,position) $position
set concentration(selected,card) $card
} else {
# If we're here, then this is the second part of a turn.
# Compare the rank of this card to the previously saved rank.
if {$position == $concentration(selected,position)} {
return
}
# Update the screen *NOW* (to show the card), and pause for one second.
update idle
after 1000
# If the ranks are identical, handle the match condition
if {$rank eq $concentration(selected,rank)} {
# Increase the score by one
incr concentration(player,score)
# Remove the two cards and their backs from the board
.game itemconfigure card_$position -image blank
.game itemconfigure card_$concentration(selected,position) -image blank
.game bind card_$position <ButtonRelease-1> ""
.game bind card_$concentration(selected,position) <ButtonRelease-1> ""
# Check to see if we've won yet.
if {[checkForFinished]} {
endGame
}
} else {
# If we're here, the cards were not a match.
# configure the cards to be back up (turn the cards face down)
.game itemconfigure card_$position -image back
.game itemconfigure card_$concentration(selected,position) -image back
}
# Whether or not we had a match, reset the concentration(selected,rank)
# to an empty string so that the next click will be a select.
set concentration(selected,rank) {}
}
}
|
The last thing to do is modify the endGame
procedure.
In the previous version of this game, we used the configure
command to make all the buttons show the card images. We can do the
same thing with the canvas itemconfigure
command.
We'll want to have a set of buttons to ask the player if they
want to play again or quit. We can build the buttons just like
we would normally, but instead of using the grid
command
to display them, we can put them directly on the canvas.
Just like we use the create image
command to put an
image on the canvas, we use the create window
command
to put a widget on the canvas.
It's hard to see the two buttons when they are just sitting on the
canvas. There are a lot of cards and stuff that keep the eye too
busy.
This code creates a rectangle under the buttons to will make
them stand out. We use a -stipple
option with the
rectangle to allow the cards under the rectangle to still be seen.
A stipple means that you have an image with lots of empty
spaces in it.
Artists have drawn pictures using stipples since almost forever.
Drawing a picture with stipples means you just draw little dots, but
never lines. This lets an artist who only has one color of ink show
different darkness levels in their pictures. For instance, an artist
with only black ink would color a section gray by putting lots of tiny
black dots in that section. This technique was used very often in the
early days of printing when you only had one color of ink to print
with and the artist wanted to show gray tones. Even today, if you
look at a newspaper picture under a magnifying glass, you'll see it's
made up of lots of little dots.
In Tcl/Tk terms, a stipple pattern is a set of pixels where the canvas
command will draw dots, and pixels where it won't. The areas where a
dot isn't drawn will show whatever is under the rectangle (or oval).
Take a look at the .game create rectangle
command in
this procedure. It uses several options we haven't used yet.
-
-stipple
- A stipple pattern puts colors on some pixels and leaves other
pixels transparent so you can see the canvas and images underneath.
The common stipple patterns are
gray50
, gray25
and gray75
.
-fill
- Tells Tcl what color to use to fill a rectangle, oval or text item.
-outline
- Tells Tcl what color to make the outline of a rectangle or oval.
-width
- Tells Tcl how wide to make the outline.
The code for the endGame
procedure looks like this:
proc endGame {}--
# Provide end of game display and ask about a new game
# Arguments
# NONE
#
# Results
# GUI is modified
#
proc endGame {} {
global concentration
for {set pos 0} {$pos < [llength $concentration(cards)]} {incr pos} {
.game itemconfigure card_$pos -image [lindex $concentration(cards) $pos]
}
# Update the screen *NOW*, and pause for 2 seconds
update idle;
after 2000
.game create rectangle 250 250 450 400 -fill blue \
-stipple gray50 -width 3 -outline gray
button .again -text "Play Again" -command {
destroy .again
destroy .quit
startGame}
button .quit -text "Quit" -command "exit"
.game create window 350 300 -window .again
.game create window 350 350 -window .quit
}
|
Here's the complete code for a concentration game.
################################################################
# proc loadImages {}--
# Load the card images
# Arguments
# NONE
#
# Results
# The global array "concentration" is modified to include a
# list of card image names
#
proc loadImages {} {
global concentration
# The card image fileNames are named as S_V.gif where
# S is a single letter for suit (Hearts, Diamonds, Spades, Clubs)
# V is a 1 or 2 character descriptor of the suit - one of:
# a k q j 10 9 8 7 6 5 4 3 2
#
# glob returns a list of fileNames that match the pattern - *_*.gif
# means all fileNames that have a underbar in the name, and a .gif extension.
foreach fileName [glob *_*.gif] {
# We discard the aces to leave 48 cards because that makes a
# 6x8 card display.
if {($fileName ne "c_a.gif") &&
($fileName ne "h_a.gif") &&
($fileName ne "d_a.gif") &&
($fileName ne "s_a.gif")} {
# split the card name (c_8) from the suffix (.gif)
set card [lindex [split $fileName .] 0]
# Create an image with the card name, using the file
# and save it in a list of card images: concentration(cards)
image create photo $card -file $fileName
lappend concentration(cards) $card
}
}
# Load the images to use for the card back and
# for blank cards
foreach fileName {blank.gif back.gif} {
# split the card name from the suffix (.gif)
set card [lindex [split $fileName .] 0]
# Create the image
image create photo $card -file $fileName
}
}
################################################################
# proc randomizeList {}--
# Change the order of the cards in the list
# Arguments
# originalList The list to be shuffled
#
# Results
# The concentration(cards) list is changed - no cards will be lost
# of added, but the order will be random.
#
proc randomizeList {originalList} {
# How many cards are we playing with.
set listLength [llength $originalList]
# Initialize a new (random) list to be empty
set newList {}
# Loop for as many cards as are in the card list at the
# start. We remove one card on each pass through the loop.
for {set i $listLength} {$i > 0} {incr i -1} {
# Select a random card from the remaining cards.
set p1 [expr int(rand() * $i)]
# Put that card onto the new list of cards
lappend newList [lindex $originalList $p1]
# Remove that card from the card list.
set originalList [lreplace $originalList $p1 $p1]
}
# Replace the empty list of cards with the new list that's got all
# the cards in it.
return $newList
}
################################################################
# proc makeGameBoard {}--
# Create the game board widgets - canvas and labels.
# Arguments
# NONE
#
# Results
# New GUI widgets are created.
#
proc makeGameBoard {} {
# Create and grid the canvas that will hold the card images
canvas .game -width 718 -height 724 -bg gray
grid .game -row 1 -column 1 -columnspan 4
# Create and grid the labels for turns and score
label .lscoreLabel -text "Score"
label .lscore -textvariable concentration(player,score)
label .lturnLabel -text "Turn"
label .lturn -textvariable concentration(turn)
grid .lscoreLabel -row 0 -column 1 -sticky e
grid .lscore -row 0 -column 2 -sticky w
grid .lturnLabel -row 0 -column 3 -sticky e
grid .lturn -row 0 -column 4 -sticky w
}
################################################################
# proc startGame {}--
# Actually start a game running
# Arguments
# NONE
#
# Results
# initializes per-game indices in the global array "concentration"
# The card list is randomized
# The GUI is modified.
#
proc startGame {} {
global concentration
set concentration(player,score) 0
set concentration(turn) 0
set concentration(selected,rank) {}
set concentration(cards) [randomizeList $concentration(cards)]
# Save the height and width of the cards to make the code easier
# to read.
set height [image height [lindex $concentration(cards) 0]]
set width [image width [lindex $concentration(cards) 0]]
# Leave spaces between cards.
incr width 2
incr height 2
# Remove any existing items on the canvas
.game delete all
# Start in the upper left hand corner
set x 2
set y 2
# Step through the list of cards
for {set pos 0} {$pos < [llength $concentration(cards)]} {incr pos} {
# Place the back-of-a-card image on the board
# to simulate a card that is face-down.
.game create image $x $y -image back -anchor nw -tag card_$pos
# Add a binding on the card back to react
# to a player left-clicking the back.
.game bind card_$pos <ButtonRelease-1> "playerTurn $pos"
# Step to the next column (the width of a card)
incr x $width
# If we've put up 8 columns of cards, reset X to the
# far left, and step down one row.
if {$x >= [expr $width * 8] } {
set x 2
incr y $height
}
}
}
################################################################
# proc playerTurn {position}--
# Selects a card for comparison, or checks the current
# card against a previous selection.
# Arguments
# position The position of this card in the deck.
#
# Results
# The selection fields of the global array "concentration"
# are modified.
# The GUI is modified.
#
proc playerTurn {position} {
global concentration
set card [lindex $concentration(cards) $position]
.game itemconfigure card_$position -image $card
set rank [lindex [split $card _] 1]
# If concentration(selected,rank) is empty, this is the first
# part of a turn. Mark this card as selected and we're done.
if {{} eq $concentration(selected,rank)} {
# Increment the turn counter
incr concentration(turn)
set concentration(selected,rank) $rank
set concentration(selected,position) $position
set concentration(selected,card) $card
} else {
# If we're here, then this is the second part of a turn.
# Compare the rank of this card to the previously saved rank.
if {$position == $concentration(selected,position)} {
return
}
# Update the screen *NOW* (to show the card), and pause for one second.
update idle
after 1000
# If the ranks are identical, handle the match condition
if {$rank eq $concentration(selected,rank)} {
# Increase the score by one
incr concentration(player,score)
# Remove the two cards and their backs from the board
.game itemconfigure card_$position -image blank
.game itemconfigure card_$concentration(selected,position) -image blank
.game bind card_$position <ButtonRelease-1> ""
.game bind card_$concentration(selected,position) <ButtonRelease-1> ""
# Check to see if we've won yet.
if {[checkForFinished]} {
endGame
}
} else {
# If we're here, the cards were not a match.
# configure the cards to be back up (turn the cards face down)
.game itemconfigure card_$position -image back
.game itemconfigure card_$concentration(selected,position) -image back
}
# Whether or not we had a match, reset the concentration(selected,rank)
# to an empty string so that the next click will be a select.
set concentration(selected,rank) {}
}
}
################################################################
# proc checkForFinished {}--
# checks to see if the game is won. Returns true/false
# Arguments
#
#
# Results
#
#
proc checkForFinished {} {
global concentration
if {$concentration(player,score) == 24} {
return TRUE
} else {
return FALSE
}
}
################################################################
# proc endGame {}--
# Provide end of game display and ask about a new game
# Arguments
# NONE
#
# Results
# GUI is modified
#
proc endGame {} {
global concentration
for {set pos 0} {$pos < [llength $concentration(cards)]} {incr pos} {
.game itemconfigure card_$pos -image [lindex $concentration(cards) $pos]
}
# Update the screen *NOW*, and pause for 2 seconds
update idle;
after 2000
.game create rectangle 250 250 450 400 -fill blue \
-stipple gray50 -width 3 -outline gray
button .again -text "Play Again" -command {
destroy .again
destroy .quit
startGame}
button .quit -text "Quit" -command "exit"
.game create window 350 300 -window .again
.game create window 350 350 -window .quit
}
loadImages
makeGameBoard
startGame
|
Type that code (or copy/paste) it into Komodo Edit and see how it plays.
It won't seem very much different from the version of concentration
we did with the buttons.
Play with the rectangle in the endGame
procedure. Try
changing the stipple pattern, color, and widths. Try putting one
rectangle over another with different stipple patterns and colors.
You can create a lot of interesting visual effects
Change 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.
We learned a few new things we can do with Tcl/Tk in this lesson.
The important points are:
- The
image height
command will return the height of an image.
- The
image width
command will return the width of an image.
- We can put bindings on items we create on a canvas, just as we can
put bindings on widgets.
- The
itemconfigure
command will modify one or more items
on a canvas.
- We can place images on a canvas with the canvas
create image
command.
- We can place widgets on a canvas with the canvas
create window
command.
- The image or window we place on a canvas will be centered on the coordinates
we put in the command unless we use a
-anchor
option.
- Using a stipple pattern in a rectangle or oval
will let whatever is under the rectangle or oval show through.
That makes a decent game. The next couple of lessons will discuss
some ways to make the game look cooler by showing cards moving with
simple animations.
Simple animations are a step towards making arcade style games like
PacMan or asteroids.
Copyright 2007 Clif Flynt