| |||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||
Print This Article REALbasic University: Column 038
RBU Pyramid XILately we've been concentrating on adding some "whiz-bang" features to RBU Pyramid, such as last week's ability to change the game window's background graphic. For the next few lessons, we're going to concentrate on finishing up the core of the game, to make it more playable. Some of this will be ugly but important stuff, like adding an undo system, and some will be more graphical, such as the help system and a high score mechanism. Before we begin, however, some of you may have noticed that the original RBU Pyramid beta application posted in column I expired at the end of November. My original intention was to have the final version released before that date, but several things conspired against that, including a bout of the flu I'm currently enduring (if part of this column sounds irrational, it's probably fever talking ;-). But I'll have a new final version posted as soon as it's finished (in the next week or so).
Adding Available Moves DetectionThis week we're going to add an incredibly useful feature to RBU Pyramid: move detection. Move detection just means our program can tell what valid moves are available. That sounds simple, but just getting a computer program to understand the rules of a game is tough. From a programming perspective, move detection must analyze every possible card match and see if the two cards are a valid match (the card values add up to 13). From the player's perspective, move detection works like this: holding down the Shift key at any time during the game will highlight any available card matches. There's another key benefit to the move detection feature: it will tell us when there are no available moves, meaning the game is over. That's helpful: is there anything more annoying than playing a game where you've reached a dead end and yet the computer doesn't tell you that? So the first thing we're going to do today is to add a global "game over" property. Open up globalsModule and add this new property (Edit menu, "New Property"): gGameOver as boolean. Now find your updateCards method inside gameWindow. Scroll through it until you see the comment reading "// See if pyramid has been won". In the blank space above that line, we're going to insert a few new lines of code. (Make sure you add this before the comment but after the "next // row" which should be the previous line.) Here's the new block of code to insert:
What this does is check to see if the deck is empty (no more cards to draw) and that we've already gone through the deck once. If so, we scan for available moves. If the game's over (no available moves), we call an end of game routine. The effect of this is that the game will stop when there are no more moves left. Since we're referring to the endOfGame method, let's add it. Go to the Edit menu and choose the "New Method" command. Name it endOfGame. For today, we're just going to leave it blank. (But we need to add the method, otherwise REALbasic wouldn't let your program compile since it wouldn't know what endOfGame meant.) Do that same thing again, and this time name the method findAvailableMoves. While we're at it, go ahead and add a showAvailableMoves method as well. Since the showAvailableMoves is simpler, we'll add its code first:
This method will be called when the user holds down the Shift key: it will redraw the pyramid with playable cards highlighted. Since the findAvailableMoves method sets the highlight property of each card, all we have to do is check that property and force the card to redraw if it's true. If showingMoves is false (meaning the Shift key is not down), we turn off the highlight setting of each card and force the card to redraw non-highlighted. That's pretty straightforward. But showingMoves is a new property: we must add it to gameWindow. Go to the Edit menu and choose "New Property" and type in showingMoves as boolean.
Finding Valid MovesNow we come to the complicated part: the findAvailableMoves method is where all the key work is done to calculate if a move is valid or not. Here's the code -- I'll explain it in a minute:
Yes, that's a lot of code for something that seems like it should be simple! But it's not as bad as it seems once you understand what's going on. Let's step through it and see. The first loop simply sets the highlight property to false for every card. That's simple enough. Then we start a for-next loop through the first 28 cards (the pyramid, not counting the discard piles). We check for the simplest thing first: a king (removable by itself). If the card is a topCard (not covered by another card) and it's a king (number = 13) then we set its highlight to true (meaning it's a valid play). Next, we want to see if a playable card in the pyramid can combine with a discard card to be a match. So first we only examine playable pyramid cards by restricting our examination to topCards that are visible (therefore playable). We add the value of card i of the pyramid with the card on top of the temporary discard pile. If the two added together equal 13, we've got a match! If they don't, we do a quick check to see if the temp discard card is a king, and if it is, we set its highlight to true. Note that there may not be a card on temporary discard pile: that's why we first check to see if that card is clickable, meaning that it's a playable card. We do the same thing again with the main discard pile: simple. But we still haven't checked for all possibilities yet: we haven't compared cards in the pyramid to other cards in the pyramid. That gets a little more complicated. To do that, we start a new for-next loop with a variable j, and loop through every card in the pyramid. For each card, we first make sure that i and j aren't the same (otherwise we'd be comparing the same card to itself), that it's a topCard, and that it is visible. That ensures that we're only dealing with playable cards. Since we already know that card i is playable, now that we know j is playable, we just have to see if the two are a match (they add up to 13). If so, we set the highlight of both cards to true. We're getting closer, but we're still not finished. Remember, if two cards overlap each other and add up to 13, they're removable, so we must check for overlapping cards. This is more difficult since the underneath card is not a topCard and yet it must be directly underneath the overlapping card for the match to be valid. Fortunately, we've already done most of the ugly math for this calculation in our updateCards routine. Remember how it calculates if a card underneath another is a match and sets its clickable property to true? (See RBU Column 031 for more on that.) So we can cheat a little and use that previous calculation. All we do is find a playable card that is not a topCard -- if it's clickable, then it must be part of a match! Then we just find which of the two cards on top of it is also clickable, and we've got the two cards we need to highlight. The formulas n = i + cardCanvas(i).row and n = i + cardCanvas(i).row + 1 set n to the number of the card on top of card i, so we can simply see if cardCanvas(n).clickable is true or not to tell if we've got a valid match. Simple! But we're still not quite finished. There's one more possibility of a match: the two discard piles could match each other! So we add a few more lines of code to see if the two added together total 13, and we set their highlight property to true if so. Finally, we could be at the end of the game: what if there are no more cards to draw and no moves available? We check for this by making sure there are no cards left to draw and that we've already gone through the deck once. If that's the case, we simply look for the any highlighted cards: if there is even one, there's a valid move so the game's not over. If there are none, however, gGameOver = true and the game will end. Whew! That was a lot of technical stuff, so I hope you followed it well (and I hope I explained it clearly). This does get complicated, but one of the things I've noticed is that REALbasic's object-oriented design greatly reduces the complicity. For instance, with the above, we're referring to cards as though they are real objects, completely ignoring the fact that they are nothing more than collections of data in a computer's memory! That's what happens as you work with data objects more and more: instead of rationally thinking "Ah, x a property of object y," you begin to think of your object's properties as settings and characteristics of your objects.
Checking for the Shift KeyYour program should run now, but the key new feature -- move detection -- won't actually work until we do one more thing. We must add a way to detect when the user presses down the Shift key. That gets a little tricky, because pressing the Shift key, while it is a key on the keyboard, does not activate gameWindow's keyDown event. We can manually check to see if the Shift key is down using the keyboard.asyncShiftKey function, but that only is checked when we call it. We'd have to call it constantly for it to do what we want. So how can we do that? The only alternative is to set up a timer object: each time the timer activates, it checks to see the state of the Shift key. If it's down, it highlights the cards. If it's up, it draws the cards normally. We can set the timer to be called as frequently as we need: ten times a second is good. (It's fast enough the user won't notice a delay and yet not so frequent as to slow down the game.) Drag a timer object to gameWindow and give it the following settings: ![]() Good. Now in moveTimer's Action event, put the following:
Since moveTimer's code is executed ten times every second (remember, we set its period property to 100 milliseconds), it will be executed multiple times while the Shift key is being held down. That means we don't want just a simple toggle: that would mean the slow and complicated showAvailableMoves and findAvailableMoves routines would get executed ten times a second! So what we do is create a new variable, original, and store in it the state of showingMoves before we check the keyboard. Then if keyboard state changes showingMoves, the two won't match, meaning we've had a change in state. If that's the case, we redraw the cards by calling showAvailableMoves. That way showAvailableMoves is only called once per Shift key down (instead of dozens or hundreds of times). Slick, eh? Well, that should do it for this week. The game should run and show you available moves when you hold down the shift key. If you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.
Organizing Your Project WindowOh, one more thing. I've noticed that our project window is getting rather crowded. Let's use a cool feature of REALbasic to organize our project's elements into folder. Go to the File menu and choose "New Folder" three times. Rename the folders like in the picture below, and put the appropriate elements into each folder: ![]() Isn't that much better?
Next WeekWe'll continue by adding in a system to remember players' high scores.
LettersThis week we hear from another Marc, Marc Gyarmaty, who writes with a question regarding arrays:
I can see how this would confuse you, Marc, if you're familiar with other programming languages such as Pascal or C which don't have a string datatype. In those languages, a "string" of letters is always stored in an array, meaning that the first character in the string is at array index 1, the second at array index 2, etc. While that can be handy for accessing the individual letters of a string, it's a pain because the size of your string is usually fixed (i.e. you can set the maximum length of the array/number of letters in the string, but you can't change that while the programming is running). The solution to your problem is simple, however. Since a(0) in your example is a string, you just use the mid() function to grab a single letter inside that string. The mid() function is flexible and powerful: you can use it to retrieve a single letter or a series. For instance, the following are valid uses of the mid() function (stick this code in a pushButton if you want to try it).
The mid() function takes three parameters: the first is the name of the string you are examining, the second is the position you want to examine, and the third is the number of letters to extract. The third parameter is optional: if you don't include it, it will return all the letters in the string past (and including) the position point. (So in the above, the final line returns the phrase " brown fox ran fast.") Just remember, REALbasic doesn't use arrays for strings, and thus strings can be any length, up to the available memory (to a maximum of two gigabytes, I believe).
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 See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
|
. |
| |||||||||||||||||||||||||