concentration(player,y)
| Y location for cards that have been matched. This will be increased
each time we find a match.
The cards are about 88 pixels wide, so lets make a space 90 pixels
wide to hold the stack of matched cards. This means we need to
change the start X location for laying out the cards.
Here's the new procedure:
################################################################
# 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(player,x) 2
set concentration(player,y) 2
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
incr height
# Remove any existing items on the canvas
.game delete all
# Start in the upper left hand corner
set x 90
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 90 + ($width * 8)] } {
set x 90
incr y $height
}
}
}
|
That's two down, and one to go. We can test our code after this change,
since none of the changes we've made so far interact with anything.
We make one other change to the playerTurn procedure. Instead of
making the cards blank when we find a match, we'll move them to
player's I found it! stack.
We'll do this with a new procedure named moveCards .
The moveCards procedure uses a couple of new canvas
commands.
We'll look at the second command we use first.
The second command is the coords command. Just like
we can change the attributes of a canvas item (things like -width
or -fill ) with the itemconfigure command,
we can change an items coordinates after it's been put on the canvas.
The canvas coords command comes in two flavors. In
the plain vanilla flavor, it will tell us what the coordinates of
an object are.
Try typing this code into Komodo Edit and see how it works.
canvas .c
grid .c
.c create rectangle 20 20 50 50 -fill red -tag redBox
set coordinates [.c coords redBox]
tk_messageBox -type ok -message "Coordinates of the red box are: $coordinates"
|
You should see a message box that looks like this:
The other flavor of coords (tutti fruity?) is when you
provide the new coordinates for the canvas item, like this:
canvas .c
grid .c
.c create rectangle 20 20 50 50 -fill red -tag redBox
button .b -text "MoveBox" \
-command {.c coords redBox 30 30 60 60}
grid .b
|
When you click the button the red box will move.
Now try this code:
canvas .c
grid .c
.c create rectangle 20 20 50 50 -fill red -tag redBox
button .b -text "MoveBox" \
-command {.c coords redBox 30 30 40 70}
grid .b
|
When you click the button the red box will move and change shape.
When we create an item on the canvas we can define a lot of options
like -fill . Later we can change these with the
canvas itemconfigure command.
In the same way, we can define the coordinates for an item when we
create it and we can change the coordinates later with the
canvas coords command.
We can use the coords command to move a card on the
canvas, like we used the itemconfigure and image commands to animate a
card flipping.
When we make items on a canvas, Tcl/Tk keeps track of the order that we
create them. The canvas puts the first one we create on the bottom and
piles newer ones on top. This is like cutting out shapes from a sheet
of paper and laying them down on a counter, the last ones go on top of
the first ones.
With paper on a countertop, you only worry about papers that are
actually on top of each other. If you move the papers around,
you'd put the last one you moved on the top.
Tcl/Tk keeps track of the order of canvas items all the time. If
you move something you created first over something you created
second, the one you created second will be on top. It's as if you
slid the first item under it.
When we're moving the cards, we don't want them to slide under some cards
and over other cards on the canvas. We want them to slide over all
the other cards.
Like everything else, we can change the order of the items on the
canvas after we've created them.
To raise a canvas item to the top of the stack, we just need to
tell the canvas what item to raise, like this:
canvasName
| The name of the canvas with an item we want to raise
| raise
| We want to raise this item
| ItemID
| An identifier to specify which item gets raised.
|
Here's a simple example of using the canvas raise command
to change the order of things on the canvas:
canvas .c
grid .c
.c create rectangle 20 20 40 40 -fill red -tag redBox
.c create rectangle 30 30 50 50 -fill blue -tag blueBox
button .br -text "Raise Red" -command {.c raise redBox}
grid .br
button .bb -text "Raise Blue" -command {.c raise blueBox}
grid .bb
|
Type this into Komodo Edit and see how it works. Try adding yellow,
green and purple boxes that overlap. Then create buttons to raise
each of the boxes.
Here's the code that raises a card to the top of the display, then
moves it to the side of the board. Notice that we pass the
moveCards procedure a prefix argument.
Whenever we call moveCards procedure in this code, the
prefix will be player, so $prefix,x will
reference the player,x index.
We're adding this parameter
so we can use this procedure in the next lesson to move cards to the
computer's stack when it finds a match.
################################################################
# proc moveCards {cvs id1 id2 prefix}--
# moves Cards from their current location to the
# score pile for
# Arguments
# id1 An identifier for a canvas item
# id2 An identifier for a canvas item
# prefix Identifier for which location should get the card
#
# Results
#
#
proc moveCards {id1 id2 prefix} {
global concentration
.game raise $id1
.game raise $id2
.game coords $id1 $concentration($prefix,x) $concentration($prefix,y)
.game coords $id2 $concentration($prefix,x) $concentration($prefix,y)
incr concentration($prefix,y) 30
}
|
This is so simple, that we could put the code to move the cards right
in the playerTurn and computerTurn
procedures. It's better practice to make a procedure, rather
than duplicate the same code in two procedures.
For instance, if we decide to do something fancier to move
the cards - like perhaps show them sliding across the canvas,
we can do that by changing the moveCards procedure.
If we merged the coords commands into the playerTurn
and computerTurn procedures, we'd need to change two sets
of code.
Twice as much typing, twice as much changing, and twice as many
chances to make a mistake.
And that's all the changes. Here's the complete 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 800 -height 724 -bg gray
grid .game -row 1 -column 1 -columnspan 6
# Create and grid the labels for turns and score
label .lmyScoreLabel -text "My Score"
label .lmyScore -textvariable concentration(player,score)
label .lturnLabel -text "Turn"
label .lturn -textvariable concentration(turn)
grid .lmyScoreLabel -row 0 -column 1 -sticky e
grid .lmyScore -row 0 -column 2 -sticky w
grid .lturnLabel -row 0 -column 5 -sticky e
grid .lturn -row 0 -column 6 -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(player,x) 0
set concentration(player,y) 2
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
incr height
# Remove any existing items on the canvas
.game delete all
# Start in the upper left hand corner
set x 90
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 90 + ($width * 8)] } {
set x 90
incr y $height
}
}
}
################################################################
# proc flipImageX {canvas canvasID start end background}--
# Makes it appear that an image object on a canvas is being flipped
# Arguments
# canvas The canvas holding the image
# canvasID The identifier for this canvas item
# start The initial image being displayed
# end The final image to display
# background The color to show behind the image being flipped.
# This is probably the canvas background color
#
# Results
# configuration for the canvas item is modified.
#
proc flipImageX {canvas canvasID start end background} {
global concentration
# Get the height/width of the image we'll be using
set height [image height $start]
set width [image width $start]
# The image will rotate around the X axis
# Calculate and save the center, since we'll be using it a lot
set centerX [expr $width / 2]
# Create a new temp image that we'll be modifying.
image create photo temp -height $height -width $width
# Copy the initial image into our temp image, and configure the
# canvas to show our temp image, instead of the original image
# in this location.
temp copy $start
$canvas itemconfigure $canvasID -image temp
update idle
after 25
# copy the start image into the temp with greater
# subsampling (making it appear like more and more of an
# edge view of the image).
# Move the start of the image to the center on each pass
# through the loop
for {set i 2} {$i < 8} {incr i} {
set left [expr $centerX - $width / (2 * $i)]
set right [expr $centerX + $width / (2 * $i)]
temp put $background -to 0 0 $width $height
temp copy -to $left 0 $right $height -subsample $i 1 $start
update idle
after 10
}
# copy the end image into the temp with less and less
# subsampling (making it appear like less and less of an
# edge view of the image).
# Move the start of the image away from thecenter on each pass
# through the loop
for {set i 8} {$i > 1} {incr i -1} {
set left [expr $centerX - $width / (2 * $i)]
set right [expr $centerX + $width / (2 * $i)]
temp put $background -to 0 0 $width $height
temp copy -to $left 0 $right $height -subsample $i 1 $end
update idle
after 10
}
# configure the canvas to show the final image, and
# delete our temporary image
$canvas itemconfigure $canvasID -image $end
image delete temp
}
################################################################
# 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]
flipImageX .game card_$position back $card gray
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 bind card_$position <ButtonRelease-1> ""
.game bind card_$concentration(selected,position) <ButtonRelease-1> ""
moveCards card_$position \
card_$concentration(selected,position) player
# Check to see if we've won yet.
if {[checkForFinished]} {
endGame
}
} else {
# If we're here, the cards were not a match.
# flip the cards to back up (turn the cards face down)
flipImageX .game card_$position $card back gray
flipImageX .game card_$concentration(selected,position) \
$concentration(selected,card) back gray
}
# 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 moveCards {cvs id1 id2 prefix}--
# moves Cards from their current location to the
# score pile for
# Arguments
# id1 An identifier for a canvas item
# id2 An identifier for a canvas item
# prefix Identifier for which location should get the card
#
# Results
#
#
proc moveCards {id1 id2 prefix} {
global concentration
.game raise $id1
.game raise $id2
.game coords $id1 $concentration($prefix,x) $concentration($prefix,y)
.game coords $id2 $concentration($prefix,x) $concentration($prefix,y)
incr concentration($prefix,y) 30
}
################################################################
# proc checkForFinished {}--
# checks to see if the game is won. Returns true/false
# Arguments
#
#
# Results
#
#
proc checkForFinished {} {
global concentration
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
set position 0
foreach card $concentration(cards) {
.game itemconfigure card_$position -image $card
incr position
}
# 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 or copy/paste that code into Komodo Edit and run it.
Change the moveCard procedure to stagger the cards in
the X dimension. Make it change the concentration(player,x)
value from 0 to 4 and back after each match.
You can do this with an if command in the moveCard
procedure.
The new things we learned in this lesson are:
- You can change the location of an object on the canvas with the canvas
coords command.
- Items created on a canvas remember the order they were created in and know
which one can be on top of another.
- We can move a canvas item to the top of the display with the canvas
raise command.
-
Teaser
Copyright 2007 Clif Flynt
|