REALbasic University Resources:

RBU: Glossary Defines common REALbasic programming terms
  Archives Previously published columns
Translations: Dutch Courtesy of Floris van Sandwijk
  Japanese Courtesy of Kazuo Ishizuka
  Chinese Courtesy of Dong Li
  RBU Translation Guide Information on Translating RBU into other languages
Books: Matt's Book (2nd Edition!) Ideal for experienced programmers
  Erick's Book Best for beginning programmers
Websites: Mother Ship The publisher of REALbasic
  RB Webring Links to hundreds of REALbasic websites
  RESExcellence Another REALbasic programming column
  REALbasic Developer Magazine The premiere source for REALbasic instruction.

REALbasic University is Sponsored by

Make your Mac do what YOU want it to. Create games, utilities, cool Mac OS X tricks. Download REALbasic now and create your own software.


Print This Article

REALbasic University: Column 031

RBU Pyramid IV

Last week we got our game to draw some cards, but they weren't shuffled and the player can't select cards. We'll get that working today.

Shuffling and Dealing

Let's start by creating a newGame routine which will initialize the variables needed for a new game. Open globalsModule (double-click on it) and add a new method (Edit menu, "New Method"). Call it newGame and put in the following code:

  
// Distribute a fresh deck of cards
freshDeck

Okay, yeah, nothing much is going on here... yet. We'll be adding to this later. For instance, we'll eventually need a variable to keep track of the player's score, and that would be initialized to zero here. For now, though, we just want to get a bare bones version of the game working. The key routine we need now is the freshDeck routine, which generates a new, shuffled deck of cards.

Let's create it. Add in another method and call it freshDeck. Here's the code:

  
dim j, i as integer

// Build a full deck and shuffle it
shuffleDeck

// Establish cards on board
for i = 1 to 28
gameWindow.cardCanvas(i).card = gTheDeck(i)
next

// Remove those cards from the full deck
for i = 1 to 28
gTheDeck.remove 1
next

// Initialize all the cards
for i = 1 to 31
gameWindow.cardCanvas(i).clickable = false
gameWindow.cardCanvas(i).selected = false
gameWindow.cardCanvas(i).visible = true
gameWindow.cardCanvas(i).topCard = false
gameWindow.cardCanvas(i).highlight = false
next

// Erase the discard
redim gTheDiscard(0)
gTempDiscard.card = 0
gDiscard.card = 0
gDeck.card = 0

If you read through this code you can see it does more than just shuffle the cards: it shuffles them, deals out 28 cards to the pyramid, builds the Deck and initializes the discard piles, and finally sets several custom properties of the cards such as whether or not the cards are visible, selected, etc. Overall it's fairly routine stuff, but very important. All this stuff must be set up exactly like this at the beginning of any game, or things won't work correctly.

Once we've done the above, we're almost done, except we must call our new newGame method. Go to the Open event of the app class we added last week and make it look like this:

  
// Initialize and start a new game
init
newGame

Excellent. You should be able to run the program now and if you've done everything correctly, you'll see a screen full of shuffled cards this time:

Selecting and Deselecting

That's great, but we still can't select any of the cards. How do we get that working?

Well, the raw selecting and deselecting is easy: we actually did most of the work last week in the cardCanvas' Paint event. That's where we check to see if a card is selected or not (by examining our .selected custom property) and draw it shaded if it's selected.

In fact, we could just add these two lines of code to cardCanvas's MouseDown event to enable selection/deselection:

  
cardCanvas(index).selected = not cardCanvas(index).selected
cardCanvas(index).refresh

Run the program and you'll see that yes, you can indeed select and deselect cards. However, the process isn't intelligent, and it not only lets you select multiple cards, it lets you select cards that shouldn't be allowed according to the rules of pyramid!

We've got to make the selection process more intelligent, and here things start to get a little complicated. Before we begin, let's do a little forethinking about what we need to happen. I've already written the code, of course, but this will give you an idea of the thinking process that I went through to generate that code: it didn't just magically appear.

Visualize the game of pyramid. How does the selection process work? For starters, we know that only the top-most cards are selectable. Next, we know that no more than one card can be selected at a time: if a second card is selected it is either a match to the first and both are removed, or it is an invalid match and the first card is deselected and the second card remains selected (effectively switching our selection). If it's a match, we've got to deal with removing those cards.

Then we've also got our Deck, which deals out a new card when clicked, as well as the discard piles.

Let's think about these issues from a programming standpoint. The first problem, that of only allowing the selection of the top-most cards, is fairly straightforward. We already have a boolean (true/false) property for each of our cardCanvas objects which tells us if a card is clickable or not. All we need is a routine that will scan through the cards and set the appropriate cards to clickable or not.

To solve the second part of the problem, that of multiple selection, we'll need some way to save the current (previous) selection. How else would we know that a card has been selected or which card it was?

This is easily solved by adding a global property which contains the currently selected card. Open globalsModule and add this property (Edit menu, "New Property"):

 gSelection as integer 

We'll also need routines that handle card matches, so let's add them now. Go back to the gameWindow Code Editor and add three methods: cardMatch, kingMatch, and discardMatch. For each method, include this parameter: index as integer.

For now, we'll pretty much leave these routines empty, but they give us a way to include calls as appropriate within our selection routine. Just so we can see what's happening, let's add msgBox "Match!" to the cardMatch method.

We're now ready to write our main selection routine. All this code goes into cardCanvas' MouseDown event (replace the two lines of temporary code we used earlier):

   
// Card is clickable and is not the deck
if cardCanvas(index).clickable and index <> 29 then
cardCanvas(index).selected = not cardCanvas(index).selected
cardCanvas(index).refresh

// Previous card is valid and is not the same as the current card
if gSelection <> -1 and index <> gSelection then

// Are both selected?
if cardCanvas(gSelection).selected and cardCanvas(index).selected then

// See if there's a match
if gCards(cardCanvas(index).card).number+gCards(cardCanvas(gSelection).card).number=13 then

// Match! Is one of the cards a discard pile card?
if index <> 30 and index <> 31 and gSelection <> 30 and gSelection <> 31 then
// No, so "delete" both cards and up the score
cardMatch(index)
else
discardMatch(index)
end if //

return true
else
// not a match, so we deselect old card
cardCanvas(gSelection).selected = false
cardCanvas(gSelection).refresh
end if // A match!

end if // both are selected
end if // gSelection <> -1 and index <> gSelection

// Delete King
if gCards(cardCanvas(index).card).number = 13 then
cardKing(index)
return true
end if // is a King


if cardCanvas(index).selected then
gSelection = index
end if
return true
end if // cardCanvas(index).clickable and index <> 29

This may look complicated and intimidating, but let's take it step-by-step. First, we simply check to see if the card clicked on is clickable -- if it is, we continue to analyze the selection to see what needs to be done. We also check at this point to make sure that the user didn't click on the Deck -- that's a completely different problem we'll deal with later.

Once we've got a valid click, we toggle the state of the current card (select it if it's unselected, deselect it if it's already selected). Next, we do a quick check to make sure that our saved selection variable, gSelection, contains a valid selection and is not the same as the current card.

Finally, we make sure that both our previous selection and the current card are selected, and if so, we check to see if there's a match. Finding out if there's a match is easy: we simply add the numbers of the two selected cards to see if they total 13.

Once we know there's a match, we check to see if one of the matched cards is on a discard pile. If it is, we handle that in our special discardMatch routine. Otherwise we pass the match to our cardMatch method.

If the match isn't valid, we simply deselect the previous card.

Finally, we check to see if the player kicked on a King, and if so, we handle removing it. Our final step is to set gSelection to the current card (if it's selected -- keep in mind it could have been toggled off).

If you run the program now, you may be disappointed. Nothing much will happen: none of the cards are clickable, and even though you put in code to handle matches, it doesn't seem to do anything.

That's because none of the cards are clickable. Remember how we initialized all the cards' clickable property to false in the freshDeck method? Since we've never done anything to make any of the clickable, we now can't click on any of them!

The solution is we need to write a routine that will update the state of all the cards, determining which are clickable. Add a new method (to gameWindow) and call it updateCards. Here's the code (it's a little complicated):

  
dim row, column, index as integer

//
// Routine to update which cards are clickable.
//

// First initialize all cards to off status
for index = 1 to 28
cardCanvas(index).clickable = false
cardCanvas(index).topCard = false
next // index

// First row cards are ALWAYS clickable
for index = 22 to 28
cardCanvas(index).clickable = true
cardCanvas(index).topCard = true
next // index

// This checks for clickable cards underneath the
// top cards (valid if the top cards are gone).
index = 0
for row = 1 to 6
for column = 1 to row
index = index+1
if (not cardCanvas(index+row).visible) and (not cardCanvas(index+row+1).visible) then
cardCanvas(index).clickable = true
cardCanvas(index).topCard = true
end if
next // column
next // row

index = 0
for row = 1 to 6
for column = 1 to row
index = index+1
// This checks for matches on top of each other (which are removable)
if (gCards(cardCanvas(index).card).number+gCards(cardCanvas(index+row).card).number=13) then
if (not cardCanvas(index+row+1).visible) and (cardCanvas(index+row).topCard) then
cardCanvas(index).clickable = true
end if
end if
if (gCards(cardCanvas(index).card).number+gCards(cardCanvas(index+row+1).card).number=13) then
if (not cardCanvas(index+row).visible) and (cardCanvas(index+row+1).topCard) then
cardCanvas(index).clickable = true
end if
end if
next // column
next // row

Yes, we are now getting into some complicated stuff!

The first parts of this routine are not too difficult: we first set all cards to unclickable, then turn on the top cards of the pyramid.

Then we start getting messy. We have two checks we need to do. First, we need to find the top-most cards and make them clickable. Remember, for RBU Pyramid, we never actually delete a cardCanvas object: all we do is make it invisible. So we can check to see if a card is top-most or not by looking at the cards above it and seeing if they're visible or not. If they're invisible, then the card underneath is clickable.

To actually figure out which cards on are top requires a little math. To figure out the formula, it's good to remember how our pyramid is built. Our pyramid structure is made of cardCanvas objects which are numbered 1 to 28, like this:

  
Row 1: 1
Row 2: 2 3
Row 3: 4 5 6
Row 4: 7 8 9 10
Row 5: 11 12 13 14 15
Row 6: 16 17 18 19 20 21
Row 7: 22 23 24 25 26 27 28

If you study the above, you may see there's a relationship between the row number and the card numbers on different rows. For example, the first row is row 1. If you add 1 to the card number, you end up with the card on the next row (card 2: 1 + 1 = 2). This also works on the second row: 2 + 2 = 4, and 4 is first card on the third row. Let's try it on the sixth row. The first card is 16. If we add 6 we get 22, which is the first card of the seventh row. It works!

This gives us a formula we can use to figure out which card is above the current card. If we step through the cards in a loop, we can look at the cards on top and see if they are invisible or not (that's the above not cardCanvas(index + row).visible [the left card on top] and not cardCanvas(index + row + 1).visible [the right card on top]). See? It's not so bad!

The second check is for the situation of where a matching pair overlap. Here the check is similar, but we can't have more than one card overlapping and the pair must be a match (total 13).

That should be about it, except that we need to call updateCards at the beginning of the game. Go back to freshDeck and add these lines to the very bottom:

    
gameWindow.updateCards
gameWindow.refresh

There, that calls updateCards so the state of selectable cards get established correctly. You should now be able to select cards and almost play the game.

As you can see, we've accomplished a great deal in the few lessons so far, but by now you should begin to see the scope of what we are doing.

There's still tons to do, of course: we haven't done anything with the card matching routines, so nothing much happens when there's a match. We also have been neglecting to do anything with the Deck and discard piles. That gives us something to do for next week.

If you would like the REALbasic project file for this week's tutorial, you may download it here.

Next Week

We continue with RBU Pyramid, making it so card matches can be removed.

Letters

Our first letter this week is from across the Atlantic:

I am following the RealBasic University with pleasure but, as someone who has been programming in BASIC since the 1980s (on Spectrums and Amstrads), I am having difficult seeing the advantages of using custom classes rather than simple arrays.

To define a global array of, say, people's names in the 'traditional' way, you would write something like:

  
[define the property in a module]
dim gName(10) as string
gName(1)= "John"

Using custom classes, you would need to write:

  
[create new class]
[define the property in the class]
dim gName(10) as nameClass
gName(1)=new nameClass
gName(1).name="John"

I can see the advantages for custom controls like editFields but, apart from grouping properties/variables together in one area (which you could do with modules), there does not seem an obvious advantage while there IS the clear disadvantage of extra steps and lines of code.

What other advantages ARE there in custom classes? I'd be happy switch to using them if I knew why I should.

Thanks,

Paul Thompson
Reading, UK

In your example, Paul, I'd use a simple array structure as well: you are correct that it's much easier. The advantage of using a custom class is when you have multiple pieces of information that benefit from being encapsulated into an object.

For example, which is easier to use:

  
dim nameArray(0) of string
dim streetArray(0) of string
dim cityArray(0) of string
dim stateArray(0) of string
dim zipArray(0) of string

// Add a record
nameArray.append "John"
streetArray.append "123 Main Street"
cityArray.append "Springfield"
stateArray.append "MO"
zipArray.append "65807"

// Delete 8th record
nameArray.remove 8
streetArray.remove 8
cityArray.remove 8
stateArray.remove 8
zipArray.remove 8

  
personClass:
name as string
street as string
city as string
state as string
zip as string

dim personArray(0) of personClass // person data
dim n as integer

// Add a record
n = uBound(personArray) + 1
redim personArray(n) // add one
personArray(n) = new personClass // Create new object

personArray(n).name = "John"
personArray(n).street = "123 Main Street"
personArray(n).city = "Springfield"
personArray(n).state = "MO"
personArray(n).zip = "65807"

// Delete 8th record
personArray.remove 8

As you can see, the two programming methods are similar, except the first uses multiple arrays while the second uses a single array of personClass objects. For some tasks, such as adding a new record, the process is similar though a couple steps more complicated for the custom class method. However, for other tasks, such as deleting a record, the custom class method is far simpler requiring a single line of code.

This becomes even more of an issue the more complicated your data structure. With the first method, it could be easy to lose track of an array's index: for example, if you forgot to delete a record from the zipArray field, suddenly your zipArray isn't the same size as the other fields and your data doesn't line up (i.e. the zip code for record 10 now lines up with record 11 of the other fields). That can be hairy to debug!

If your data structure is at all complicated, the custom class method will soon prove its value. For instance, in the above two examples, which would be easier to add another element to the client data structure (such as home telephone or id number)?

And what happens if your data's sub-structure is dynamic? Let's say this database structure we've created was a list of your salespeople and each had their own list of sales. Since the selling data is different for each person, you can't use a static array like your example. With the custom class solution you'd simple define a subfield, salesClass, like this:

  
salesClass:
saleDate as date
amount as double
product as string
quantity as integer

Then you'd simply add that to your original personClass definition:

  
personClass:
name as string
street as string
city as string
state as string
zip as string
sales(0) as salesClass

Now there's a separate array of sales for each person. You can't even do this with your method. (About the best you could do would be to use a multi-dimensional array with the array's second dimension being the sales data, but that would require that everyone have the same number of sales as the person with the most sales.) And I don't even want to think about how complicated this could get if you had multiple dynamic data to keep track of per person!

In conclusion, you do pay a slight price in complication using a custom class, but the benefits can be considerable if your data structure is complicated. If it's simple, don't use a custom class.


Keep those letters coming! I may not answer your question immediately, but I'll hopefully get around to it in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise I'll assume it's fair game.)


About the Column
REALbasic University is a weekly instructional column on programming with REALbasic and is brought to you by REALbasic Developer, the magazine for REALbasic programmers.

Each week we answer select reader questions, and we're always open to ideas for future columns. Send your questions to . (Keep your questions simple and specific. General queries like "How do I write my own web browser?" will be neglected.) Your question won't be answered immediately, but will be answered in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise it's fair game.)

About the Author
is an author, philosopher, graphic designer, photographer, film director, soccer fanatic, and programmer (among other things). He writes for MacOpinion, runs his own software company, Stone Table Software, which sells the revolutionary Z-Write word processor, and is Publisher and Editor of REALbasic Developer. He lives in Northern California with his cats, Mischief and Mayhem, and is rapidly running out of free time.

See the REALbasic University Archives


REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.

Email This Article - Comment On This Article

.

Reader Specials

Server Racks Online:
Apple Xserve CompatibleServer Racks and Universal Network Racks
42U KVM Switch Solutions:
High-End Mac and Multi-Platform KVM Matrix switching solutions!
Digital Camera Online:
Great prices on Digital Cameras and accessories!
KVM Switches Online:
Great prices on Mac KVM Switches from the leading manufacturers!
LCD Monitors Online:
Great prices on LCD Monitors from the leading manufacturers!
LCD Projectors Online:
Shop online for LCD Projectors from the leading manufacturers!
USB 2.0 Online:
Great prices on USB 2.0 products from the leading manufacturers

Serious Business Software:
Accounting, Sales, Inventory, CRM, Shipping, Payroll & more!

KVM Switch solutions for MACs:
DAXTEN is a KVM switch, KVM extender and monitor splitter specialist for PC, SUN and MAC applications from name brand manufacturers - offices worldwide.

The "Think Different Store: The iPod Accessories Store - iPod cases, iPod mini, iPod photo, speakers, itrip, inMotion, Soundstage and all other iPod accessories

Earn Cash with the ThinkDifferent Store Affiliates Program

Need A Web Site?
Applelinks Web Hosting Starting at 19.95 a Month

iTunes_RGB_9mm

.

iTunes_RGB_9mm

Cool Mac Gear


iPod 1G-2G
iPod 3G
iPod 4G
iPod Mini
PowerBook-iBook
Keyboard Skins
Garageband