| |||||||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||||||
Print This Article REALbasic University: Column 042
RBU Pyramid XV: Undo Part 1Admittedly, last week's lesson wasn't too challenging. But this week we've got a mountain to climb. We're going to add an undo system to RBU Pyramid. To be honest, we're going about this the wrong way. An undo system is so fundamental to the operation of a program that it should be one of the first things you design. When you're planning your program, especially the data structure, think about how you'll implement undo. This is even more critical when you want to support multiple undos, undos of various kinds of actions, and have a redo system as well. It's certainly not impossible to add an undo system after the fact, but it's not easy. You'll find tracking down bugs to be a horrible pain. In fact, I've known there are bugs in RBU Pyramid's undo system for months -- and I've been putting off fixing them! But for our lesson today we're going to be creating the buggy version -- extra points to whomever catches the bugs. We'll fix the problems in a future column, but meanwhile you'll get chance to see how difficult this is to add after the fact, as well as how complicated it is to debug.
Undo BasicsAs always, we start by planning what our undo system is going to do. Is it going to be a single undo/redo or multiple undo/redo? The nature of the Pyramid card game is such that a player follows various paths by making irreversible choices: Do you match the 8 and 5 in the pyramid or the 8 in the pyramid and the 5 on the permanent discard pile? Because of those multiple paths, it can sometimes be valuable to back up several moves and try a different trail. Also, since I was writing RBU Pyramid specifically with the idea of using it in a tutorial, I felt a more complex, full featured undo system would be most valuable for RBU readers. Thus I made the decision that I wanted to support multiple undos. In fact, unlimited undos. The player should be able to undo all the way back to the beginning of the game! However, since a pyramid can be cleared (won) and redealt as many times as the player keeps winning, I also decided that the undo system would be reset with each card shuffle. There's no point in a player going back to a game they won. That does make our job a little easier, but it's still going to be rough. Let's look at what happens in a typical Pyramid card game. The user clicks on two visible, uncovered cards, attempting to find a match. If the values of the cards add up to 13, the cards are removed. Kings are removed with a single click. There are two discard piles: a temporary and a permanent. When the user clicks on the Deck, a fresh card is placed on the temp discard. If there's already a card there, it is moved to the permanent discard. Cards already in place on the permanent discard pile just stack up on top of each other (face up). Worst of all, when the Deck runs out of cards the first time, the permanent discard cards are flipped over and become the new Deck! So already we can see several challenges. To support undo, we must be able to keep track of what the user has done. If a pair of cards have been removed, they must be put back. We must also watch what happens with the Deck and two discard piles, reversing any changes therein. And -- and this is the killer one -- we must be able to put the Deck back even after the user redistributed the permanent discard pile! In principle, an undo system is simple: we just keep track of what the user has done, step by step, and reverse it step by step, if the user requests it. Simple, right? In practice, however, it can be much tricker. For example, let's say the user deletes something. If your program actually deletes the data, it's gone. There is no undo. You can't regenerate that data from nothing. So the only way undo works is because the program doesn't actually delete it: it just pretends to do so while keeping a backup copy around in case the user changes their mind. You can probably see how unlimited undo eats up memory. Just a few years ago, this meant that few programs offered more than one undo or maybe a handful, such as 10. Today, most computers have hundreds of megabytes of RAM and huge hard drives, so with simpler programs, multiple undos are feasible (though still complicated). Even Adobe Photoshop supports multiple undos via its History palette (though telling Photoshop to remember more than a few undos can dramatically slow down the program, especially if you're editing 100 megabyte photos). In the case of RBU Pyramid, program resources are not a significant problem: with a meg of memory we could probably remember thousands of user actions. But it's something to remember if your program creates a lot of data.
Creating an Undo Data StructureThe first thing we start with when designing our undo system, of course, is with a data structure. We've got to have some way to remember everything the user does. In our case, any action by the user could result in multiple things happening: the Deck could advance or reset, there could be movement between the two discards, and cards could be removed. That's a lot of action to remember! So the first thing I did was to create a custom undo class, containing properties telling me the state of things when the user does something. Within your project, create a new class (File menu, "New Class"). Name it undoClass and add the following properties to it (Edit menu, "New Property"): ![]() As you can see, we've got a number of flags (flags are usually booleans that you use to tell you if a state exists or a condition has changed) and several properties which will tell you which cards were on the discard piles or selected by the user. That gives us a structure for our undo data, but we still have no place to put it. Let's create a global array for the undo data. Open globalsModule and add this property: gUndoList(0) as undoClass. Now since we want the undo data to be erased whenever a new shuffle of cards is dealt, let's make sure it gets deleted whenever there's a new shuffle. Go to the freshDeck method (within globalsModule) and scroll to the bottom. Somewhere after the // Erase the discard line, put in this:
That should finish our work with globalsModule for now. Close it and let's get into gameWindow's code editor (Option-tab while the item is selected).
Adding Undo RoutinesThe first thing we'll do is add a few methods (Edit menu, "New Method"). Name the first one doUndo, the second eraseUndo, and the third saveUndo. For all of them, leave the "parameters" line blank. Now let's add property to gameWindow. Go to the Edit menu, choose "New Property" and type in theUndo as undoClass and click okay. Now let's write the saveUndo method. This is what you'll call whenever you want the current state of the program saved (i.e. right after the user has done something undoable). Here's the code for saveUndo:
The first thing we do is enlarge our global undo array by one element: we need to make room for the new data. We do this by finding the current size of the array with the uBound function, add one that value, and redim the array with the new size. Next, we initialize that new array element with a new instance of the undoClass object. (Remember, classes are objects, and thus must be instantiated before they can be used.) Once we've done that, the rest of the code is simple: we just assign the values in theUndo to the matching values in the array element.
The eraseUndo method is very simple: it just resets everything in the undo object to their default values. We need that, because as you'll see, most routines that call saveUndo only modify the field or fields of theUndo that need to be changed. If we didn't erase theUndo first, those fields would contain old, incorrect data that would be saved in the undo array. Here's eraseUndo:
As I said, very simple. However, doUndo is not:
Woah, what the heck does all that do? Well, I'm not going to tell. I'll just let you figure it out for yourself. Just kidding! I wouldn't be that cruel. Figuring out someone else's code is bad enough, and wading through undo code is the worst. The first thing to do is to take this gently, step by step. Then it's not so overwhelming. Note that the code is heavily commented: I didn't do that so much for the tutorial as I did for my own sanity while writing this. Trust me: when you get into hairy stuff like this, it's best to go slow and comment like mad! Remember the purpose of this routine: this method is called when the user chooses the undo command from the Edit menu. So our code here must figure out exactly what actions we need to undo and do that. The first thing we do is figure out the size of the undo array. That's because the last element is the one containing the undo info we need. So, using that array element, we look to see if there was a Deck advance or Deck reset. The Deck reset is the most complicated. If that's the state, we have to do a bunch of stuff:
If it's not a Deck reset, then it's a Deck advance (the user drew a card from the Deck). This is simpler, but still involved:
After either of these actions, we must refresh the Deck (gDeck) so it is redrawn with the appropriate appearance. Finally, if neither of the above conditions were true (Deck reset or Deck advance), the user must have made a match. The only thing tricky about this is figuring out what kind of a match: two pyramid cards, two discard cards, or a combination of pyramid and one of the discard piles? That sounds intimidating, but it's not that bad. If there's a value greater than zero within gUndoList(n).pyramidCard1 or gUndoList(n).pyramidCard2, we know those are pyramid cards. All we do is restore those cards to their visible, clickable state. (Remember, cards involved in matches in the pyramid are never actually deleted, just hidden!) If there's a value within either of the discards, it's slightly more complicated as we have to restore the original value and put a card back onto the stack of the main discard pile (from a programming perspective, we add a card element to the gTheDiscard array). Note that for any of these occurrences -- a card match -- we decrement the player's score. We wouldn't want them racking up a super high score by cheating! Finally, after all this stuff has been done, we remove the current undo object from the undo array: it's been used so we don't need it any more. Then we call enableMenuItems to make sure our menus are up-to-date and updateCards to make sure all the cards are in the correct state (selectable, etc.). That's it! We're done! Just kidding, unfortunately. We've only scratched the surface. All we've done is write the code to support the undo data structure -- we have never actually called any of these routines anywhere in our program yet, so running RBU Pyramid right now would do nothing differently. However, this is all we've got time for this week, so we'll finish the rest of the undo system next week. Meanwhile, if you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.
Next WeekIn Undo Part 2, we'll have less theory and get that darn undo system working!
LettersThis week a couple readers respond with suggestions for last week's letter regarding large numbers in REALbasic. George Bate writes:
That's great, George! Thanks for the link... though RPN (Reverse Polish Notation for those of us without math degrees) is something I've never mastered. I'm glad Bob includes a tutorial. Next, John Johnson (you can tell his parents had a sense of humor ;-) chips in with:
Groan. I knew I shouldn't have opened to door to a math question! I failed algebra twice. (Okay, technically I got a D the first time and dropped the class halfway through the retake, but it's pretty much the same thing. Math and Marc just don't mix well.) I guess if I knew what a logarithm was I'd be impressed, but I do note that your 2568 digits just happens to match the power amount George used in his letter... I guess that's not a coincidence, but I'm too math ignorant to understand the significance. Oh well. I'm sure this info will be helpful for someone out there. 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.
| |||||||||||||||||||||||||||||||