Liberty Basic Beginners Series - Part 8
Well, here we are again (we will see just where that is in a minute). Welcome back. In the last installment I called this a mini-series on Beginning Programming, but I am beginning to think this is more of a saga instead. This is the eighth installment in the series, an amazing fact when I consider it. I had no idea it would go so far and yet be so far from the end. So we press ever onward.
If you are still with me, then you are doing well. I know some of the material in the last couple of installments has been challenging. Building windows and handling events can be a bit daunting initially, but I am sure as you practice and work with it the concepts will become easier to understand. I must stress that practice and experimentation is the secret ingredient in learning and excelling at a language.
Like previous articles in this series, we will be maintaining the Glossary. Items that are explained in better detail in the Glossary are highlighted in the text and linked back to the Glossary page.
You Are Here..
When I started this series we assumed no prior programming experience. Beginning with simple concepts like output (PRINT) and input (INPUT) we have built on our basic knowledge bit by bit until we have arrived here. With the ability to create real Windows programs. In fact in this installment we will complete out first Windows based game - in answer to the challenge in the last installment.
We will be discussing the differences in window types, using STATICTEXT controls, altering captions during run time and many other exciting things. We really have come a long way recently.
So, here we are...
Last time we left off with a challenge to convert the Roll Dice program, which graphically rolls two dice on the screen, into the simple version of the game of Craps. We want to use the rules we presented and used in part 4 of this series. Here is a recap of those rules:
Officially we will again be using a thin version of the casino game Craps, and again I am not condoning gambling. This simple version of the game does not include any betting, merely the random qualities and basic rules for the play of Craps that result in either winning or losing.
Here are the basic rules we will be using: Two dice are used. Initial roll of the dice is to establish the "point" which you will be trying to roll again. Rolling a 7 or 11 with your first roll is an automatic win. Rolling a 2, 3 or 12 with your initial roll is an automatic loss. An initial roll of 4, 5, 6, 8, 9 or 10 establishes the "point".
Once a point is established the player must roll the dice again (repeatedly until they win or lose). Rolling the "point" again is a win. Rolling a 7 is a loss. Have fun!
The challenge is fairly straight forward. It identifies for us a high level goal and specifies the basic rules of the game, which will drive the game logic. It does not discuss the presentation of the game other than to imply that we will base it on our previous work. Presentation will be up to the programmer.
Setting the presentation aside for a moment, developing the logic of the game will not be hard. As the rules imply, game play happens in two phases. The first phase is rolling the dice to make a point. The second phase is attempting to re-roll the point. In the program we can track what tasks the program must perform by tracking which phase of play the player is in. This will be done by creating a variable called "PHASE" which will hold either a '1' or a '2', depending on the correlating phase.
The rules that are applied to the dice roll are different in each phase of play. We can use an IF-THEN-END IF block to separate the two different state's logic. Something like:
If Phase = 1 then ...do phase 1 processing Else ...do phase 2 processing End If
This will be the high level structure of the game play logic - we should wait to develop the detailed logic for each phase of play until we better understand the User Interface development.
G is for GUI
The User Interface (UI) or in the case of Windows the Graphic User Interface (GUI) is the presentation portion of the program. It is where the user and the program come together. It provides the mechanisms that allow the user to interact with the program you are writing.
In the case of Windows, a basic GUI will include a form where various components are laid out, such as buttons, text fields, labels, scroll bars and many other items that are common to Windows experience. It is your job as a programmer to design and layout your program's UI in a manner that benefits the user interaction and allows you program to operate smoothly. There are many texts dedicated strictly to the art of good User Interface design. One particularly good article is available free online, and is a highly recommended read. It is called User Interface Design For Programmers by Joel Spolsky on-line at [http://www.joelonsoftware.com/uibook/fog0000000249.html].
In the case of the challenge, we have a basic working GUI, in the form of the Roll Dice program, to begin with. What we need to do is add the additional components required to take this dice rolling demo to a complete game. The challenge, as presented, left most of the detailed requirements of the User Interface to the imagination of the programmer. Therefore we will have to develop these before we can create the GUI.
Let's list the things we want to have included in the User Interface:
1. Two Dice - graphically presented
2. Roll Dice Button
3. Quit Button
4. Help Button
5. New Game Button
6. Score in the form of wins and losses displayed
7. Information about what is expected of the user, and results of the roll
8. Information about the Point we are trying to make
9. An About Button
Not a very tall order. What will something like this look like? I like to draw my initial impressions of an UI on paper, and then adapt these to the computer. In the case of this game I quickly sketched the following out:
The changes I propose include making the window slightly higher and about a third wider, relocating the quit button, making the Roll Dice button wider, adding three new buttons and several labels. My intention is to leverage the work already invested in the Roll Dice program.
Flavors of Windows
Upon investigating the Roll Dice program a little bit I notice a major flaw in my plans. In an effort to simplify the programming experience in earlier installments, I decided to use a "Graphics Window" type of window instead of the more common "Window" type. Initially this was not really a problem, but as we begin to develop the program further, it will come back to haunt us.
Lets examine this "Graphics Window" verses standard "Window" relationship. The graphics window is a unique implementation of the basic window. It allows some special processing of events (which we will discuss in future installments) and supports drawing. Unfortunately it also limits the use of some standard windows controls, causing some to not function at all, while others are marginally supported. We saw from our last exercise using the Roll Dice program that buttons function fairly well in Graphics Windows, although it is generally discouraged practice to use them in such a window.
In this exercise we will begin working with a label control called the STATICTEXT control. It simply puts a text label on the window. It can not be manipulated by the user. This control is not supported at all within a Graphics Window. The Liberty Basic help file clearly states:
It should be noted that graphics windows and graphicboxes are intended for drawing graphics. It is not advisable to place controls within them, since some controls do not work properly when placed in graphicboxes or graphics windows. If there is a need for text display within a graphicbox or graphics window, use the graphics text capabilities rather than a statictext control.
I debated on my GUI design, whether to stay the course with the Graphics Window type, against the advice of the help file, or whether to change my window type to a kind that supports all GUI elements. The flip side of this argument is that I need to be able to draw the faces of the two dice, and graphics are not permitted in a "Window" type window.
What appears as an impasse really is a simple matter of understanding what controls are available to us as we design our window. Liberty Basic has a special control called the GRAPHICBOX which supports all the drawing commands that a "Graphics Window" would have supported. The Graphicbox can be sized as needed (from a few pixels to the full window size) and placed where required. The helpfile explains the Graphicbox as such:
A GRAPHICBOX is a box that displays graphics, such as bitmap images, or drawn objects like circles and lines, or even text. Use a graphicbox to give the user a graphic display, such as showing a bitmap image, drawing a graph or chart, or use it simply to add visual interest to a program. .end .end Graphicboxes are windows controls like the button control, and as such they must be created for the window before the window is opened. This is done with the GRAPHICBOX command. The syntax is as follows: .i .indentgraphicbox #handle.ext, xOrg, yOrg, width, height #handle.ext - The #handle part must be the same as for the window you are adding the graphicbox to. The .ext part must be unique for the graphicbox. xOrg & yOrg - This is the position of the textbox in x and y from the upper-left corner of the window. width & height - This is the width and height of the textbox in pixels.
My plan is to change the window type from "Graphics" to "Window" and add two Graphicboxes at the original location where the two dice were being drawn. Using the code base from RollDice, the altered program would look like this:
NOMAINWIN WindowWidth = 260 WindowHeight = 230 UpperLeftX = 40 UpperLeftY = 40 button #main, "Quit",[quit],UL, 130, 150, 100, 25 button #main, "Roll Dice",[roll],UL, 15, 150, 100, 25 graphicbox #main.dice1, 20, 20, 80, 80 graphicbox #main.dice2, 140, 20, 80, 80 Open "Roll Dice" for window_nf as #main
There will be quite a few changes we will need to make to get the Roll Dice program working again after this change. If you made the changes above and then clicked RUN, you would have gotten an error. In the old Roll Dice program we are sending graphics commands to the #main handle. This refers to a standard type window that no longer accepts graphics commands. Now we must send our graphics commands to one of two graphics boxes we created.
Before we get into that though, let's examine the GRAPHICBOX command we are issuing. Something new for this command is concept of handle extensions. These are the ".ext" part of the handle in the command syntax we looked at above. These can be assigned to quite nearly any control, but we have not messed with them, since they have been optional. They are, however, required for the GRAPHICBOX command. Without the extension, the control would be useless. The extension allows us to address the specific control and send commands directly to it, just like we have done with the window handle of the Graphics Window in the past.
In the code above we have specified the first graphicbox as #main.dice1 - this is the window handle that will contain the control (i.e. #main) and the specific extension that defines this control (i.e. dice1). The first dice is placed in the exact location where the old dice outline use to be (i.e. 20, 20). That is the x and y coordinates, by pixel from the upper left corner of the window. We have specified the graphicbox as 80 pixels in the x direction and 80 pixels in the y direction.
Notice that the second GRAPHICBOX command is almost the same. We have specified an extension of "dice2" for this control, and placed it at 140 pixels in the x direction and 20 pixels in the y direction. This one is also 80 x 80 pixels square.
These two commands create the graphicboxes. There are two of them with two unique names. This has monumental effects to our code. We can no longer build our dice display (showing the dots on the dice) using a single subroutine and changing the offset for one dice or another as we did in the original code. This is because we must specify WHICH graphicbox to draw the circles in by using the extension name of one or the other. In the old code we simply drew to the graphics window. This will not work now.
What is more, we always drew our circles relative to the upper left corner of the window. Now we must change the calculation a little. We must draw our circles relative to the upper left corner of each graphicbox. This means that the offset variable we used in the original Roll Dice program (you should go back and look at it in the last installment) is not required, and we will no longer need to calculate the 'x' and 'y' locations to locate our filledcircle we are drawing. We will know the absolute locations, because we can only draw one dice face or the other with a given routine. This means we can hard code the locations in each of the separate drawing subroutine for each of the two dice.
Does that make any sense at all? If not, don't fear. We will cover it again soon. Let us just look at what we need to do to get the current program to run using the new window type and two graphics windows.
The original program contained this code after the window was opened:
#main "trapclose [quit]" #main "down; fill white; up; flush" #main "font ms_sans_serif 10" 'show dice outlines #main "goto 20 20; down; box 100 100; up" #main "goto 140 20; down; box 220 100; up" #main "flush" Wait
Obviously some of this code will fail now. Specifically the code that sends graphics commands to the window #main. It needs to reference one or both of the graphicboxes now. What we really want to do is fill each of the two dice figures with white. It is no longer necessary to draw the dice outlines, since the graphicboxes come with their own outlines, and they will work quite well.
So we change the first bit of code as follows, forcing each of the two graphicboxes to white:
#main "trapclose [quit]" 'format graphics boxes #main.dice1 "down; fill white; up; flush" #main.dice2 "down; fill white; up; flush"
We also remove the entire section for showing dice outlines. Notice in the code above that we added two lines. They each perform the same action, however on different graphicboxes. I am not planning on going through all the graphics commands we covered last time in this installment. Please review the seventh installment for a refresher on any commands that might confuse you - if required.
When we run the new code we get the following window:
The Roll Dice button still does not work, and if you press it you will notice that the program crashes with an error. This is because all the graphics commands are still referencing the main window.
Learning to Draw Again...
The next challenge is to rebuild the drawing routine so that it is able to work against the two smaller graphicsboxes, instead of the single graphics window. Originally the both dice had their faces drawn by a single routine. This routine was called [showDice] and it was able to draw the required black dots to produce the correct dice face on either dice with the same code. This was possible because both dice existed in the same graphics control - the graphics window. The use of the offset value made this work. An offset of zero lined the drawing up over the left dice. An offset of 120 lined the drawing up over the right hand dice. Here is that routine:
[showDice] #main "backcolor black" if dice = 1 or dice = 3 or dice = 5 then x = 60 + offset y = 60 #main "up; goto ";x;" ";y #main "down; circlefilled 5" end if if dice > 1 then x = 35 + offset y = 35 #main "up; goto ";x;" ";y #main "down; circlefilled 5" x = 85 + offset y = 85 #main "up; goto ";x;" ";y #main "down; circlefilled 5" end if if dice > 3 then x = 85 + offset y = 35 #main "up; goto ";x;" ";y #main "down; circlefilled 5" x = 35 + offset y = 85 #main "up; goto ";x;" ";y #main "down; circlefilled 5" end if if dice = 6 then x = 35 + offset y = 60 #main "up; goto ";x;" ";y #main "down; circlefilled 5" x = 85 + offset y = 60 #main "up; goto ";x;" ";y #main "down; circlefilled 5" end if return
Notice in the code above that the X coordinate was calculated by adding the value of "offset" to the value of "x", however the Y coordinate was never calculated. This is important to realize as we consider simplifying the code to support drawing a specific dice face. We will do this by implementing two separate drawing routines, one for each graphics box. I have decided to name these [showDice1] and [showDice2]. These routines will be very similar to the original with one difference - we will locate the pen using in the drawing commands using static coordinate values, rather than calculate coordinate values. This is since we no longer need to compensate for drawing two dice with the same routine, using calculated X coordinates to place each filled circle. They are always the same for each dice. Here is the code for [showDice1] after my first pass as modification:
[showDice1] #main.dice1 "backcolor black" 'Draw dice if dice = 1 or dice = 3 or dice = 5 then #main.dice1 "up; goto 40 40;down; circlefilled 5" end if if dice > 1 then #main.dice1 "up; goto 15 15; down; circlefilled 5" #main.dice1 "up; goto 65 65; down; circlefilled 5" end if if dice > 3 then #main.dice1 "up; goto 65 15; down; circlefilled 5" #main.dice1 "up; goto 15 65; down; circlefilled 5" end if if dice = 6 then #main.dice1 "up; goto 15 40; down; circlefilled 5" #main.dice1 "up; goto 65 40; down; circlefilled 5" end if return
Notice the code in [showDice1] is very similar to the code in [showDice]. Take for instance the portion the places the center dot for the values 1, 3 or 5. In the new routine we simply move the pen to position 40, 40 (relative to the upper left corner of the graphicsbox, not the window), then draw the filled circle. In the original code we calculate the location of X, but the location of Y was fixed at 60 (that is 20 pixels from the top of the window to the top of the dice, and 40 more pixels to the center of the dice). The X coordinate is also 60 (same reason, 20 pixels from the left edge of the window to dice edge, the 40 to the center), only it might also be modified by adding the value of the offset if we are drawing the right most dice face. Either way, the center dot is 40 pixels from the top of the dice and 40 pixels from the edge of the dice. The result is the same either way, only our new version uses much less code for a single dice face. Unfortunately we need to have two nearly identical routines to draw both dice.
As a point of refresher, remember that we are PRINTing the graphics commands to the controls that accept them (specifically a graphicsbox). In our code above the PRINT statement is optional and I have omitted it. The statement basically says:
Send this command string "up; goto 15 40; down; circlefilled 5" to the control called "dice1" which is part of the window "#main".
The addressing of the graphics box is accomplished through the extension applied to the control at the time the window was created. The extension is the "dice1" part of the controls name. It is critical to addressing this control specifically. However we must also address the control as a part of the window. The window's name is "#main", thus the addressing as "#main.dice1".
If we create a second routine called [showDice2] and copy all the code from [showDice1] into it, we will almost be done with that second routine too. All the coordinates are the same, as they are all in reference to the graphicsbox upper left corner, and the second dice is another graphicsbox. What we must do to make this new routine work is change the name of the control referenced from "#main.dice1" to "#main.dice2". We need to do this all the way through the entire routine. The resulting routine would look something like:
[showDice2] #main.dice2 "backcolor black" 'Draw dice if dice = 1 or dice = 3 or dice = 5 then #main.dice2 "up; goto 40 40;down; circlefilled 5" end if if dice > 1 then #main.dice2 "up; goto 15 15; down; circlefilled 5" #main.dice2 "up; goto 65 65; down; circlefilled 5" end if if dice > 3 then #main.dice2 "up; goto 65 15; down; circlefilled 5" #main.dice2 "up; goto 15 65; down; circlefilled 5" end if if dice = 6 then #main.dice2 "up; goto 15 40; down; circlefilled 5" #main.dice2 "up; goto 65 40; down; circlefilled 5" end if return
So, the retrofit of the code for the new window type is coming along very nicely. As I develop programs, I like to test them out from time to time, as I know they are reaching a stage where they might actually run. Such is the case now with this program. Clicking the run icon shows a very encouraging window with my two dice outlines. If I click "Roll Dice" however, I get an error:
"Bad command for #main, DISCARD"
This means that I am trying to use the graphics command "DISCARD" on the main window, which use to work, but does not work any longer, since it is not a graphics window anymore. Looking through the code, I find the offending command in the [roll] routine. It has several graphics commands that are referencing the original graphics window. We are going to have to adapt these to the new configuration. Here is the code in question:
'clear any used memory #main "discard" 'clear last dice #main "backcolor white" #main "up; goto 20 20; down; boxfilled 100 100; up" #main "goto 140 20; down; boxfilled 220 100; up"
The first is the offending DISCARD command. It was used against the #main window back when it was a graphics window to clean up the used memory that drawing the various filled circles consumed. I would have preferred to use the CLS command back in the original code since it both clears the content of the graphics window/box and releases the used memory, but that would have caused the dice outline to appear to flash when I redrew it a moment later.
Since the dice outline is part of the graphicsbox control I am not drawing dice outlines of any kind. That means I can use the CLS command and it will work great. I need to issue this command against each of the two graphicsboxes, and the best place to do this is in the [showDice1] and [showDice2] routines. So I have modified the top of each routine by adding the code as follows - for the first dice:
[showDice1] 'clear graphicsbox contents release any used memory #main.dice1 "cls"
And for the second dice:
[showDice2] 'clear graphicsbox contents release any used memory #main.dice2 "cls"
This single command resets the graphicsbox back to a blank square (still filled white), so the additional lines of code from the original [roll] routine are not required - as their job was to simply redraw the dice outlines and fill them with a blank white space. I have simply removed all the offending code (shown above) out of the [roll] routine.
The program will now run, but I wanted to make one more changes. The code that sets the graphicsbox background to black is being executed every time I draw a dice face, but it is never set to another color (at least not in this version - it was in the earlier version of Roll Dice), so we can safely move it to just above the WAIT statement earlier in the program. The following two lines of code (one form the [showDice1] routine and one from the [showDice2] routine) are combined and placed as described just before the WAIT statement:
#main.dice1 "backcolor black" #main.dice2 "backcolor black"
We need to make one more small change to each of the two [showDice1] and [showDice2] routines. These changes will prevent the dice faces from being wiped clear should another window cover our Roll Dice program when running. They are simply FLUSH command which will cause the graphics to stick. Combining FLUSH in a loop can be dangerous because it can quickly consume your system memory and produce a GPF. This is only possible because the CLS command we discussed above will clear ALL memory used by graphics and restore it to the system for future use. Add the following line just before the RETURN statement of the [showDice1] routine:
and, this line just before the RETURN statement of the [showDice2] routine:
The resulting code now is well optimized and runs with out issues. You can see this version of the program in Appendix A. Go ahead, load it up and run it. Take a look at the changes required to move from a graphics window to a standard window using two graphicsboxes. There really is not any more code than before, and in reality, it is clearer and easier to understand.