Previous

Moving Images

Next

The last concentration game made the cards blank once we found a match. In a real game, we'd move the cards off the board and have a stack of cards we've won on our side of the board.

We can do that with the computer version of concentration, too.

Here's the things we'll have to change:

  1. makeGameBoard : Make the canvas wider to hold the stack of cards we've found.
  2. startGame : Add initialization for where to put the cards.
  3. playerTurn Change what's done after we find a match.

Let's start with the makeGameBoard procedure. We can change the makeGameBoard procedure without changing any other code. That means we can test the new code to be sure it's working before we change other code.

We can show the stack of cards we've found on one side of the board, and the the cards in play on the other side. All we need to do for this is to make the canvas wider.

The new game will look something like this:

Notice that the cards that have been won are stacked so we can see the rank of each pair. We'll add another index to the concentration array to keep track of where the cards go.

Here's the new makeGameBoard procedure. The big change was to make the canvas wider. I changed the names of the labels because we'll be letting the computer play in the next lesson, and we'll need new labels (with new names) for that.


################################################################
# 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
}

The changes to startGame are also simple. We just need some new array indices to keep track of where to put the cards as we find matches.

We'll need to keep track of how many cards have been found, so we know how far down to put the new pairs we find. That means we need a variable that will stay in memory and never go away until we exit the program.

We call variables that don't go away persistent variables.

The global variables we first looked at in lesson 12 are persistent variables.

So, we'll use a global variable to keep track of the Y location, since we'll be changing that every time we put new cards on the stack, but we don't want it to vanish in between tims. We could keep the X location constant (since it never changes), but it's generally a better idea to not hardcode numbers in a procedure. If you put the numbers in a global variable, you can modify the program more easily when you want to change something.

We can keep X and Y location data in the global array (concentration). We'll use these new indices:

concentration(player,x) X location for cards that have been matched. This will stay the same throughout the game.
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:


Teaser

Previous

Next


Copyright 2007 Clif Flynt