So we have already worked through the display routine. We will probably enhance it a bit latter. For now we need to do our game initialization. Part of that initialization was the dimensioning of the starmap$ array, as we will see. We will also need to track the ships location, amount of energy, remaining lives and for convenience, the move number. In the code bellow you will see that we have a variable initialized for each of these.
'StarQuest - by Brad Moore 'public domain, 2003 'for Liberty Basic 3.x 'initialize some variables dim starmap$(8,12) [beginAgain] shipx = 1 shipy = 1 energy = 20 lives = 3 move = 1
Notice also that I have placed a label just after the dimensioning of the starmap$ array, and before the other variables are initialized. This will be used to begin the game again if the user wants to play a new game at the end of their current game.
Next is a section you will recognize. It is where the starmap is populated. We constructed this code in the previous pages.
'form the starmap for x = 1 to 8 : for y = 1 to 12 : starmap$(x,y) = "." : next y : next x starmap$(1,1) = "@" starmap$(7,11) = "*" starmap$(7,12) = "*" starmap$(8,11) = "*" starmap$(8,12) = ""
So now all the basic initializing is done, lets get on with the introduction of the game. I like my programs to have some form of introduction. This is a common feature in software today. Quite frequently you will see these as a splash screen in the windows environment. For us it will be a simple text banner. We will follow this by an invitation to view the instructions.
'Welcome the user print "===============================================" print " S T A R Q U E S T" print "===============================================" PRINT print "Welcome to the game." print 'see if the user wants instructions input "Do you want instructions (y or [N])? ";a$
Let me just stop here and mention another design decision that I made regarding my implementation of this game. I decided that every user prompt will have a default response that the program will assume if the user inputs an invalid response, or if the user simply presses ENTER. I have indicated the default response by putting a bracket around it, as in the query above where the "N" is the default.
This is accomplished by comparing the users input with the explicit response desired. In the case above we are looking for a "Y". Any other response will considered as being the default, or an "N". We do this by testing for the non-default value using an IF statement. We explained the following statement in the last installment:
if instr(upper$(a$),"Y") > 0 then gosub [instructions]
Basically it says that if the users input contains a "Y" in any position in the string (that is what the INSTR function does) then the statement is TRUE and the conditional clause should be executed. In this case a GOSUB to the label [instructions]. I usually put my instructions at the end of the actual code, so we will write these later.
Right now we are ready to start the actual game. We will do this by beginning the main game loop with a label to loop back to:
'now enter the main game loop [loop]
Next we will display the starmap. We developed that code earlier. I have chosen to use a gosub to branch to the code that displays the map, instead of putting that code in line here. Either method works well.
'show the map gosub [showmap]
Lets work on the showmap routine, as it is central to the development of the game. It will be placed toward the end of the physical code. You will notice some changes from our earlier work:
[showmap] print print "-----------------------------------------------------" print " Lives: ";lives;" | Move: ";move;" | Energy: ";energy;" gigajoules" print "-----------------------------------------------------" print print " "; for x = 1 to 12 a$ = right$("00" + str$(x),2) + " " print a$; next x print for x = 1 to 8 print " ";x;" "; for y = 1 to 12 print " ";starmap$(x,y);" "; next y print next x print return
The first thing you will notice is the status bar showing the number of lives, current move and total energy. I have also made some spacing changes to the map, moving it to the right several spaces. The RETURN statement will cause the program to return to where it branched from.
It is important to keep the show map routine at the bottom of our code for now. We will be adding code ABOVE the show map routine. What we add is more code to the main game loop. This code will display the ships location coordinates bellow the starmap we just put on the screen.
Following the display portions we will give the user a chance to quit the game gracefully. Not quitting will be our default. The IF statement will specifically look for a "Q" in the users input, anything else will be considered not a "Q" and game play will continue.
'display ship location print "You are at star position (";shipx;",";shipy;")." 'now find out where they want to move input "Press ENTER to begin move, type 'Q' to quit";a$ 'if they respond anything other than 'q' then get new coordinates... if instr(upper$(a$),"Q") > 0 then
If the user has indicated they desire to quit, ask them again to make sure that this was not an error. We will use an INPUT statement and indicate a default (this time I chose quitting as the default). Again the IF statement does the magic with the help of the INSTR function. This is explained very well in installment four.
input "Do you really wish to quit the game ([y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [loop] else goto [quitting] end if
If the user specifically typed a "N" for "no" then the game will continue by jumping up to the top of the loop and displaying the starmap again. Other wise we go to a label called "[quitting]" which we will add to our list of routines to finish later. This is a complete IF-THEN-END IF statement, but it is nested in another IF-THEN-END IF statement.
So far I have not mentioned this, but the IF-THEN-END IF statement has another clause. The ELSE clause. This allows us to say the following: if this equals that, then do the following, BUT if this does not equal that, then do the following. The "BUT" is the equivalent of the ELSE clause. It allows you to take alternate action if the condition of the IF-THEN is not satisfied. Visiting the Liberty Basic help file we learn:
IF...THEN...END IF is another form using what are called conditional blocks. This allows great control over the flow of program decision making. Here is an example of code using blocks.
if qtySubdirs = 0 then print "None." goto [noSubs] end if
A block is merely one or more lines of code that are executed as a result of a conditional test. There is one block in the example above, and it is executed if qtySubdirs = 0.
We can use the else keyword as well with blocks, like so:
if qtySubdirs = 0 then print "None." else print "Count of subdirectories: "; qtySubdirs end if
You can see how neat and clean this is. There are two blocks in this example. One is executed if qtySubdirs = 0, and one is executed if qtySubdirs is not equal to 0. Only one of the two blocks will be executed (never both as a result of the same test).
And so we encounter an ELSE clause which is the alternate action to take if the user did not elect to quit the game above.
Since we are going to play another round, lets get the users desired coordinates to move to. Again, in keeping with my goal, I have supplied the users current position (both X and Y coordinates) as defaults to the queries. This is suggested in the input message, and actually carried out a few lines later.
input " Enter the 'X' (up and down) coordinate [";shipx;"] : ";x$ input " Enter the 'Y' (left & right) coordinate [";shipy;"] : ";y$
Since the user input strings, lets convert them to numeric variables for later use.
'convert the values entered into numbers x = val(x$) y = val(y$)
Now, if the user entered nothing, or of they entered just letters, then the value of the variables x and y will be zero. In this case we will set them to the current ships location. This is how we achieve the impression of defaulting to the current location.
'if the user entered nothing default to current position if x = 0 then x = shipx if y = 0 then y = shipy
Next, we will make sure the move is valid. There are certain values that are just not part of this starmap. If the move is invalid we will show a message and start over at [loop].
'if the user entered coordinates that are out of bounds, retry... if x < 0 or x > 8 or y < 0 or y > 12 then print print "The coordinates that you entered are out of bounds. You must retry..." input "Press enter to select a new location...";a$ goto [loop] end if
Now we are on a roll. We have valid coordinates from the user. They are in numeric form. Now we need to determine what the user has intended for a move and then verify that with the user. We a specifically interested in whether they are moving or not, moving up and down, or left and right or diagonally. To determine this we need to know the degree of change in the horizontal and vertical directions. We will do this by subtracting the current ships coordinates from those entered by the user. We also want a positive number regardless of whether they are moving backwards or forwards. To force a positive number we use the ABS function which returns the absolute value of the result. Here is the code:
'determine the movement in both the x and y directions movex = ABS(shipx - x) movey = ABS(shipy - y)
Now we can determine the actual direction of movement. As we mentioned, there are four cases for this: standing still, moving horizontally, moving vertically and moving diagonally. We will evaluate each of these cases using an IF-THEN statement and branching to a specific labeled subroutine to do the actual processing. I am presenting the a set of three conditional IF-THEN-END IF statements here. Notice that each of them evaluates the movex and movey variables to determine which direction the user has decided to move in. If the IF condition is satisfied (evaluated as TRUE) then the program echoes back to the user the move it has determined and asks the user to verify the move. You will recognize the method we have used to indicate a default response and evaluate the response, as we have discussed it at length above. We have introduced two more routines that will go on our list of routines to finish later, they are [sitstill] and [moveship]. Also notice that we have introduced a new variable in relation to the statements that deal with ship movement. It is "drain" - which is the calculated energy required to move the ship to the new location.
'there are four cases - no movement, movement x, movement y and both x & y if movex = 0 and movey = 0 then print input "You have decided to remain idle - is that right ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [loop] else goto [sitstill] end if end if 'move in x direction if movex <> 0 and movey = 0 then print input "You are moving ";movex;" spaces vertically - is that right ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [loop] else 'calculate how much energy is required drain = movex * 100 goto [moveship] end if end if 'move in y direction if movex = 0 and movey <> 0 then print input "You are moving ";movey;" spaces horizontally - is that right ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [loop] else 'calculate how much energy is required drain = movey * 100 goto [moveship] end if end if
The fourth option for movement is not enclosed in an if then statement. It is the only other possible alternative after the other three have been evaluated as false. We simply fall through to this condition, which is a diagonal movement. It is special because of the energy drain calculation we need to perform (refer to the rules above). Before we actually calculate drain we verify with the user that they really do want to move diagonally. It is another input with a default indicated, old hat now...
'the only other option is that we are moving diagonally print input "You are moving diagonally to (";x;",";y;") - is that right ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [loop] else
If the user does not respond as "N" for "no" then we find ourselves here evaluating the energy drain. It is basically the calculation of the length of the hypotenuse of the triangle formed by the vertical and horizontal distances to be traveled, multiplied by 1.25.
Liberty Basic moves numeric variables freely between floating point values and integer values. Once we perform the calculation we will have a floating point value with an undetermined length of decimal points. It is cumbersome to deal with when all we really want is integers. There are several ways to convert between floating point values and integers, the most common is the INT function, which we have dealt with before. We could have used it again, by multiplying the result of drain by 100 and then truncating the decimal portion of the number. Consider another approach:
'calculate how much energy is required drain = (sqr((movex * movex) + (movey * movey))) * 1.25 'fix the drain variable so that it only has two decimal places drain$ = using("######.##",drain) drain = val(drain$) 'now scale drain to fit the model drain = drain * 100
In the code above we have used the USING function to set the result of the first drain calculation to a specific format. This is what USING does. A quick look into the Liberty Basic help file tells us:
This function formats numericExpression as a string using templateString. The rules for the format are similar to those in Microsoft BASIC's PRINT USING statement, but since using( ) is a function, it can be used as part of a larger BASIC expression instead of being useful only for output directly. The template string may consist of the character "#" to indicate placement for numerals, and a single dot "." to indicate placement for the decimal point. The template string must be contained within double quote marks. A template string would look similar to this:
amount$ = using("######.##", 1234.56)
The real advantage of the USING function over using the INT function with a scaled value (a value multiplied by 100) is that the USING function also rounds for us. This is what the help file goes on to say about that:
The using() function for Liberty BASIC 3 has been modified so that it rounds its output like PRINT USING does in other BASICs.
So using the USING function we have both fixed the number of decimal places at two and caused Liberty Basic to round our value up or down as appropriate. The result of the using function is a string, so we have stored the result in the variable drain$. Next we convert the value back to a number using the VAL function and then multiply that number by 100 to get a whole number representing the total number of gigajoules required for the ship to move to the new location.
Next we complete the IF-THEN statement with the following code, and also close the upper level IF-THEN block that we entered some time back in response to the users choice not to quit the game (this was the ELSE side of that clause).
goto [moveship] end if end if
If you have lost touch with where we are in our pseudo code, we have completed everything (except actually printing instructions) through the end of step 4. We are now starting step 5, processing the condition where the user has chosen not to move.
This is a fairly simple step. We use the RND function multiplied by 40 to get a number between .000001 and 39.99999. Adding 81 to this scales it to our desired range of about 80 through 120. The INT function is used to discard the decimal portion of the value as we do not need this. The result is our new energy harvested which we will add to the current energy. We tell the user how much energy was added after we have actually added it.
If the total energy exceeds the maximum amount of 485 gJ we inform the user and then cap the total at 485 gJ. Then we go to another yet unwritten routine called [placemine].
[sitstill] 'Now lets do the part where we do not move, gather energy newenergy = int(rnd(0)*40)+81 energy = energy + newenergy print print "You have harvested ";newenergy;" gigajoules of energy." if energy > 485 then print "Unfortunatly your storage cells can not hold more than 485 gigajoules." energy = 485 end if goto [placemine]
The next routine is a bit more complex. Here we actually move the ship. There still are a few verifications to complete before we can start the process of moving the ship. We still have not checked to see if we are landing on the startgate perimeter which is designated by '*' around the open gate. This is the first thing we do in the "moveship" routine which we are now creating. This is step 6 now in our pseudo code.
[moveship] 'We can move the ship - make sure we are not landing on the gate if starmap$(x,y) = "*" then print "You have chosen an illegal location - this is the stargate perimeter" input "Press enter to select a new location...";a$ goto [loop] end if
So, it appears that the move we have decided on is valid, now we need to see if we have enough energy to actually complete the move. We echo the energy requirement to the screen. If there is not enough energy, inform the user and see if they want to move elsewhere, or simply set still. Move elsewhere is the default and will cause the program to branch back up to the [loop] label and start the move over.
'we know the energy drain - verify we have enough energy print print "This move requires ";drain;" gigajoules of energy." if drain > energy then print "You do not have enough energy to make the move!" input "Do you want to select another destination ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then goto [sitstill] else goto [loop] end if end if
We know if we are here that the move is valid and there is enough energy to complete the move. We have several things to do now. We will remove the energy from the reserves. We will set the place of ship origin to a period (signifying empty space). We will see if the coordinates on the starmap contain a 'k' (if they do then we have hit a mine!). We will change the new coordinates to the symbol for the ship and finally update the X and Y position of the ship. Note that if we did hit a mine that we will once again go to an unwritten routine. Here is the code:
'deduct the energy first energy = energy - drain 'now set the original ship coordinates to a period starmap$(shipx, shipy) = "." 'now see what is in the new location - is it a mine? if starmap$(x,y) = "k" then [hitmine] 'it must not be a mine - put the ship there starmap$(x,y) = "@" shipx = x shipy = y
Tell the user about the successful move!
'tell the user about the move print print "You have moved your ship to star postion ";shipx;",";shipy
Finally if we are in coordinates 8,12 then we have entered the stargate and we have conquered this region of space. We win. Jump to the as yet unwritten routine called "[winner]".
'If this is the stargate portal - you win. if shipx = 8 and shipy = 12 then [winner]
So that brings us to the end of the ship movement. We now need to place a mine at a random place in space. This is the beginning of the routine [placemine] as designated by the label. The first thing out is to select a random X and Y coordinate.
[placemine] 'now we place another of those pesky K mines x = int(rnd(0)*8)+1 y = int(rnd(0)*12)+1
Once the coordinates have been selected, we verify whether they are valid. There are a few places we do not want to place a mine.
'make sure the mine is not placed in an invalid location if x = 8 and y = 12 then [placemine] if x = 7 and y = 12 then [placemine] if x = 7 and y = 11 then [placemine] if x = 8 and y = 11 then [placemine] if x = 1 and y = 1 then [placemine]
We also do not want to place a mine on top of another mine. It the selected location already has a mine in it, we will go back to the beginning of the routine and get a new random set of coordinates.
'if there is already a mine there then choose again if starmap$(x,y) = "k" then [placemine]
So we actually have a valid location to put a mine. That was really not so hard. Now we actually place the 'k' mine by changing the starmap$ array value to a "k". We also inform the user where the mine was placed.
'add the mine to the starmap starmap$(x,y) = "k" print "A 'k' mine was placed at star position ";x;",";y
Of course there is one more step - we need to see if the 'k' mine was placed on top of the ship. We do this by comparing the X and Y coordinate values with the shipx and shipy coordinate values which have the current ship location. If they are equal, then we go to the as yet unwritten [hitmine] routine.
'see if the ship was there... if shipx = x and shipy = y then [hitmine]
This completes step 8 in the pseudo code. We actually jump to step 11 now to finish up the tail end of the main game loop. In this little bit of code we prepare for the next move. We inform the user that this is the end of the current move and to press ENTER to go on. We increment the move counter and then clear the screen and then jump back up to the label [loop].
'That is the end of normal processing - the move ends print input "End of move ";move;", press enter to start next move...";a$ 'increment the move counter move = move + 1 'clear the screen cls 'go back up to the top of the loop and begin another move goto [loop]
Now that the main game loop is complete, we can write the rest of the code. At this point you can test some of what you have done. If you hit a mine or win or quit you will produce an error because you will be trying to execute code at a label we have not created yet. Let get to work and finish those loose ends off.
You can write the next bit of code at the very bottom of the program (following the showmap routine) or in the middle where we are now. Since these routines are not part of the main game loop the location is not as critical. Lets begin with the routine for winning.
This is begun with the label we branch to called "[winner]". Next we inform the user they won, and how many moves it took to win. Finally we ask them if they want to play again. Here, as elsewhere, we indicate a default value (in this case 'Y' for play again). The code is familiar by now. If the user chooses 'N' then the game ends. Anything else will cause the program to branch to [beginAgain] and the game will start over. Pretty basic stuff by now I hope.
[winner] print "You have won! - Way to go!" print "It took you ";move;" moves to reach the stargate!" print input "Do you want to play again ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then end else goto [beginAgain] end if
The next routine is the hitmine routine. This one does a bit more processing, but nothing complex. First we let the user know the bad news:
[hitmine] print "B O O M ! ! ! ...You hit a 'K' mine!"
Next we remove a life.
'subtract a life lives = lives - 1
If there are still lives left over after we deduct one, then we continue the game. We need to let the user know what is about to happen. Then we put the spaceship back at the starting position: 1,1 and set energy to zero.
if lives > 0 then print "Your ship is returned to star position 1,1" print "All of your energy reserves have been drained." shipx = 1 shipy = 1 energy = 0
Since the game is not over, we inform the user of the end of this move, increment the moves by one (we do this here because we missed it at the end of the main game loop since we jumped out of the game loop). Clear the screen and then go back to the top of the main game loop.
print input "End of move ";move;", press enter to start next move...";a$ 'increment the move counter move = move + 1 'clear the screen cls goto [loop] end if
If lives are zero, then we have been beaten by the computer. The game is over. Let the user know and see if they want to play again.
'Looks like the game is over! print "All of your ships have been destroyed..." print "You have lost this round. Better luck next time!" print input "Do you want to play again ([Y] or n)?";a$ if instr(upper$(a$),"N") > 0 then end else goto [beginAgain] end if
Now we only have two remaining loose ends. The "quitting" routine and the "instructions" routine. I put both at the very end of my program. They are fairly simple and straight forward:
[quitting] end [instructions] print "---------------------------------------------------------------" print print "Instructions... Page 1 of 2" print Print "The goal is to move your spaceship '@' form star position 1,1" print "through the stargate at star position 8,12. Avoid 'K' mines " print "as you go, they are randomly placed in the starfield each move." print print "Each move requires power. 100 gigajoules is required to move" print "one space horizontally or vertically. Diagonal movements " print "require larger amounts of power but are more efficient." print print "Power increases by resting a move. A random amount of power" print "between 80 and 120 gigajoules will be added each time you do" print "not move your ship for a turn. You ship has limited capacity" print "to store energy. Any energy gathered over 485 gigajoules will" print "be dumped overboard to avoid an explosion." print print "Did we mention that space is a hostile place? It is! The danger" print "is 'K' mines, which the enemy is randomly dropping into space." print "Be careful moving about so that you do not hit a 'K' mine or have " print "one placed on top of you!" print input "Press ENTER to go to the next page...";a$ print print "---------------------------------------------------------------" print print "Instructions continued... Page 2 of 2" print print "If you are hit by a 'K' mine you will loose all your stored" print "power and be returned to star position 1,1. You have three lives." print "You will lose the game if you are hit by 'K' mines three times in" print "your attempt to pass through the stargate." print print "The interface is very simple - answer 'yes' or 'no' questions by" print "typing either a 'y' or an 'n' and pressing ENTER. All prompts" print "have default values indicated by a bracketed value, for example a" print "yes or no question will offer a choice like this: ([Y] or n)." print "Pressing the ENTER key will select the default 'Y' in the example." print print "Coordinates to move to also offer the current location as default" print "values. Pressing ENTER at the prompt for each will cause the " print "spaceship to remain in the same location for that turn, harvesting" print "energy." print Print "Good luck and have fun!" print input "Press ENTER to begin the game";a$ cls return
This completes the program. A complete listing of the program is available in Appendix A.