Create Zork - Tutorial Part III - PARSER
BASIC SYNTAX OF THE PARSER
Finally, it's time to learn the parser!
Before Zork came along, the first adventure games used simple command systems. Some were menu driven such as 'Hunt the Wumpus', one of the very first adventure games. The game had only three commands the player could choose from, 'Shoot, Move or Quit (S-M-Q)'. Shortly after that, the Colossal Cave Adventure came on the scene with a two word parser that allowed the player to enter a verb followed by a noun, such as 'enter building'. That type of two word parser was commonly used in many text adventure games up until Zork. The Zork parser blew the other games out of the water with it's sophisticated interpreter which allowed players to enter full sentences, often with many commands. With such a robust parser, the player was able to manipulate the game world much more. This allowed him to become more immersed in the game and this is the parser you will learn to make.
A great deal of what the parser does is related to objects. Opening objects, dropping objects, examining objects and the list goes on. The parser is practically made for objects. If we keep this in mind, it will make programming the parser a bit easier. The parser is also used to give the player directives like moving from one location to another or saving the game. Yet, the player can also be viewed as an object if you think about it.
All parser commands start with a verb, such as take, drop and examine, and are usually followed by a noun.
The basic syntax of a simple two word parser is this:
VERB NOUN
VERB = 'DROP'
NOUN = 'STONE'
'DROP STONE'
The Zork parser expanded on that syntax by adding a preposition followed by a second noun. This allows the player to use one object with another. The basic syntax of this parser is the four word construct:
VERB NOUN PREPOSITION NOUN
VERB = 'PUT'
NOUN = 'GOLD'
PREP = 'IN'
NOUN = 'SACK'
'PUT GOLD IN SACK'
You can also label the syntax like this:
ACTION OBJECT PREP OBJECT
This basic construct is at the heart of the Zork parser. This can be expanded further because objects can have descriptions, such as, GOLD COIN'. So the syntax with descritpions are:
ACTION DESC OBJECT PREP DESC OBJECT
'PUT RED CRAYON ON WOODEN DESK'
'PUT THE RED CRAYON ON THE WOODEN DESK'
This basic syntax can be added to further if we join two commands with a conjuction like this:
'PUT CRAYON ON DESK AND TAKE THE PEN FROM THE DRAWER'
The basic syntax is still the same, we just have two commands together:
'PUT CRAYON ON DESK'
'TAKE PEN FROM DRAWER'
We would deal with this as if the player entered two separate commands, one at a time.
Now that you know the basic syntax, we can begin coding the parser in the next section.
SECTION II - CODING THE PARSER
The best way to code the parser is to break everything down into steps. Below is a list of the main steps or tasks that we have to code. I will give a short summary of each step and then we will learn how to code them.
Step 1
Copy each word from the command sentence and put them into an array.
It is much easier to deal with the command if we have the sentence broken down into individual words in an array and we can focus on one word at a time. Let's look at a typical command sentence:
'PUT THE BROWN SACK ON THE TABLE'
When we put each word into an array, it might look like this:
ARRAY(1) = 'PUT'
ARRAY(2) = 'THE'
ARRAY(3) = 'BROWN'
ARRAY(4) = 'SACK'
ARRAY(5) = 'ON'
ARRAY(6) = 'THE'
ARRAY(7) = 'TABLE'
Now, this is much easier to manage when we are trying to process the commands.
When coding the parser it's always a good idea to simplify; cut out anything unnecassary. We want to get the command sentence 'boiled down' to just the bare-bones. One obvious thing we could do is remove all 'the' words. These words tell us nothing, so we can remove them.
Our array now looks like this:
ARRAY(1) = 'PUT'
ARRAY(3) = 'BROWN'
ARRAY(4) = 'SACK'
ARRAY(5) = 'ON'
ARRAY(7) = 'TABLE'
The next thing we can do is find any descriptive words and remove them. The word 'brown' in 'brown sack' can be removed. Now the array is even shorter:
ARRAY(1) = 'PUT'
ARRAY(4) = 'SACK'
ARRAY(5) = 'ON'
ARRAY(7) = 'TABLE'
What we are left with is just the essentials, and it is manageable.
Step 2
Identify word types.
Every word in a command is of a certain type. For instance, the mailbox is an object. That's the type of word it is, an object word. We need to identify each word and the type of word it is. Is it an object? A verb? We will identify the word type with an integer. Here is a list of types we will use:
0 - UNKNOWN TYPE
1 - VERB
2 - OBJECT DESCRIPTION
3 - OBJECT
4 - PREPOSITION
5 - CONJUNCTION
Looking at the previous command, we can see what type each word is:
PUT=1 BROWN=2 SACK=3 ON=4 TABLE=3
The word 'put' is type 1 which is a verb, 'brown' is type 2 which is a description. The word 'on' is a type 4 which is a preposition.
Prepositions are words such as 'on', 'in' and 'from' found in typical commands, such as:
'PUT GOLD IN SACK'
'TAKE GOLD FROM SACK'
Conjunctions join command sentences together. The most common word for this is the word 'and'.
'TAKE SWORD AND GO EAST'
These are two different commands separated by the conjunction 'AND'.
When we come accross a word type which we can't identify then we mark it as unknown and display a message telling the player.
Step 3
Process the words of the command.
This is a fairly simple task. Look at the first word in the array which is the first word of the command. The first word should always be a verb, if it isn't then display an error message and return to main loop. If the first word is a verb then call the subroutine for that verb.
For instance, if the first word is 'TAKE' then you would execute the 'GOSUB TAKE' routine.
These three steps are the basic setup of your parser and we will now look at the code for each step starting with step 1.
Coding Step 1
Copy each word from the command sentence and put them into an array.
Before we can copy each word in the command sentence to an array, we have to create the array first.
REM COMMAND ARRAY
TYPE CA
CWORD AS STRING
CTYPE AS INTEGER
ENDTYPE
The first variable in the array is a string which will store each word from the command sentence. The second variable will store what type of word it is, such as verb, object etc.
Now we are going to make the array a dynamic array. This array will not have a set size because when the player enters his commands we have no way of knowing the number of words in those commands. They will vary from one command sentence to another. For instance, 'TAKE MAP' is a two word command while 'UNLOCK IRON GATE WITH SKELETON KEY' is a six word command.
We will call our array BWORDS().
DIM BWORDS() AS CA
This dynamic array will grow in size as needed depending on the number of words in the command.
Now that we have an array we will make a subroutine that copies each word in the command and plugs them into the array. We will name this subroutine CMD_BATCH, because the array will basically be a batch of command words. So our code to call this routine is:
GOSUB CMD_BATCH
This line of code would go at the end of our PARSE routine.
Now let's look at the actual subroutine itself. The first line gets the character length of the command.
REM CHARACTER LENGTH OF COMMAND
CL = LEN(CMD$)
So, if the command were 'GO EAST', CL would equal 7, seven characters including the space. Why do we need to know the length of the command?
Because it will help us as we try to identify each word in the command sentence. Every word is separated by a space, so whenever we reach a space in the command, we know that the previous letters make up a word.
We will use a loop to go through each character one at a time to see if it is a space. In order for us to look at all of the characters, we have to know how many characters there are total in the command sentence.
In this example, the character length of the command sentence is 7, thus CL is equal to 7. So we know the last character is at character position number 7. This variable lets us know when we have looked at the last character.
Now we need a variable that keeps track of the specific character we are currently looking at. We will look at the very first character starting out, so we make the variable equal to 1.
REM CHARACTER POSITION
CP = 1
As we copy each word into the array we will need to have an array index that increases with every word.
So for every word that we find, the index will increase by 1. We will give it the value of 0 starting out.
BW_INDX = 0
Next we empty the array. This is necessary because the next time the player enters another command we don't won't this array to contain any words from the previous command. So, this empties out the array and lets us start over again for each new command.
EMPTY ARRAY BWORDS()
Now we create the loop, this loop will repeat over and over again until we get to the last letter of the command and have every word copied to the array. We will use the REPEAT-UNTIL loop.
REM PERFORM LOOP UNTIL ENTIRE BATCH IS COMPLETE
REPEAT
The first thing we do inside the loop is look at each character, to see if it is a space.
REM IF WE FIND A SPACE THEN PUT TRAILING WORD(previous letters) INTO BATCH(array)
IF MID$(CMD$, CP) = CHR$(32)
The
'IF MID$(CMD$, CP)' says look at the middle of this command, at the character position equal to CP. Let's say that CP is currently equal to 5. So, this looks at the fifth character in the command sentence. If the command is 'TAKE GOLD' then the fifth character is a space. The
'= CHR$(32)' is the ascii representation of a space. So, that command line means:
'if the fifth character = a space'
If it does equal a space then we can say that the previous characters are a word. For instance, in the command 'TAKE GOLD', all the characters before the space make up the word 'TAKE'. That is our word that we want to put into the array.
Before we copy it into the array we need to make room for the word inside of the array. Remember this is a dynamic array. It grows one word at a time and starting out it has no room. So, we tell it to add a new array room, and insert it starting at the bottom or beginning of the array:
ARRAY INSERT AT BOTTOM BWORDS()
Now that we have increased the size of the array by 1, we need to assign it's new size to the array index.
As this loop executes over and over again the array size will grow as each new word is added. The index is the number of the latest array room that has been added and that tells it where to put the next word in the array:
BW_INDX = ARRAY COUNT(BWORDS())
Next we copy all the previous characters that were behind the space that we found. Those characters are the word which we put into the array:
BWORDS(BW_INDX).CWORD = LEFT$(CMD$, CP)
Let's look at that a bit closer.
'LEFT$(CMD$, CP)' is every character to the LEFT of character position CP. So, if the command is 'GO EAST' and CP is at the
space character position which is position number 3, then every character before position number 3 is a word. In this case there are two characters before the space, a 'G' and an 'O', so the word is 'GO'. All the command is saying is:
BWORDS(BW_INDX).CWORD = "GO"
The next line of code chops off the previous word from the command, so 'GO EAST' becomes 'EAST'. Here is why we do that. As each word is found and copied to the array we no longer need it in the command. There is also a more important reason. Let's say the command is this:
'TAKE ROPE AND KNIFE'
Remember, each time we find a space, we copy all the previous letters as one word, and plug it into the array. Well, if we get to the second space in this command, all the previous characters are,
'TAKE ROPE'. We only want to copy the word
'ROPE' not
'TAKE ROPE'. If we remove each
word as we copy them, we won't have this problem. For instance:
'TAKE ROPE AND KNIFE'
We copy 'TAKE' then remove it from the command sentence. The command sentence now becomes:
'ROPE AND KNIFE'
And every character that comes before the space is now
'ROPE' and not
'TAKE ROPE'
We remove, leave out each word as we copy them in this way. Here is the command that does that:
CMD$ = RIGHT$(CMD$, (CL-CP))
CL is the length of the command and CP is the character position. What this says is, make the new command sentence = the old command sentence minus every character up to and including the current character position. So, once again let's look at the command:
'TAKE ROPE AND KNIFE'
The character length of this command is 19, so
CL = 19. The first space we come to is at character position 5, so
CP = 5. CL-CP is 19-5 or
14. So, the 14 characters to the RIGHT of the command (CMD$) is 'ROPE AND KNIFE'. So the new command sentence is 'ROPE AND KNIFE'. We have left out the word 'TAKE'. Basically, we removed that word from the command sentence. Thus:
CMD$ = RIGHT$(CMD$, (CL-CP)) - When we copy the new command sentence from the old command sentence, we leave out the previous word.
So, let's see it again, step by step:
The original command is 'TAKE ROPE AND KNIFE'
This command is 19 characters in length, CL = 19.
The space is at character position 5, CP = 5.
CMD$ = RIGHT$(CMD$, (CL-CP))
Same as:
CMD$ = RIGHT$(CMD$, (19-5))
Same as:
CMD$ = RIGHT$(CMD$, (14))
Same as:
CMD$ = 'ROPE AND KNIFE' (notice its the last 14 characters on the right of the original command)
The word 'TAKE' has been removed.
Each time we leave out a word from the command sentence, it's character length changes, gets smaller. We need to get the new command length:
CL = LEN(CMD$)
Since we are adding another word to the array, we need to increase the array index by 1.
INC BW_INDX
So you can see, the array gets larger as we copy more words to it, while the command sentence gets smaller as we remove words from it. Remove word from command and put it into array, that's how it works.
Once we finish a word copy, we now reset the character position back to 0 and start all over until we find another space with another word. For every space we find, we copy a word and then remove it from the command.
CP = 0
ENDIF
The endif signifies the end of the condition:
If we find a space then copy word into array and remove word from command, end condition.
If we still haven't found a space yet, then we increment the character position by 1 and when the loop repeats, it will look at the next character to see if it is a space. So, the process begins again until the last character of the command sentence is looked at.
INC CP
UNTIL CP => CL : REM REPEAT LOOP UNTIL LAST CHARACTER OF COMMAND IS REACHED
After the loop is finished and there is no more spaces, the command sentence will contain one last word. We copy this remaining word into the array:
REM GET LAST WORD
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = CMD$
And that's it. That is the process for finding the words in the command sentence and putting them into the array.
Here is the code for that subroutine.
REM GET BATCH COMMANDS
CMD_BATCH:
REM CHARACTER LENGTH OF PLAYER COMMAND
CL = LEN(CMD$)
REM SPACES COUNTER
CP = 1
BW_INDX = 0
EMPTY ARRAY BWORDS()
REM PERFORM LOOP UNTIL ENTIRE BATCH IS COMPLETE
REPEAT
REM COME UPON A SPACE THEN PUT TRAILING WORD INTO BATCH
IF MID$(CMD$, CP) = CHR$(32)
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = LEFT$(CMD$, (CP))
CMD$ = RIGHT$(CMD$, (CL-CP))
CL = LEN(CMD$)
INC BW_INDX
CP = 0
ENDIF
INC CP
UNTIL CP => CL : REM REPEAT LOOP UNTIL LAST CHARACTER OF COMMAND IS REACHED
REM GET LAST WORD
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = CMD$
RETURN
You will notice that it is not that long really, and fairly easy to follow.
Now, here is the entire program with the new code:
REM Project: Zork Tutorial
REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM
ink rgb(255,255,255), 1
set text font "TIMES NEW ROMAN"
SET TEXT SIZE 16
SET TEXT TO NORMAL
REM MAXIMUM NUMBER OF LOCATIONS
MAX_LOC = 7
REM CREATE A TYPE DEFINITION FOR THE LOCATION VARIABLES
type LOC
LOCATION as string
NORTH as integer
SOUTH as integer
EAST as integer
WEST as integer
UP as integer
DOWN as integer
endtype
REM CREATE AN ARRAY OF LOCATIONS
dim LOC_ARRAY(MAX_LOC) as LOC
REM POPULATE THE ARRAY WITH MAP LOCATION DATA
for i = 1 to MAX_LOC
read LOC_ARRAY(i).LOCATION
read LOC_ARRAY(i).NORTH
read LOC_ARRAY(i).SOUTH
read LOC_ARRAY(i).EAST
read LOC_ARRAY(i).WEST
read LOC_ARRAY(i).UP
read LOC_ARRAY(i).DOWN
next i
REM LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN
data "West of House", 2,4,-1,0,0,0
data "North of House", 0,-2,3,1,0,0
data "East of House", 2,4,0,5,0,0
data "South of House", -2,0,3,1,0,0
data "Kitchen", 0,0,3,6,7,0
data "Living Room", 0,0,5,-3,0,0
data "Attic", 0,0,0,0,0,5
REM OBJECTS
REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME
MAX_OBJ = 2
REM CREATE A TYPE DEFINITION FOR OBJECTS
type OBJ
NAME as string
LOCATION as integer
INVENTORY as integer
endtype
REM CREATE AN ARRAY OF OBJECTS
dim OBJ_ARRAY(MAX_OBJ) as OBJ
REM POPULATE THE ARRAY WITH OBJECT DATA
for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
read OBJ_ARRAY(i).INVENTORY
next i
REM NAME, LOCATION, INVENTORY
data "small mailbox", 1, 0
data "leaflet", 2, 0
REM PLAYER STARTING LOCATION
PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE
REM PLAYER CAN'T GO THAT WAY MESSAGES
MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I
REM MESSAGES 0 - 3
DATA "You can't go that way."
DATA "The door is boarded and you can't remove the boards."
DATA "The windows are all boarded."
DATA "The door is nailed shut."
REM PARSER VARIABLES
REM COMMAND LENGTH
CL = 0
REM KEYWORDS
MAX_KW = 2 : REM TOTAL KEYWORDS
DIM KWORD$(MAX_KW)
FOR I = 1 TO MAX_KW
READ KWORD$(I)
NEXT I
DATA "put", "-on"
REM BATCH COMMAND ARRAY
TYPE BCA
CWORD AS STRING
CTYPE AS INTEGER
ENDTYPE
REM CREATE DYNAMIC ARRAY
DIM BWORDS(0) AS BCA
REM DISPLAY CURRENT LOCATION
GOSUB CRNT_LOC
REM DISPLAY CURRENT LOCATION DESCRIPTION
GOSUB CRNT_LOC_DESC
REM LINE SPACING
PRINT
REM DISPLAY OBJECTS AT PLAYER LOCATION
GOSUB DISPLAY_OBJECTS
REM MAIN GAME LOOP
DO
REM LINE SPACING
PRINT
REM GET PLAYER COMMAND
INPUT "> ", CMD$
CMD$ = LOWER$(CMD$)
REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS
CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF
REM PROCESS COMMAND
GOSUB PARSE
REM LINE SPACING
PRINT
SYNC
LOOP
REM END MAIN LOOP
REM DISPLAY CURRENT LOCATION
CRNT_LOC:
PRINT LOC_ARRAY(PLR_LOC).LOCATION
RETURN
REM END GOSUB
REM DISPLAY CURRENT LOCATION
CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house, with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no door here, and all"
PRINT "the windows are boarded up. To the north a narrow path winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the forest to the east. In"
PRINT "one corner of the house there is a small window which is slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to have been used"
PRINT "recently for the preparation of food. A passage leads to the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east, a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB
REM DISPLAY OBJECTS
DISPLAY_OBJECTS:
REM LIST ANY OBJECTS AT PLAYER LOCATION
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).LOCATION = PLR_LOC
PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."
ENDIF
NEXT I
RETURN
REM END SUB
REM PARSE PLAYER COMMAND
PARSE:
REM RETURN TO MAIN LOOP IF CMD$ IS EMPTY
IF LEN(CMD$) < 1
PRINT "Time passes."
RETURN
ENDIF
MOV_MSG = 0
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF LOC_ARRAY(PLR_LOC).NORTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).NORTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).NORTH)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "s"
IF LOC_ARRAY(PLR_LOC).SOUTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).SOUTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).SOUTH)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "e"
IF LOC_ARRAY(PLR_LOC).EAST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).EAST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).EAST)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "w"
IF LOC_ARRAY(PLR_LOC).WEST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).WEST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).WEST)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "u"
IF LOC_ARRAY(PLR_LOC).UP > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).UP
PLR_MOVE = 1
ELSE
PLR_MOVE = -1
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).UP)
ENDIF
ENDCASE
CASE "d"
IF LOC_ARRAY(PLR_LOC).DOWN > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).DOWN
PLR_MOVE = 1
ELSE
PLR_MOVE = -1
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).DOWN)
ENDIF
ENDCASE
REM LOOK
CASE "l"
PLR_MOVE = 1
ENDCASE
REM INVENTORY
CASE "i"
INV_CNT = 0
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).INVENTORY = 1
INC INV_CNT
IF INV_CNT = 1 THEN PRINT "You are carrying:"
PRINT OBJ_ARRAY(I).NAME
ENDIF
NEXT I
IF INV_CNT = 0
PRINT "You are empty-handed."
ENDIF
RETURN
ENDCASE
CASE "take leaflet"
IF OBJ_ARRAY(2).LOCATION = PLR_LOC
OBJ_ARRAY(2).INVENTORY = 1
OBJ_ARRAY(2).LOCATION = 0
PRINT "Taken."
ELSE
PRINT "You can't see any leaflet here!"
ENDIF
RETURN
ENDCASE
CASE "drop leaflet"
IF OBJ_ARRAY(2).INVENTORY = 1
OBJ_ARRAY(2).INVENTORY = 0
OBJ_ARRAY(2).LOCATION = PLR_LOC
PRINT "Dropped."
ELSE
PRINT "You don't have any leaflet!"
ENDIF
RETURN
ENDCASE
ENDSELECT
IF PLR_MOVE = 1
REM DISPLAY NEW LOCATION
GOSUB CRNT_LOC
REM DISPLAY NEW LOCATION DESCRIPTION
GOSUB CRNT_LOC_DESC
REM LINE SPACING
PRINT
REM DISPLAY OBJECTS AT PLAYER LOCATION
GOSUB DISPLAY_OBJECTS
RETURN
ELSE
IF PLR_MOVE = -1
REM DISPLAY APPROPRIATE MESSAGE THAT PLAYER CANNOT GO THAT DIRECTION
PRINT NOGO$(MOV_MSG)
RETURN
ENDIF
ENDIF
REM GET BATCH COMMANDS
GOSUB CMD_BATCH
RETURN
REM END SUB
REM GET BATCH COMMANDS
CMD_BATCH:
REM CHARACTER LENGTH OF PLAYER COMMAND
CL = LEN(CMD$)
REM SPACES COUNTER
CP = 1
BW_INDX = 0
EMPTY ARRAY BWORDS()
REM PERFORM LOOP UNTIL ENTIRE BATCH IS COMPLETE
REPEAT
REM COME UPON A SPACE THEN PUT TRAILING WORD INTO BATCH
IF MID$(CMD$, CP) = CHR$(32)
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = LEFT$(CMD$, (CP))
CMD$ = RIGHT$(CMD$, (CL-CP))
CL = LEN(CMD$)
INC BW_INDX
CP = 0
ENDIF
INC CP
UNTIL CP => CL : REM REPEAT LOOP UNTIL LAST CHARACTER OF COMMAND IS REACHED
REM COPY LAST WORD
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = CMD$
REM PRINT OUT THE WORDS IN THE ARRAY
FOR I = 0 TO BW_INDX
PRINT "WORD " + STR$(I) + " = " + BWORDS(I).CWORD
NEXT I
RETURN
I have added a bit of code that will print out each word in the array so that you can see for yourself how it works. For instance, if you were to type in:
'TAKE THE KEG OF BEER FROM TROLL'
You will see the array print out like this:
WORD 0 = TAKE
WORD 1 = THE
WORD 2 = KEG
WORD 3 = OF
WORD 4 = BEER
WORD 5 = FROM
WORD 6 = TROLL
Go ahead, give it a try and you will see the parser has copied each word into the array in the code we just made.
In the next section we will move on to Step 2, identifying the word types.
Beyond Zork, takes Zork to the next level. Check it out:
http://gallery.guetech.org/beyond/beyond.html
Coding Step 2
Identify Word Types
Now that we have all the command words in an array, the next step is to identify their types. Identifying what type each word is will help us when we process each command. You will see how this works later.
We will begin with objects. Any word which is an object type will be assigned the number 3.
Let's create a new subroutine named WTYPE, which will identfy word types.
REM ASSIGN TYPES TO WORDS
WTYPE:
Now we identify object types. Remember, we have put all the command words into an array and we have all objects in an array too. What we will do is walk through each command word and compare it to every word in the object array. If the word matches any of those object words then we assign it as an object word type. So the method will be along these lines:
If first command word = object 1 then type of word = 3
If first command word = object 2 then type of word = 3
If first command word = object 3 then type of word = 3
If second command word = object 1 then type of word = 3
If second command word = object 2 then type of word = 3
If second command word = object 3 then type of word = 3
...
Of course, that's not the actual code but pseudo-code that shows you what the routine will do. In this case, we will use two loops. The main loop will pull each command word in the command word array. The second loop will go through each object in the object array.
So, just like the pseudo-code above, it will look at the first command word and compare it to all of the objects in the object array. Then, it will pull the second command word and compare it to all the objects in the object array. It will do this until every command word has been compared and it's type identified.
First, we need to set the index to 0 for the command word array.
WT_INDX = 0
Then we enter the main loop which is a
Repeat-Until loop:
REPEAT
Now make a
For-Next loop to compare the command word to every object in the object array:
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
If the command word matches the object, assign it type number 3.
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
End the condition and go through the loop for each word comparing them to all objects.
ENDIF
NEXT I
Now add 1 to the command word index, which will point to the next command word.
INC WT_INDX
Do the loop again with the next command word until all command words are processed.
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
Here's the entire routine:
REM ASSIGN TYPES TO WORDS
WTYPE:
WT_INDX = 0
REPEAT
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
ENDIF
NEXT I
PRINT BWORDS(WT_INDX).CTYPE
INC WT_INDX
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
RETURN
REM END ASSIGN TYPE
We have now assigned object types, but we also need to assign the rest of the types too. Before we do that though, we need to include something else. If we identify a word as an object type, it would help if we could also find out what the object is. Is it a sword, a torch, etc. What we can do is create a new variable in the command array and assign the object number to it. Let's look at the command array as it is before we add the object number variable:
REM BATCH COMMAND ARRAY
TYPE BCA
CWORD AS STRING
CTYPE AS INTEGER
ENDTYPE
You can see there is one variable to store the command word and one to store the type. Now we add another to store the object number:
REM BATCH COMMAND ARRAY
TYPE BCA
CWORD AS STRING
CTYPE AS INTEGER
CNUM AS INTEGER
ENDTYPE
We named this variable CNUM. Since this variable stores the number of the object, all other words will not use this variable. So, if the word is a verb for instance, the CNUM variable will remain empty. It will only be assigned a number if the type is an object or an object description. If it is then the number of the object will be stored in CNUM.
Now that we have added the variable to the command word array, we can assign the object number in the routine that we made earlier. We add this statement:
REM ASSIGN THE OBJECT NUMBER
BWORDS(WT_INDX).CNUM = I
Now, let's look at the routine after we added the above line of code:
REM ASSIGN TYPES TO WORDS
WTYPE:
WT_INDX = 0
REPEAT
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
REM ASSIGN THE OBJECT
BWORDS(WT_INDX).CNUM = I
ENDIF
NEXT I
PRINT BWORDS(WT_INDX).CTYPE
INC WT_INDX
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
RETURN
REM END ASSIGN TYPE
Notice that we assigned the CNUM = I. 'I' is the current object count, the object's number. Thus, if the 'sword' is the second object then the CNUM variable will = 2. This is good, because when we process the command we'll already know what object the player is talking about.
Then we can look at the object attributes to see if the player can use the object with the command he entered. For instance, if the player tries to take the mailbox, we just look at the object number and find the attributes for that object number. The attribute for taking the mailbox object is 0, i.e. false.
So, far so good. Now we are going to identify types that are object descriptions.
Right now, we don't have any object descriptions in the game yet. So the first thing we need to do is add a couple of descriptions to the objects. In this game, objects can have one or two descriptions. For instance, the mailbox will have a description named 'small'. We will also add the 'elongated brown sack'. As you can see, the sack has two descriptions.
We add the object description variables to our current object array:
REM CREATE A TYPE DEFINITION FOR OBJECTS
TYPE OBJ
NAME AS STRING
DESC1 AS STRING
DESC2 AS STRING
LOCATION AS INTEGER
INVENTORY AS INTEGER
ENDTYPE
We named the variables, DESC1 and DESC2. If an object has only one description or no description at all then we just make it an empty string.
So, the data statements for these two objects in the array are:
DATA "mailbox", "small", "", 1, 0
DATA "sack", "elongated", "brown", 2, 0
Object Name, Desc1, Desc2, Location, and Inventory
Notice there is only one description for the mailbox. DESC2 is just an empty string ""
We assign the word type for object descriptions by looking at the DESC1 and DESC2 variables of the objects. If the command word matches any of these descriptions then the word type will = 2.
Now, let us go back to the For-Next loop in the type routine. After we check to see if the word is an object, we then check to see if it is an object description:
REM FOUND OBJECT DESCRIPTIONS MATCH
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC1 OR BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC2
So, if the command word is Desc1 or Desc2 then we assign the type as a description:
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 2
Remember a bit earlier, when we assigned the type for objects, we then added a variable to include the object number too? Well, we are also going to assign the object number to the same variable for object descriptions.
The object number will tell us what object that description goes with. If the player says 'brown mailbox', we need to look at the word 'brown' and if it is a description for the sack and not the mailbox, then we know that 'brown mailbox' isn't correct and we tell the player there is no brown mailbox.
We assign the object number in the same way as before:
REM ASSIGN THE OBJECT
BWORDS(WT_INDX).CNUM = I
Now, here is the routine with the new code for description types:
REM ASSIGN TYPES TO WORDS
WTYPE:
WT_INDX = 0
REPEAT
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
REM ASSIGN THE OBJECT
BWORDS(WT_INDX).CNUM = I
ENDIF
REM FOUND OBJECT DESCRIPTIONS MATCH
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC1 OR BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC2
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 2
REM ASSIGN THE OBJECT NUMBER
BWORDS(WT_INDX).CNUM = I
ENDIF
NEXT I
INC WT_INDX
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
RETURN
REM END ASSIGN TYPE
Everything is the same except we added a second IF statement for description types. In the next section we will add the verb, prep and conj types. After that, we will go on to Step 3 where we will process the commands.
Coding Step 2 - Continued
Identify Word Types - Verbs, Prepositions and Conjunctions
Now that we have identified word types for objects and object descriptions, we will identify them for the verbs, preps and conjs. First, we will make an array to store these words. It will be called the keyword array.
TYPE KW
KWORD AS STRING
KTYPE AS INTEGER
ENDTYPE
There are two variables in the keyword array. One to store the word (verb, prep or conj) and one to store it's type (1, 4 or 5).
MAX_KW = 5 : REM TOTAL KEYWORDS
DIM KW_ARRAY(MAX_KW) AS KW
FOR I = 1 TO MAX_KW
READ KW_ARRAY(I).KWORD
READ KW_ARRAY(I).KTYPE
NEXT I
DATA "take", 1
DATA "drop", 1
DATA "go", 1
DATA "on", 4
DATA "and", 5
For now, we will have just five words. The first three are verbs, the fourth is a prep and the last word is a conjunction. We will place this array definition along with the other ones at the start of the program code.
Now that we have defined the keywords, let's include them in the loop where we identify word types. We already have identified object and object description types in the loop. Right below that code we will include a small routine to see if any of the command words match the keywords from the array. If they match, we will assign either a verb type, conj or prep type.
We'll start it with the usual For-Next loop:
FOR I = 1 TO MAX_KW
Now, compare the command word to the keyword:
IF BWORDS(WT_INDX).CWORD = KW_ARRAY(I).KWORD
If it matches, assign the type:
BWORDS(WT_INDX).CTYPE = KW_ARRAY(I).KTYPE
That's it!
ENDIF
NEXT I
Now we have compared all the command words to every type of word in the game. We are almost ready to start processing the commands, but before we do that we should take care of a few things. What if a command word does not match any of the words in our game? We need to send a message to the player informing him that we don't know what that word is. Let's put some code in the routine that does that:
IF BWORDS(WT_INDX).CTYPE < 1
PRINT "I don't know the word '" + BWORDS(WT_INDX).CWORD + "'."
EXIT
ENDIF
This checks the word type, and if it is less than 1 then no word type has been assigned. That means the command word did not match any of the words in the game, so we return a message informing the player. We can also check something else while we're at it.
Earlier in the tutorial I said that every command starts with a verb, an action. What if the player enters a command that does not start with a verb? What we can do is check the first command word and if it is not a verb we return another message to the player:
IF WT_INDX = 0 AND BWORDS(WT_INDX).CTYPE <> 1
PRINT "That sentence isn't one I recognize."
EXIT
ENDIF
This code looks at the command word index and if it is zero then that means the loop is on the very first word in the command array. Then it looks at the word type, and if it does not equal a 3, which represents a verb type, then this command is not a valid command sentence because it does not begin with a verb. We then return a message informing the player.
*In Zork, there can be a few commands that don't start with a verb, but for this tutorial we will stick with the first word is a verb rule.
Well, we have a couple of command messages here that we may want to use again in some other part of the game. What we can do is make a 'Select-Case' code and just reference each message whenever we need to. Let's make a function that will contain the messages in a Select-Case clause.
First, is the function declaration:
FUNCTION COMMAND_MSG(MSG_NUM AS INTEGER, MWORD AS STRING)
The name of the function is COMMAND_MSG. The function will accept an integer and a string. The integer, MSG_NUM, will identify the message number.
For instance, if MSG_NUM = 1, then we will display the first message. The MWORD string is a command word which is either not recognized by the game or not understood in the way it is used. I will soon show you an example which will make this clear.
After the function declaration we make the rest of the function which consists of the messages in the Select-Case clause:
SELECT MSG_NUM
CASE 1
PRINT "That sentence isn't one I recognize."
ENDCASE
CASE 2
PRINT "I don't know the word '" + MWORD + "'."
ENDCASE
ENDSELECT
ENDFUNCTION
So, let's take an example. The player enters this command: 'Take Playstation'. Since there is no 'Playstation' in this game we need to return a message informing the player just like we did earlier. This time we will use the message function. Right now there are two messages in the message function. The message we want to use for this example is the second message in Case 2. The game does not know the word 'Playstation'. So, in the message function, we assign 2 to the MSG_NUM variable, which represents Case 2, or the second message. Then we assign the word, 'Playstation' to the MWORD variable. So our function call will look like this:
COMMAND_MSG(2, "playstation")
In the actual game, we will not hard-code the word 'playstation' in the function code. We don't know what the actual command word will be but we do know that whatever it is, it is in the command word array. So the actual function call will be:
COMMAND_MSG(2, BWORDS(WT_INDX).CWORD )
In many cases, we do not need to pass a word to the message function. For instance, the first message in case 1 does not use any command word. In those cases, we just pass an empty string:
COMMAND_MSG(1, "")
And that's about it for the message function. Here's the entire function so far:
FUNCTION COMMAND_MSG(MSG_NUM AS INTEGER, MWORD AS STRING)
SELECT MSG_NUM
CASE 1
PRINT "That sentence isn't one I recognize."
ENDCASE
CASE 2
PRINT "I don't know the word '" + MWORD + "'."
ENDCASE
ENDSELECT
ENDFUNCTION
We will add more messages as we need them in the game.
Now that we have a command message function, we will call it for any messages instead of typing in the same message each time. So let's go back to the previous code where we printed the unknown word message:
IF BWORDS(WT_INDX).CTYPE < 1
PRINT "I don't know the word '" + BWORDS(WT_INDX).CWORD + "'."
EXIT
ENDIF
Let's change that code to use the new message function:
IF BWORDS(WT_INDX).CTYPE < 1
COMMAND_MSG(2, BWORDS(WT_INDX).CWORD )
EXIT
ENDIF
We also had a message for any command that did not start with a verb:
IF WT_INDX = 0 AND BWORDS(WT_INDX).CTYPE <> 1
PRINT "That sentence isn't one I recognize."
EXIT
ENDIF
Let's also change that code to use the new message function:
IF WT_INDX = 0 AND BWORDS(WT_INDX).CTYPE <> 1
COMMAND_MSG(1, "")
EXIT
ENDIF
This command function is not really required but it makes it convenient to send messages. It is entirely up to you if you want to use the function or not, but it is there for you. We will add a couple of more messages to it as the tutorial progresses.
Now, here is the complete word type subroutine and below that is the message function at the end of the program:
REM ASSIGN TYPES TO WORDS
WTYPE:
REM COMMAND WORD TYPE INDEX
WT_INDX = 0
REPEAT
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
REM ASSIGN THE OBJECT
BWORDS(WT_INDX).CNUM = I
ENDIF
REM FOUND OBJECT DESCRIPTIONS MATCH
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC1 OR BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC2
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 2
REM ASSIGN THE OBJECT NUMBER
BWORDS(WT_INDX).CNUM = I
ENDIF
NEXT I
REM COMPARE KEYWORDS
FOR I = 1 TO MAX_KW
REM IF COMMAND WORD MATCHES A KEYWORD
IF BWORDS(WT_INDX).CWORD = KW_ARRAY(I).KWORD
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = KW_ARRAY(I).KTYPE
ENDIF
NEXT I
IF BWORDS(WT_INDX).CTYPE < 1
COMMAND_MSG(2, BWORDS(WT_INDX).CWORD )
EXIT
ENDIF
REM NOW CHECK TO SEE IF FIRST WORD IS VERB, IF NOT DISPLAY ERROR AND EXIT LOOP
IF WT_INDX = 0 AND BWORDS(WT_INDX).CTYPE <> 1
COMMAND_MSG(1, "")
EXIT :REM EXIT OUT OF PARSER LOOP
ENDIF
INC WT_INDX
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
RETURN
REM END ASSIGN TYPE
FUNCTION COMMAND_MSG(MSG_NUM AS INTEGER, MWORD AS STRING)
SELECT MSG_NUM
CASE 1
PRINT "That sentence isn't one I recognize."
ENDCASE
CASE 2
PRINT "I don't know the word '" + MWORD + "'."
ENDCASE
ENDSELECT
ENDFUNCTION
Well, we have accomplished a lot. We put all of the command words into an array. We identified all of their types and created a message function too. Here is the listing of the complete game code thus far:
REM Project: Zork Tutorial
REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM
set display mode 640, 480, 16
SET TEXT FONT "TIMES NEW ROMAN"
SET TEXT SIZE 16
SET TEXT TO NORMAL
REM MAXIMUM NUMBER OF LOCATIONS
MAX_LOC = 7
REM CREATE A TYPE DEFINITION FOR THE LOCATION VARIABLES
type LOC
LOCATION as string
NORTH as integer
SOUTH as integer
EAST as integer
WEST as integer
UP as integer
DOWN as integer
endtype
REM CREATE AN ARRAY OF LOCATIONS
dim LOC_ARRAY(MAX_LOC) as LOC
REM POPULATE THE ARRAY WITH MAP LOCATION DATA
for i = 1 to MAX_LOC
read LOC_ARRAY(i).LOCATION
read LOC_ARRAY(i).NORTH
read LOC_ARRAY(i).SOUTH
read LOC_ARRAY(i).EAST
read LOC_ARRAY(i).WEST
read LOC_ARRAY(i).UP
read LOC_ARRAY(i).DOWN
next i
REM LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN
data "West of House", 2,4,-1,0,0,0
data "North of House", 0,-2,3,1,0,0
data "East of House", 2,4,0,5,0,0
data "South of House", -2,0,3,1,0,0
data "Kitchen", 0,0,3,6,7,0
data "Living Room", 0,0,5,-3,0,0
data "Attic", 0,0,0,0,0,5
REM OBJECTS
REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME
MAX_OBJ = 10
REM CREATE A TYPE DEFINITION FOR OBJECTS
TYPE OBJ
NAME AS STRING
DESC1 AS STRING
DESC2 AS STRING
LOCATION AS INTEGER
INVENTORY AS INTEGER
TAKE AS INTEGER
C_ON AS INTEGER
ENDTYPE
REM CREATE AN ARRAY OF OBJECTS
DIM OBJ_ARRAY(MAX_OBJ) AS OBJ
REM POPULATE THE ARRAY WITH OBJECT DATA
FOR I = 1 to MAX_OBJ
READ OBJ_ARRAY(I).NAME
READ OBJ_ARRAY(I).DESC1
READ OBJ_ARRAY(I).DESC2
READ OBJ_ARRAY(I).LOCATION
READ OBJ_ARRAY(I).INVENTORY
READ OBJ_ARRAY(I).TAKE
READ OBJ_ARRAY(I).C_ON
NEXT I
REM NAME, LOCATION, INVENTORY
DATA "mailbox", "small", "", 1, 0, -5, 0
DATA "leaflet", "", "", 2, 0, 1, 0
DATA "sword", "elvish", "", 2, 0, 1, 0
DATA "table", "", "", 5, 0, -7, 1
DATA "north", "", "", 0, 0, -1, -1
DATA "south", "", "", 0, 0, -1, -1
DATA "east", "", "", 0, 0, -1, -1
DATA "west", "", "", 0, 0, -1, -1
DATA "up", "", "", 0, 0, -1, -1
DATA "down", "", "", 0, 0, -1, -1
REM PLAYER STARTING LOCATION
PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE
REM PLAYER CAN'T GO THAT WAY MESSAGES
MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I
REM MESSAGES 0 - 3
DATA "You can't go that way."
DATA "The door is boarded and you can't remove the boards."
DATA "The windows are all boarded."
DATA "The door is nailed shut."
REM PARSER VARIABLES
REM COMMAND LENGTH
CL = 0
REM KEYWORDS
TYPE KW
KWORD AS STRING
KTYPE AS INTEGER
ENDTYPE
MAX_KW = 5 : REM TOTAL KEYWORDS
DIM KW_ARRAY(MAX_KW) AS KW
FOR I = 1 TO MAX_KW
READ KW_ARRAY(I).KWORD
READ KW_ARRAY(I).KTYPE
NEXT I
DATA "take", 1
DATA "drop", 1
DATA "go", 1
DATA "on", 4
DATA "and", 5
REM BATCH COMMAND ARRAY
TYPE BCA
CWORD AS STRING
CTYPE AS INTEGER
CNUM AS INTEGER
ENDTYPE
REM CREATE DYNAMIC ARRAY
DIM BWORDS() AS BCA
REM DISPLAY CURRENT LOCATION
GOSUB CRNT_LOC
REM DISPLAY CURRENT LOCATION DESCRIPTION
GOSUB CRNT_LOC_DESC
REM LINE SPACING
PRINT
REM DISPLAY OBJECTS AT PLAYER LOCATION
GOSUB DISPLAY_OBJECTS
REM MAIN GAME LOOP
DO
REM LINE SPACING
PRINT
REM GET PLAYER COMMAND
INPUT "> ", CMD$
CMD$ = LOWER$(CMD$)
REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS
CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF
REM PROCESS COMMAND
GOSUB PARSE
REM LINE SPACING
PRINT
SYNC
LOOP
REM END MAIN LOOP
REM DISPLAY CURRENT LOCATION
CRNT_LOC:
PRINT LOC_ARRAY(PLR_LOC).LOCATION
RETURN
REM END GOSUB
REM DISPLAY CURRENT LOCATION
CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house, with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no door here, and all"
PRINT "the windows are boarded up. To the north a narrow path winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the forest to the east. In"
PRINT "one corner of the house there is a small window which is slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to have been used"
PRINT "recently for the preparation of food. A passage leads to the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east, a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB
REM DISPLAY OBJECTS
DISPLAY_OBJECTS:
REM LIST ANY OBJECTS AT PLAYER LOCATION
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).LOCATION = PLR_LOC
PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."
ENDIF
NEXT I
RETURN
REM END SUB
REM PARSE PLAYER COMMAND
PARSE:
REM RETURN TO MAIN LOOP IF CMD$ IS EMPTY
IF LEN(CMD$) < 1
PRINT "Time passes."
RETURN
ENDIF
MOV_MSG = 0
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF LOC_ARRAY(PLR_LOC).NORTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).NORTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).NORTH)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "s"
IF LOC_ARRAY(PLR_LOC).SOUTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).SOUTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).SOUTH)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "e"
IF LOC_ARRAY(PLR_LOC).EAST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).EAST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).EAST)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "w"
IF LOC_ARRAY(PLR_LOC).WEST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).WEST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).WEST)
PLR_MOVE = -1
ENDIF
ENDCASE
CASE "u"
IF LOC_ARRAY(PLR_LOC).UP > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).UP
PLR_MOVE = 1
ELSE
PLR_MOVE = -1
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).UP)
ENDIF
ENDCASE
CASE "d"
IF LOC_ARRAY(PLR_LOC).DOWN > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).DOWN
PLR_MOVE = 1
ELSE
PLR_MOVE = -1
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).DOWN)
ENDIF
ENDCASE
REM LOOK
CASE "l"
PLR_MOVE = 1
ENDCASE
REM INVENTORY
CASE "i"
INV_CNT = 0
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).INVENTORY = 1
INC INV_CNT
IF INV_CNT = 1 THEN PRINT "You are carrying:"
PRINT OBJ_ARRAY(I).NAME
ENDIF
NEXT I
IF INV_CNT = 0
PRINT "You are empty-handed."
ENDIF
RETURN
ENDCASE
ENDSELECT
IF PLR_MOVE = 1
REM DISPLAY NEW LOCATION
GOSUB CRNT_LOC
REM DISPLAY NEW LOCATION DESCRIPTION
GOSUB CRNT_LOC_DESC
REM LINE SPACING
PRINT
REM DISPLAY OBJECTS AT PLAYER LOCATION
GOSUB DISPLAY_OBJECTS
RETURN
ELSE
IF PLR_MOVE = -1
REM DISPLAY APPROPRIATE MESSAGE THAT PLAYER CANNOT GO THAT DIRECTION
PRINT NOGO$(MOV_MSG)
RETURN
ENDIF
ENDIF
REM FIRST STEP: COLLECT ALL WORS FROM SENTENCE AND PUT THEM IN AN ARRAY
GOSUB WBATCH
REM SECOND STEP: ASSIGN A TYPE TO EACH WORD AND CHECK FOR SYNTAX ERRORS
GOSUB WTYPE
RETURN
REM END SUB
REM GET BATCH COMMANDS
WBATCH:
REM CHARACTER LENGTH OF PLAYER COMMAND
CL = LEN(CMD$)
HWORD$ = ""
REM SPACES COUNTER
CP = 1
BW_INDX = 0
EMPTY ARRAY BWORDS()
ARRAY INDEX TO BOTTOM BWORDS()
REM PERFORM LOOP UNTIL ENTIRE BATCH IS COMPLETE
REPEAT
REM COME UPON A SPACE THEN PUT TRAILING WORD INTO BATCH
IF MID$(CMD$, CP) = CHR$(32)
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
HWORD$ = LEFT$(CMD$, (CP-1))
BWORDS(BW_INDX).CWORD = HWORD$
REM IF WORD IS 'THE' THEN DELETE IT FROM ARRAY
IF HWORD$ = "the"
ARRAY DELETE ELEMENT BWORDS(0).CWORD, BW_INDX
ENDIF
INC BW_INDX
CMD$ = RIGHT$(CMD$, (CL-CP))
CL = LEN(CMD$)
CP = 0
ENDIF
INC CP
SYNC
UNTIL CP => CL : REM REPEAT LOOP UNTIL LAST CHARACTER OF COMMAND IS REACHED
REM GET LAST WORD
ARRAY INSERT AT BOTTOM BWORDS()
BW_INDX = ARRAY COUNT(BWORDS())
BWORDS(BW_INDX).CWORD = CMD$
REM IF LAST WORD A 'the' JUST IGNORE IT
IF CMD$ = "the" THEN DEC BW_INDX :REM PUSH BACK ARRAY INDEX SO PARSER NEVER GETS TO LAST WORD
RETURN
REM END OF GBATCH
REM ASSIGN TYPES TO WORDS
WTYPE:
REM COMMAND WORD TYPE INDEX
WT_INDX = 0
REPEAT
REM CHECK ALL OBJECTS AND DESCRIPTIONS TO SEE IF ANY MATCH COMMAND WORD
FOR I = 1 to MAX_OBJ
REM IF COMMAND WORD MATCHES AN OBJECT
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).NAME
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 3
REM ASSIGN THE OBJECT
BWORDS(WT_INDX).CNUM = I
ENDIF
REM FOUND OBJECT DESCRIPTIONS MATCH
IF BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC1 OR BWORDS(WT_INDX).CWORD = OBJ_ARRAY(I).DESC2
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = 2
REM ASSIGN THE OBJECT NUMBER
BWORDS(WT_INDX).CNUM = I
ENDIF
NEXT I
REM COMPARE KEYWORDS
FOR I = 1 TO MAX_KW
REM IF COMMAND WORD MATCHES A KEYWORD
IF BWORDS(WT_INDX).CWORD = KW_ARRAY(I).KWORD
REM ASSIGN TYPE TO ARRAY
BWORDS(WT_INDX).CTYPE = KW_ARRAY(I).KTYPE
ENDIF
NEXT I
IF BWORDS(WT_INDX).CTYPE < 1
COMMAND_MSG(2, BWORDS(WT_INDX).CWORD )
EXIT
ENDIF
REM NOW CHECK TO SEE IF FIRST WORD IS VERB, IF NOT DISPLAY ERROR AND EXIT LOOP
IF WT_INDX = 0 AND BWORDS(WT_INDX).CTYPE <> 1
COMMAND_MSG(1, "")
EXIT :REM EXIT OUT OF PARSER LOOP
ENDIF
INC WT_INDX
UNTIL WT_INDX > ARRAY COUNT(BWORDS())
RETURN
REM END ASSIGN TYPE
FUNCTION COMMAND_MSG(MSG_NUM AS INTEGER, MWORD AS STRING)
SELECT MSG_NUM
CASE 1
PRINT "That sentence isn't one I recognize."
ENDCASE
CASE 2
PRINT "I don't know the word '" + MWORD + "'."
ENDCASE
ENDSELECT
ENDFUNCTION
Now it is time to process the commands!