Chapter 8 - Role Play
8.1 RPGs
Role play games are regarded both as a generic form and a specific game type. As a generic term it covers all games where you take on a simulated task of some sort, whether a band of thieves in a forest, an airline pilot or a bank manager.
Role play games as a specific game type don't originate with computers at all, but with people acting out character parts within a set of rules and guidelines set out by the creator of the RPG. Generally, a 'world' is described where the players live, fight, and perform (almost) all the normal human activities. As the scenario is an artificial world the game maker can make available whatever characteristics or new sciences he or she likes. The favourite addition is, of course magic.
RPGs are often open ended with no specific task to perform but guidelines are given for improving your status in the game world. From there on, as in the real world, you learn by experience. You must therefore be consistent in any rules you apply. Players will lose interest if they find that the same monster is dispatched in different ways simply because they meet it in a different area of the game.
RPGs fit very easily into computers. At a basic level you can write one using text instructions and keyboard input. The game can be completely open ended in that there need not be a specific set of tasks to perform. This is the typical scenario of the Dungeons and Dragons type games. Players can simply wander around your artificial world picking up treasures and becoming steadily more adept at dealing with obstacles, puzzles and monsters.
The first essential point is that you must have a map of the layout of your game. This can be based on a two dimensional grid or, if the scenario is, for example, a castle, you can create a three dimensional plan. All treasure, wizards and monsters can be identified simply by grid reference. If your player has the same reference then they are in the same area and you can give the player a brief description of the scene and what options are available to him or her.
This structure lends itself very well to desktop working as you can use a set of icons for both objects and activity choices. These can be dropped on a simple sprite background within a window, with mouse clicks over the icons for various actions. In this way no text is needed at all.
For example with a two dimensional forest game world with text and graphic components you can maintain a location map of place description strings with a parallel array with sprite lists for the major drawing. In this simple plan you could limit the area to between 0 and 8 locations in both the X and Y directions. Your player starts off with area coordinates of 4,4. In the absence of any specific alternative the game can assume a standard trees and shrubs background.
Although the game world is two dimensional the scene can be drawn as a three dimensional picture. You need a string array with a list of all the objects and a parallel array giving the X,Y map coordinates of the objects and the object type. The arrays would include screen plotting position and sprite number of the graphic representation of the objects. These arrays are then scanned to see if any objects are located at 4,4 and if so they are plotted and described.
Anything picked up should be marked as at location -1,0. This can be scanned when the object needs to be used, and as we have no negative elements in the map only the X coordinate need be checked. Location -2,0 can be used for objects or monsters that are destroyed or hidden. The game can randomly re-generate these in new locations. This gives an apparently limitless supply of creatures to use. For items permanently destroyed I suggest -3,0 as their location
Using ordinary X,Y movement controls your player can move to 4,5 or 5,4 or 3,4 or 4,3. Logically these would correspond to East, North, West and South respectively. Obviously directions like 5,5 would give Northeast. Once at the new location the arrays can be scanned again and the new scene drawn up.
Listing 8.1 is a skeleton of this idea with minimal text descriptions rather than a graphical display.
REM > RPG
:
PROCinitialise
action%=1
REPEAT
IF action% PROCdescribe
action%=GET
CASE action% OF
WHEN ASC"Z":IF X%>0 X%-=1
WHEN ASC"X":IF X%<wide% X%+=1
WHEN ASC"/":IF Y%>0 Y%-=1
WHEN ASC"'":IF Y%<high% Y%+=1
WHEN ASC"C":PROCcollect
WHEN ASC"F":PROCfight
OTHERWISE action%=0
ENDCASE
UNTIL FALSE
END
:
DEFPROCinitialise
MODE 12
OFF
PRINT TAB(31,5) "Demo RPG Game"
X%=4:Y%=4
xloc%=0:yloc%=1:type%=2
collect%=FALSE
fight%=FALSE
RESTORE+0
READ wide%,high%
DIM place$(wide%,high%)
FOR J%=0 TO wide%
FOR I%=0 TO high%
READ place$(I%,J%)
IF place$(I%,J%)="" place$(I%,J%)="You are deep in the forest.
Tall pines block your view."
NEXT
NEXT
READ numobs%
DIM object%(numobs%,2),object$(I%)
FOR I%=0 TO numobs%
READ object%(I%,xloc%),object%(I%,yloc%),object%(I%,type%), object$(I%)
NEXT
ENDPROC
DATA 7,7
DATA Loc.0/0,,,,,You are by a swift river,You are on open heath,
DATA This is the wide road to Hanri
DATA ,,,,,,You are near a babbling brook,
DATA You are on an old gravel road
DATA ,,,,,,You are by a crystal stream,
DATA You you are on a narrow track
DATA ,,,,,,,
DATA Loc.0/4,,,,,,,LOC.8/4
DATA ,,,,,,,
DATA ,,,,,,,
DATA Loc.8/0,,,,LOC.8/4,,,Loc.8/8
DATA 2
DATA 4,4,0,A stone seat
DATA 3,4,1,A silver challice
DATA 2,4,2,An ugly Troll
:
DEFPROCdescribe
collect%=FALSE
fight%=FALSE
PRINT'''place$(X%,Y%)
FOR I%=0 TO numobs%
IF object%(I%,xloc%)=X% AND object%(I%,yloc%)=Y% THEN
PRINT object$(I%) " is here"
CASE object%(I%,type%) OF
WHEN 1:collect%=TRUE
WHEN 2:fight%=TRUE
ENDCASE
ENDIF
NEXT
PRINT' "Options:"''SPC 5 "Z - West"'SPC 5 "X - East"'
SPC 5 "' - North"'SPC 5 "/ - South"
IF collect% PRINT SPC 5 "C - Collect"
IF fight% PRINT SPC 5 "F - Fight"
PRINT'"Escape - Stop"''"Your choice :"
ENDPROC
:
DEFPROCcollect
action%=0
IF NOT collect% ENDPROC
FOR I%=0 TO numobs%
IF object%(I%,xloc%)=X% AND object%(I%,yloc%)=Y% AND object%(I%,type%)=1 THEN
object%(I%,xloc%)=-1
PRINT object$(I%) " - carried"
ENDIF
NEXT
collect%=FALSE
ENDPROC
:
DEFPROCfight
action%=0
IF NOT fight% ENDPROC
FOR I%=0 TO numobs%
IF object%(I%,xloc%)=X% AND object%(I%,yloc%)=Y% AND object%(I%,type%)=2 THEN
object%(I%,xloc%)=-2
PRINT object$(I%) " - killed"
ENDIF
NEXT
fight%=FALSE
ENDPROC
Although there is primitive fight recognition full combat sequences are a bit more complicated as you have to give a real air of urgency and at the same time enable the player to respond quickly to the changing situation. It may be best to expand the object types into parallel arrays of friends, foes, treasures, and utility objects.
Each player takes control of a character in the game, these typically have the following attributes:
- Strength - combat ability
- Psi - magical ability
- Intelligence - general capability
- Experience - learned abilities
The ability to win any combat depends on matching these attributes against those of the adversary and assessing which actions are attempted and the response time
Combat is usually broken down to melee rounds and player attributes updated after each round. As well as giving players strike by strike control this lets you generate multi-character combat. Only one-to-one fights take place in each round but by alternating characters you can give the appearance of a real gang fight.
One way to achieve this is to create a pair of arrays to hold character attributes, one for goodies, the other for baddies. At random you can then match any goodie against any baddie. If a character dies its place in the array is taken by another and the array compacted. The combat session finishes when one or other array holds no more characters. There is an outline of this technique in Listing 8.2
REM > Combat
:
PROCinitialise
PROCplayer
PROCfight
PROCendgame
END
:
DEFPROCinitialise
MODE 12
OFF
RESTORE+35
READ weapons%
DIM weapon$(weapons%)
FOR I%=1 TO weapons%
READ weapon$(I%)
NEXT
sword%=1
spell%=2
feint%=3
READ attribnum%
line$=" Character Strength Psi Intelligence Experience"
strength%=1
psi%=2
intelligence%=3
experience%=4
READ goodies%,baddies%
IF goodies%>baddies% size%=goodies% ELSE size%=baddies%
DIM attributes%(1,size%,attribnum%):REM goodies/baddies, characters,size%
DIM gang%(1)
DIM name$(1,size%)
FOR I%=1 TO goodies%
READ name$(0,I%)
FOR J%=1 TO attribnum%:REM zero element used as player flag
READ attributes%(0,I%,J%)
NEXT
NEXT
FOR I%=1 TO baddies%
READ name$(1,I%)
FOR J%=1 TO attribnum%
READ attributes%(1,I%,J%)
NEXT
NEXT
PRINT''"Test Combat sequence"'
VDU 28,0,31,79,VPOS
ENDPROC
DATA 3
DATA Sword,Spell,Feint
DATA 4
DATA 3,3
DATA Dwarf,50,10,9,1
DATA Elf,10,50,15,1
DATA Wizard,5,60,20,1
DATA Troll,50,5,5,1
DATA Minataur,20,30,15,1
DATA Pixie,5,60,15,1
:
DEFPROCplayer
PRINT "Select your character by number"
gang%(0)=goodies%
gang%(1)=baddies%
PROClist(0)
REPEAT
num%=GET-48
UNTIL num%>0 AND num%<=goodies%
CLS
attributes%(0,num%,0)=TRUE:REM set player flag
REM could be repeated for several players & with baddies too
ENDPROC
:
DEFPROClist(flag%)
LOCAL I%,J%
PRINT'line$
FOR I%=1 TO gang%(flag%)
PRINT';I% TAB(2) name$(flag%,I%);
FOR J%=1 TO attribnum%
PRINT TAB(J%*11) attributes%(flag%,I%,J%);
NEXT
NEXT
ENDPROC
:
DEFPROCfight
LOCAL attack%,defend%
gang%(0)=goodies%:REM temp store for fight only
gang%(1)=baddies%
REPEAT
IF INKEY(50)
CLS
PRINT'"Goodies"
PROClist(0)
PRINT'''"Baddies"
PROClist(1)
PRINT'
attack%=RND(2)-1
defend%=1-attack%
assail%=RND(gang%(attack%))
IF assail%=0 assail%=1
IF attributes%(attack%,assail%,0) PROCselect ELSE PROCmatch
PROCstrike
UNTIL gang%(attack%)=0 OR gang%(defend%)=0
ENDPROC
:
DEFPROCselect
PRINT name$(attack%,assail%) ", select your opponent by number"'
REPEAT
oppose%=GET-48
UNTIL oppose%>0 AND oppose%<=gang%(defend%)
PRINT'"Select your weapon by number"'
FOR I%=1 TO weapons%
PRINT"(";I%") " weapon$(I%)
NEXT
REPEAT
type%=GET-48
UNTIL type%>0 AND type%<=weapons%
ENDPROC
:
DEFPROCmatch
oppose%=RND(gang%(defend%))
IF oppose%=0 oppose%=1
IF attributes%(attack%,assail%,strength%)>attributes%(defend%, oppose%,strength%)
type%=1
IF attributes%(attack%,assail%, psi%)>attributes%(defend%,oppose%,psi%)
type%=2 ELSE type%=3
ENDPROC
:
DEFPROCstrike
PRINT "The " name$(attack%,assail%) " attacks the " name$(defend%,oppose%) " ";
CASE type% OF
WHEN sword%:PROCsword
WHEN spell%:PROCspell
WHEN feint%:PROCfeint
ENDCASE
IF INKEY 150
ENDPROC
:
DEFPROCsword
LOCAL diff%,sum%
diff%=attributes%(attack%,assail%,intelligence%)*
attributes%(attack%,assail%,experience%)
-attributes%(defend%,oppose%,intelligence%)
*attributes%(defend%,oppose%,experience%)
IF diff%>90 diff%=90
sum%=(attributes%(attack%,assail%,strength%)+attributes%(defend%,
oppose%,strength%))DIV 4+1
PRINT "with a blade of true steel."
IF RND(9)=1 THEN
PRINT "Fumbled! The " name$(defend%,oppose%) " has a lucky escape."
ELSE
IF RND(diff%)>30 THEN
PRINT "The " name$(defend%,oppose%) " is too clever to be caught so easily."
ELSE
PRINT "A hit. ";
attributes%(defend%,oppose%,strength%)-=sum%
IF attributes%(defend%,oppose%,strength%)<1 THEN
PROCdead(defend%,oppose%)
attributes%(attack%,assail%,strength%)+=(sum% DIV 2)
attributes%(attack%,assail%,experience%)+=1
ELSE
PRINT "The " name$(defend%,oppose%) " is still strong."
attributes%(attack%,assail%,strength%)-=(sum% DIV 2)
IF attributes%(attack%,assail%,strength%)<1 PROCdead(attack%,assail%)
ENDIF
ENDIF
ENDIF
ENDPROC
:
DEFPROCspell
LOCAL diff%
diff%=attributes%(attack%,assail%,intelligence%)
-attributes%(defend%,oppose%,intelligence%)
IF diff%>20 diff%=20
PRINT "with a strange enchantment."
IF RND(9)=1 THEN
PRINT "The " name$(defend%,oppose%) " ducks the spell."
attributes%(defend%,oppose%,experience%)+=1
ELSE
IF attributes%(attack%,assail%,psi%)>attributes%(defend%,oppose%, psi%)
AND diff%>3 THEN
PRINT "The " name$(defend%,oppose%) " is ensnared by the spell."
attributes%(attack%,assail%,psi%)+=1
attributes%(attack%,assail%,experience%)+=1
attributes%(defend%,oppose%,strength%)-=(attributes%(defend%,oppose%,
strength%) DIV 3)
IF attributes%(defend%,oppose%,strength%)<1 PROCdead(defend%,oppose%)
ELSE
PRINT "The " name$(attack%,assail%) " hasn't the mental power to
overcome the " name$(defend%,oppose%)
attributes%(defend%,oppose%,experience%)+=1
ENDIF
ENDIF
ENDPROC
:
DEFPROCfeint
PRINT "by a cunning feint."
IF attributes%(attack%,assail%,intelligence%)>attributes%(defend%,
oppose%,intelligence%) THEN
IF attributes%(attack%,assail%,experience%)>attributes%(defend%,
oppose%,experience%) DIV 2 THEN
PRINT "It fools the " name$(defend%,oppose%) ", but saps strength."
attributes%(attack%,assail%,strength%)-=2
ENDIF
ELSE
PRINT "The "name$(defend%,oppose%)" laughs and continues the fight."
attributes%(defend%,assail%,experience%)+=1
ENDIF
ENDPROC
:
DEFPROCdead(flag%,character%)
PRINT "The " name$(flag%,character%) " dies."
IF character%<gang%(flag%) PROCmovedown
gang%(flag%)-=1
ENDPROC
:
DEFPROCmovedown
LOCAL I%,J%
FOR I%=character% TO gang%(flag%)-1
name$(flag%,I%)=name$(flag%,I%+1)
FOR J%=0 TO attribnum%
attributes%(flag%,I%,J%)=attributes%(flag%,I%+1,J%)
NEXT
NEXT
ENDPROC
:
DEFPROCendgame
LOCAL flag%
CLS
IF gang%(0)>0 THEN
FOR I%=1 TO gang%(0)
IF attributes%(0,I%,0) flag%=TRUE
NEXT
IF NOT flag% PRINT "Unfortunately your character died but t"; ELSE PRINT "T";
PRINT "he good guys won the fight."
ELSE
IF gang%(1)>0 THEN
PRINT "The baddies rule OK!"
ELSE
PRINT "Everyone died. There are no victors."
ENDIF
ENDIF
VDU 26
PRINT TAB(0,5)
ON
ENDPROC
A problem that can arise is where a dying character suddenly unleashes a spell of enormous power that completely destroys the enemy and yet still hasn't the strength to pick up a sword. To avoid this you should make death occur on the basis of several attributes falling below a certain point rather than any single one reaching zero. To add further realism, instead of working directly with the stored figures for attributes work from a continuously updated set of inter-related ones.
If you decide to write an RPG then you must ensure that any magic or pseudo-science you devise is consistent. There is nothing more irritating than finding that (say) a firemaking spell produces a roaring inferno when you have a few wet twigs but not so much as a whiff of smoke from a bundle of old newspapers.
Combining the RPG map routine with the combat program gives a basic text only RPG. This is too crude for today's players, but there is enough here to give an idea as to how you can develop your own ideas into a fully fledged graphical game.
As a final tweak it may be worth considering having the ability for either the goodies or the baddies to decide they are losing too heavily and run away. For this you would keep a check on the average attributes of all the baddies and if it falls below a certain figure then, with a suitable message, re-locate them all to random locations in the game. For the goodies your player would decide when it was time to run away, and possibly which direction to run to.
8.2 Adventures
Adventure games are often thought of as RPGs with the combat section removed, although that is a rather simplistic view. Firstly adventures tend to be more focussed, in that there is a specific set of tasks to be performed, usually in a fixed order. Similarly there is usually a fixed number of objects, monsters, etc.
8.2.1 Rooms
The map structure is not rigidly defined, but moving to different locations in the game should be logical. Going East from a room that you travelled West to reach should take you back to your original location. The exception is mazes, where experienced players will expect peculiar directions. Unlike RPGs adventures don't usually have all possible locations set on a grid, it's normally a free-flowing map.
Because of this the usual representation of the game map is not a simple two dimensional array but an array of pointers and links. The player's current location is a location number, this being an index to the main location array. This is often referred to as a room list . Typically there will be four other rooms that can be reached from any room. Testing an attempt to move, say, North would involve looking at the first direction link, assuming this represents North, and seeing which room number it points to. Zero would mean that this direction is closed off. This map representation is shown in figure 8.1. Notice how the room numbers don't need to follow any special pattern. This is particularly useful when you want to open or close links as the game progresses. Note the one way link between rooms 6 and 4. This is a common way of dealing with cliff tops, pits, etc. where the player can get in but not back out again. Keeping the West option reserved in room 4 allows you to create a magic door back late in the game.
The Room list will also have flags for any special characteristics that each room may have. For example, caves would need light. Below is a typical room list structure. This can be held in parallel arrays, with the text information a string array. The array index numbers would be the actual room numbers.
- Direction link 1
- Direction link 2
- ......
- Room attribute 1
- Room attribute 2
- ......
- Sprite pointer(s)
- Description text pointer(s)
8.2.2 Locating objects
Once you have defined your map you must consider how objects are to be placed. This can be done in a similar manner to that of RPGs. However, instead of storing X,Y coordinates you use an array of objects storing room numbers. The array index number itself is a unique identifier, so you don't need a type element in the array. However, you can usefully keep a set of flags determining the generic attributes of the object. These could be combustible, breakable or wearable, etc. Your object structure would be very similar to the place structure, as below.
- Location;
- Object attribute 1;
- Object attribute 2;
- ......
- Sprite pointer(s);
- Description text pointer(s).
All you need do now for descriptions, or any other object handling, is scan the list picking out any object that is marked as at the room in question. It is usual to mark room 0 as hidden objects, room -1 as carried, and room -2 as worn.
One source of confusion arises when an object can be carried by another object. Does location 5 refer to room 5 or object 5? The solution is simple. Add an offset of say, &1000 to the object numbers. You are never likely to want 4096 rooms and it can easily be masked in and out of calculations and array indices as shown.
index%=object% AND &FFF
object%=index% OR &1000
8.2.3 Understanding Instructions
The core of a text adventure game is the parser. This is the part of the program that reads in a player's input and works out exactly what is being requested. All words read from the text are given number representation. If no known words are found then the word identifiers are set to zero. Older adventures used to recognise simple verb/noun combinations such as Get Brick or Drop Fish. Later versions scanned the text for just these two combinations but were able to skip over and discard any unwanted words as rubbish. This didn't improve the flexibility of the parser but it did allow more natural sentences to be decoded.
A useful addition to this simple parser is the handling of prepositions. These are words that indicate how a noun is used or where it may be found such as:
- Put hat on peg
- Sweep floor with broom
- Look under rug
- Hide key in pocket
Two other useful additions are adjectives and adverbs. Adjectives would be noun modifiers giving results like:
- Get the green bottle
- Find a rough spot
- Examine the open box
Adverbs, as their name suggests are verb modifiers. Using all of these word types will give remarkably intelligent decoding results. All of the following can be recognised by such a parser:
- Quietly enter the open door;
- Carefully drop a silver coin into the metal bucket;
- Eat the big cake quickly.
It is doubtful if there is much to be gained by taking the parser much further, even though it might be an interesting challenge to produce really sophisticated parser of the kind that can handle a sentence like:
Use the string to tie all the keys except the red one to the tag and hang them up
Most game players will quickly revert to quick-fire three or four word commands, with only the occasional attempt at something more exotic when all else fails. Therefore effort spent in developing your parser would, sadly, be wasted.
A somewhat ticklish area is that of handling obscenities. Everyone gets frustrated sometimes and then uses words that no decent computer wants to read. If you decide to recognise such words then you must take great care to ensure that it is completely impossible for your game player to accidentally reveal them, otherwise you could cause very real offence to an innocent player. It is best to recognise the offending words as verbs and then give a simple response like:
- I don't like that kind of language
- You can't do that sort of thing in this game
- This is a family game
You can keep an obscenity count if you like, and after a couple of warnings terminate the player with an act of the supreme being's displeasure.
A working verb, noun, adverb, adjective parser is shown in listing 8.3
REM > Parser
:
PROCinitialise
REPEAT
REPEAT
command$=FNinput
UNTIL command$>""
PROCparse(command$)
PRINT "Verb=";ver%,"Noun1=";no1%,"Noun2=";no2%,"Prep.=";pre%,
"Adj.1=";ad1%,"Adj.2=";ad2%,"Adverb=";adv%'
UNTIL command$="*"
VDU 26
PRINT TAB(0,30)
END
:
DEFPROCinitialise
MODE 12
READ numverbs%
DIM verb$(numverbs%),verb%(numverbs%)
PRINT "Verbs"
FOR I%=0 TO numverbs%
READ verb$(I%),verb%(I%)
PRINT TAB(I%*10) verb$(I%);
NEXT
READ numnouns%
DIM noun$(numnouns%),noun%(numnouns%)
PRINT''"Nouns"
FOR I%=0 TO numnouns%
READ noun$(I%),noun%(I%)
PRINT TAB(I%*10) noun$(I%);
NEXT
READ numpreps%
DIM prep$(numpreps%),prep%(numpreps%)
PRINT''"Prepositions"
FOR I%=0 TO numpreps%
READ prep$(I%),prep%(I%)
PRINT TAB(I%*10) prep$(I%);
NEXT
READ numadjes%
DIM adje$(numadjes%),adje%(numadjes%)
PRINT''"Adjectives"
FOR I%=0 TO numadjes%
READ adje$(I%),adje%(I%)
PRINT TAB(I%*10) adje$(I%);
NEXT
READ numadves%
DIM adve$(numadves%),adve%(numadves%)
PRINT''"Adverbs"
FOR I%=0 TO numadves%
READ adve$(I%),adve%(I%)
PRINT TAB(I%*10) adve$(I%);
NEXT
PRINT''"Enter sentence to parse (capitals only), or * to stop."
VDU 28,0,31,79,VPOS+1
ENDPROC
DATA 7:REM verbs
DATA GO,1,GET,2,CARRY,2,DROP,3,KILL,4,HIT,5,EXAMINE,6,FIND,7
DATA 6:REM nouns
DATA KEY,1,DOG,2,GLASS,3,BEAKER,3,BOX,4,STONE,5,ROCK,5
DATA 11:REM prepositions
DATA INSIDE,1,IN,1,OUTSIDE,2,UNDER,3,OVER,4,ON,5,ONTO,5,BESIDE,6
DATA BY,6,AGAINST,6,WITH,7,USING,7
DATA 7:REM adjectives
DATA RED,1,BLUE,2,GREEN,3,YELLOW,4,BIG,5,LITTLE,6,ROUGH,7,SMOOTH,8
DATA 4:REM adverbs
DATA QUICKLY,1,SLOWLY,2,QUIETLY,3,GENTLY,4,HEAVILY,5
:
DEFFNinput
LOCAL c$
INPUT c$
IF NOT FNmore THEN
WHILE RIGHT$(c$,1)=" "
c$=LEFT$(c$,LEN c$-1)
ENDWHILE
ENDIF
=c$
:
DEFPROCparse(c$)
ver%=0:no1%=0:no2%=0:pre%=0:ad1%=0:ad2%=0:adv%=0
REPEAT
flag%=FNmore
PROCword(verb$(),verb%(),ver%,numverbs%)
PROCword(noun$(),noun%(),no1%,numnouns%)
PROCword(noun$(),noun%(),no2%,numnouns%)
PROCword(prep$(),prep%(),pre%,numpreps%)
IF no1%=0 PROCword(adje$(),adje%(),ad1%,numadjes%) ELSE
PROCword(adje$(),adje%(),ad2%,numadjes%)
PROCword(adve$(),adve%(),adv%,numadves%)
IF flag%=FALSE PROCdiscard
UNTIL c$=""
ENDPROC
:
DEFFNmore
WHILE LEFT$(c$,1)=" "
c$=RIGHT$(c$,LEN c$-1)
ENDWHILE
=c$=""
:
DEFPROCword(t$(),t%(),RETURN w%,n%)
LOCAL a%,w$
IF w%>0 OR flag% ENDPROC
a%=INSTR(c$," ")
IF a%=0 THEN
w$=c$
ELSE
w$=LEFT$(c$,a%-1)
ENDIF
I%=-1
REPEAT
I%+=1
UNTIL I%>=n% OR t$(I%)=w$
IF t$(I%)=w$ THEN
w%=t%(I%)
IF a%=0 c$="" ELSE c$=MID$(c$,a%+1)
flag%=TRUE
ENDIF
ENDPROC
:
DEFPROCdiscard
LOCAL a%,w$
a%=INSTR(c$," ")
IF a%=0 THEN
c$=""
ELSE
c$=MID$(c$,a%+1)
ENDIF
ENDPROC
To extend the vocabulary you need only alter the data lines, increasing the word counter and fitting in the words. You will see that several words can have the same reference number to allow for players using variants of the same command.
Notice how as it strips out words from the input text the parser re-checks for word types it may have missed. This is to cope with the vagaries of English grammar that allow adverbs, in particular, to be put almost anywhere. Both of the following will be correctly read by the example parser:
- Softly stroke the cat
- Stroke the cat softly
Any words that can't be matched are dumped by the discard procedure. This lets your player use all the common redundant conjunctions, giving the feel of real understanding from your game.
8.2.4 Finding Nouns
In a graphic adventure you can isolate objects simply and unambiguously with a mouse click. With a text adventure things are more difficult. There are two basic approaches to resolving this problem. The first, and commonest, is to keep a list of words, as was done in the example parser. However, as with verbs, you need to allow for the player using a similar but not identical word. Take the sentence:
"You are on a rocky hill path. Sharp flinty stones are all around."
If you have a stone as an object your player could quite easily describe it as a rock or a flint, and be most annoyed if the game refuses to understand. This means you need to look at the text very carefully and make sure that you have covered all reasonable possibilities with duplicate noun names. Once you have a match, you need to make sure that the object referred to is actually there, or has been seen by the player. To simply say "You don't have it yet." is a dead giveaway that the object actually exists somewhere in the game.
An alternative method of finding nouns is to use the description text itself. You scan all the text for the current room and all the objects in that room a word at a time. by doing this you can guarantee that the object or place exists, and also that the player can see it. All you need to do is keep a pointer to the text you are scanning at the time the match is made. This is slightly slower than the other method but far more reliable.
8.2.5 Puzzles
Having described and identified everything in the game you now need to do something with that information. Adventure generator programs use a complex pseudo language to build sets of responses to player input. These puzzle systems can consist of many hundreds of lines that are tested until an exact match is found with the action attempted by the player. However, in a home grown game you can get equally good results by using a simple tree structure.
The first step is to isolate the verbs. This is done with a CASE statement as shown.
CASE verb% OF
WHEN 1:PROCcarry
WHEN 2:PROCdrop
WHEN 3:PROCthrow
OTHERWISE:PROCimpossible
ENDCASE
Taking the carry situation you can remove some of the tedious checks as below.
CASE TRUE OF
WHEN carried%>maxc%:PROCtoomany
WHEN weight%>maxw%:PROCtooheavy
WHEN size%>maxs%:PROCtoobig
WHEN objectloc%<1:PROCalreadycarried
WHEN objectloc%<&1000:PROCcarryroom
OTHERWISE:PROCcarryobject
ENDCASE
Notice that there may be some valid situations where you appear to carry a room, but are really handling a hidden object, hence the extra procedure.
Finally, in the carryobject procedure you can have the lines testing specific object characteristics and room locations. Anything not handled specifically, would be passed on to a general pickup routine at the end of the procedure.
8.3 Combat
There is a small group of games that rely on combat only. They still fit with the general classification of role play, but have no storyline or special attributes. These are the martial arts type games. They only really became attractive when there were machines with the ability to run high resolution animated graphics. They are usually single or two player games. The player is given a number of kicks, punches, jumps and rolls, The character on the screen will perform these on a given keypress or mouse click.
The graphical part is really very easy to implement. You simply build up a set of film animations for all the actions you require. These will all take place within a fixed character area. You have an identical set of actions, but with a different sprite set, for the opponent, whether another player or computer driven.
If you use a variant of coordinate collision testing then you can produce a realistic hit system. You relate the damage to your opponent to the distance from the attacking player. Where the two sprites concerned are obviously not in contact at all, the strike just wastes the attacker's energy. As they begin to overlap during the action, you produce a graded energy loss to the character under attack. Hence the reason for keeping the animation within a fixed area.
Usually you need only keep a single energy variable for each combatant, incrementing it with time and successful encounters, decreasing it with failed encounters or unnecessary action. When the energy level falls below a certain level, the character dies. You can give greater realism to this by only allowing the character to perform the more energetic actions, like high kicks, if it's energy level is above a certain figure.
Because these games tend to be particularly fast and furious it is probably best to organise the function keys so that each is assigned a single action, this can be duplicated with a row of matching mouse sensitive icons. This not only allows your player to decide which is more important - speed or keyboard life - but is a useful on screen reminder.
8.4 Simulators
I suppose that simulators can only be thought of as role play in the very broadest sense but nevertheless they still fit the overall classification. The games that most quickly spring to mind when simulations are mentioned are aircraft flight simulators. However, almost any day-to-day activity can provide a basis for simulation. Many education centres use simulated shopping to help teach small children how to handle their money wisely. The logical extension to this is, of course, a trading simulation, where the player run's a large corporate business or even an entire country's economy.
8.4.1 Real World Situation
With real world simulations you need to make a distinction between real time and game time. Logically, if you maintain a ratio of 1 transaction : 1 move, then one year of trading will take you a year to play out on the game. Hardly practical! For many of these games you can use a ratio as coarse as 1 year : 1 move. On the other hand, if you are simulating the running of a power station or chemical works, you would probably work at nearer 1 hour : 1 move.
Unless you are developing a simulation for education use you will need to fine tune the time scale to give a game that is slow enough to be playable without becoming boring. Also, with real world simulators you either need to know a fair bit about statistical analysis, or you will have to develop an idea a bit at a time and make empirical adjustments to keep the simulation in balance.
As an example I'll outline a simulation for I Bodgit, computer manufacturers. Mr Bodgit only makes cheap machines, with no monitor, disk drives, or other accessories. At it's basic level the simulation needs to handle three areas as below.
1 purchase of components
2 manufacture of computers
3 sale of computers
These can be expanded as follows.
1.1 cost of components
1.2 delivery charges
1.3 working capital
1.4 factory storage space
2.1 labour costs
2.2 throughput
2.3 rejects
2.4 warehouse facilities
3.1 asking price
3.2 dealer network/delivery costs
3.3 market saturation
We should also, at this point, consider general aspects that will affect all areas of production. Some of these are as below.
4.1 services (gas, electricity)
4.2 rent/rates
4.3 breakages
4.4 crime
You could now produce a fair simulation with just this information. We'll look at section one in some detail, so you can see how the ideas develop.
The factory storage space limits how many items you can hold in stock and, with your working capital, will limit your purchasing. Also a reasonable simulation should allow for lower price breaks on bulk orders and lower or free delivery charges over a certain order size. All this can be done with simple maths.
8.4.2 Scaling and Balance
It pays to put everything in terms of anonymous units rather than real figures. It's the ratios that matter not the actual figures. These can be scaled later to give meaningful results to the game player as well as a balanced simulation. In our example we can assume that our main stores has a storage capacity of 500 units and storage units required for the parts needed for one computer are as follows.
Units Item
1 plain PCB
3 PCB components
2 keyboard
6 computer case
This gives us a total requirement of 12 units and our factory can store materials for almost 42 computers. However, you also need storage for finished machines. These would logically require slightly more storage space than empty cases, say 7 units, so the player will have to balance the two.
Component cost can be looked at in exactly the same way. You can start with a working capital of 400 units, and cost the parts so.
Units Item
2 plain PCB
20 PCB components
5 keyboard
1 computer case
Again, not all the working capital can be used as you also have to pay wages and other costs. However you should allow players to make this mistake. Let them find out the hard way exactly what happens when their workers don't get paid!
Component costs, wages and final unit price all have to be kept in proportion. You could assume that component costs only make up about a tenth of the asking price of the completed computer. As a rough guide an average week's wages should be set at about half the asking price of one machine.
You will find that balances develop naturally as the game evolves. If, for example, the player allows too little storage space for completed machines the labour force will have to stop work until some computers are sold but still expect their wages. All you need do is to tweak the figures so you don't get runaway situations.
8.4.3 External influences
The situation is slightly more complicated regarding things like market saturation. Here you have to relate the actual number of computers sold to the apparent reluctance for people to buy. For simplicity, we'll consider that all factors, like inflation, recession, and total competitive computer manufacturing are lumped together as a single negative factor.
A separate factor is the total number of computers you've already sold. As people buy your machine, they won't be likely to want another, unless it fails. Eventually your selling capability could stagnate completely.
Putting it simply you have the very approximate formula below.
computers sold = computers available/(machines sold/time)*asking price*saturation
Time and saturation are pseudo constants you should fiddle to get a reasonable balance. Time partially represents the ageing of I Bodgit's cheap computers. With both this and asking price, I've simplified the situation. A very long ageing time will give poorer sales, but in reality too short a time would have the customers grumbling. Similarly, too low an asking price would look rather suspicious. Also, you obviously can't sell 0.132 of a computer, so you take only the integer value.
Of increasing popularity is whole community simulation. This has enormous scope for programmer and player alike. Below is a brief list of the sort of factors you can integrate into such a simulation. The secondary factors listed are just a sample of the relationships you will need to follow up. In fact, almost everything will inter-relate, so the feel of your simulation for the players will be a direct reflection on how thoroughly you understand your community.
- Population - Birth rate, Death rate, Disasters
- Housing - Building, Population, Civil Engineering
- Employment - Manufactured goods, Agriculture, Services
- Crime - Laws, Poverty, Population
- Services - Politics, Infrastructure
- Disasters - War, Natural
Obviously these whole communities are highly complex living organisms in their own right, and at best, yours will be only a very limited simulation. A key point to remember when planning such a game is that trends are usually more significant that isolated incidents. If you can, you should follow up the following lines of enquiry for more information about how groups of people behave.
- History
- Politics
- Local government
- Sociology
- Market research
- Statistics
8.4.4 Graphical Simulations
Unfortunately, many graphical simulations require a lot of drawing rather than sprite plotting, and drawing is usually much slower than sprite plotting. This is particularly relevant with flight simulators, where you draw in real time as the plane flies. However it is often possible to work out a compromise. If you look at figure 8.2, a rather crude drawing, you will see that only the central shaded area has to be drawn, using the three dimensional techniques described earlier. All the rest of the aircraft cockpit can be handled by sprite plotting. For example, there is a limit to the practical resolution of the altimeter and heading dials. Rather than try to draw these it is simpler to have a sprite film of their readings, and select the one nearest to the actual figures. Although rather memory hungry, this technique can be used for numeric as well as metered displays to speed up response time.
Initially you would define a graphic viewport where the dotted rectangle is and perform the drawing in this area. Then you mask out the unwanted parts with sprites of the dashboard, window framing and overhead area. These sprites would also contain all the fine detail of switches, dials and lights that are not in fact active. Finally you can plot in the small dynamic detail.
8.4.5 Terrain Mapping
The discussion so far has assumed flat earth scenarios. With many simulations this is far from the case. The game where this is of greatest significance is probably golf. There are two main variables with the impact of the ball with the ground. The first is the obvious relative height above or below the starting point, and the other is the texture of the surface the ball hits.
Height is relatively easy to deal with. You only need maintain a two dimensional array of spot heights. The elements of the array would represent the height above an arbitrary reference point, spaced, say, 100 metres apart. For simplicity, you then calculate the actual height based on the distance of the ball from the nearest four surrounding points and their actual height figures. Listing 8.3 shows this idea in practice with a graphical display of a small map read from data. The algorithm used is accurate enough for our purposes. As it is quite easy to calculate the average height of a square area a recursive procedure is used create progressively smaller squares round the actual point we wish to calculate, until the X,Y differences, and therefore the height differences, are small enough to be insignificant. You will see that by making extensive use of barrel shifting and scaling our dimensions up the routine is almost entirely integer driven, with no complex calculations at all. This makes it very fast.
REM > Terrain
:
ON ERROR PROCerror:END
PROCinitialise
:
REPEAT
INPUT "Start X (1151 max):" xpos
INPUT "Start Y (511 max):" ypos
INPUT "End X (1151 max):" xend%
INPUT "End Y (511 max):" yend%
CLG
xstep=(xend%-xpos)/width%
ystep=(yend%-ypos)/width%
FOR I%=0 TO width%
pX%=xpos
pY%=ypos
xpos+=xstep
ypos+=ystep
PROCset
POINT I%<<2,FNhigh(S%)>>3
NEXT
UNTIL FALSE
END
:
DEFPROCerror
MODE 12
IF ERR<>17 PRINT REPORT$ " @ ";ERL
ENDPROC
:
DEFPROCinitialise
MODE 12
COLOUR 0,128,128,128
PRINT TAB(30,5) "Terrain Map" TAB(30,7) "Escape to stop"
VDU 28,0,9,23,0
VDU 24,0;0;1279;511;
width%=320
size%=128
acc%=4
RESTORE+10
READ X%,Y%
DIM map%(X%,Y%)
FOR J%=0 TO Y%
FOR I%=0 TO X%
READ height%
map%(I%,J%)=height%<<10
NEXT
NEXT
ENDPROC
DATA 9,5
DATA 1,2,3,4,5,6,6,5,4,3
DATA 2,3,5,5,6,6,7,6,5,3
DATA 4,4,6,6,7,8,7,6,4,2
DATA 5,5,7,6,6,8,6,5,4,3
DATA 3,4,6,5,5,7,6,5,4,3
DATA 2,2,5,3,3,4,3,3,3,4
:
DEFPROCset
X%=pX% DIV size%
Y%=pY% DIV size%
Ah%=map%(X%,Y%)
Bh%=map%(X%+1,Y%)
Ch%=map%(X%+1,Y%+1)
Dh%=map%(X%,Y%+1)
S%=size%
pX%=pX% MOD size%
pY%=pY% MOD size%
ENDPROC
:
DEFFNhigh(S%)
IF S%<acc% THEN
height%=(Ah%+Bh%+Ch%+Dh%)>>3
ELSE
half%=S%>>1
:
IF pX%>half% THEN
pX%-=half%
Ah%=(Ah%+Bh%)>>1
Dh%=(Ch%+Dh%)>>1
ELSE
Bh%=(Ah%+Bh%)>>1
Ch%=(Ch%+Dh%)>>1
ENDIF
:
IF pY%>half% THEN
pY%-=half%
Ah%=(Ah%+Dh%)>>1
Bh%=(Bh%+Ch%)>>1
ELSE
Dh%=(Ah%+Dh%)>>1
Ch%=(Bh%+Ch%)>>1
ENDIF
:
height%=FNhigh(half%)
ENDIF
=height%
Mapping surface texture is probably best done slightly differently. You can usually assume that the fairway is of reasonably consistent bounce and roll performance with some minor random deviations. The rough and bunkers will have virtually zero bounce, and water obstructions will lose the ball altogether. Therefore you need to plan a finer set of coordinates for these obstructions. These are best stored as a list rather than a map, and the ball's position compared with the nominal centre of such obstructions. This also lends itself to defining specific, above ground obstructions, such as trees.
It should be possible to integrate these maps with the game drawing routine so you have consistent views and behaviour regardless of the location of the ball.
8.5 Status Saving and Reloading
Most role play games are designed for many hours of play. Where arcade style games can just about survive without game saving facilities, it is quite unreasonable to expect a role-player to start from scratch each time. To facilitate saving and re-loading you should split your game data into distinct static and dynamic parts. Static data will be the overall map or plan of the game, including any text, sprites and the like. Dynamic data is everything that can possibly be modified by the game as it progresses.
It is remarkably easy to leave out dynamic data from a saving routine. The simplest mistake is where most of the data is in arrays but just a few items are ordinary integer variables. What you have to look out for is saving all the arrays but forgetting, say, the number of monsters killed.
Unless your game is vast and takes up all available disk space I recommend that your program saves dynamic data in a special directory within the game application. There is then no need for the game player to hunt out separate disks, remembering which ones is needed. Your saving and loading system becomes simpler too as you only need to scan the relevant directory and present a list of the player positions available.
As usual, there is an exception to this. If your game is implemented as a fully multi-tasking application it makes sense to use a distinct file type so that a player double clicking on this will load the application and game together in true desktop fashion.
|