If there is an Apshai game engine, it is the DM program. This 342 line BASIC porgram runs all the titles in the Dunjonquest series. The reason one could say its game engine - as opposed to just a game - is because it is largely data driven.
Although its similar in length, the DM program is significantly harder to follow than the Innkeeper, largely due to its reliance on reading and writing variables directly from memory addresses. Consequently, discussion of this program will be spread across a number of posts. The program is fairly logically structured, so we will continue to follow the code as it is laid out in the file.
The first part of the code ( lines 5-5000 ) deals mainly with rendering the dungeon and monsters. The code from 5000-7000 is a large switch-like statement that reads and executes the player's actions. The code from 7000-10000 updates the monsters, and the remaining code handles death, loading/saving etc and other infrequent tasks.
In this post we will mainly look at the utility subroutines, memory layout and some of code used to describe and render the dungeon.
The code starts with a copyright note and is followed by a data array holding the text headings for the left hand status column. This is followed by a call to the subroutine at 20000 which asks which level of the dungeon the player wishes to explore and then loads the appropriate data file. The subroutine at 25000 sets up the high resolution screen and displays the status text.
The naming is slightly odd. "WOUNDS" represents health, so a character that is fully fit will have 100% WOUNDS. Likewise "FATIGUE" represents stamina and a player with 100% FATIGUE is fully invigorated.
KA is an important variable. This points to the address of the data file that will be loaded from disk. These files store descriptions of each level of the dungeon and are the main thing that differentiates dunjonquest titles.
5 REM COPYRIGHT 1979 AUTOMATED SIMULATIONS,REV1(DISK)
6 DATA 195,0,"ROOM NO.:",195,8,"WOUNDS:",195,16,"FATIGUE:",195,24,"WEIGHT:",195,56,"ARROWS:",195,64,"MAG ARRWS:",195,88,"TOTAL SLAIN:"
7 KA = 4194
9 TEXT
10 GOSUB 20000
11 GOSUB 25000
This is followed by some initialization code. The most important lines are 16 and 17 which establish the locations of various important fields within the data file. The file stores 60 room descriptions, followed by 10 trap descriptions, followed by 20 treasure descriptions and 12 monster descriptions. Not all of these entries are necessarily valid - only the starting room - ( always room 1 ) - needs to be valid and levels frequently have fewer than 60 rooms, 12 monsters, etc. Moving to room 0 triggers leaving the dungeon.
12 Q = 60: DIM P(4),S(4),ZA(3),ZD(5),TX(3,1),TY(3,1),TM(5),H(3),T$(9),RF(3),RN(3)
13 Q1 = 12: DIM M$(Q1):KM = 0:B1 = 24
15 Q2 = 20: HOME : DATA 0,2,-1,2,-2,6,2,1,4,1,12,-2,1,-2,2,18,-2,-1,-4,-1,10,13,2,1,1: FOR I = 0 TO 3: READ H(I): FOR J = 0 TO 1: READ TX(I,J),TY(I,J): NEXT J: NEXT I: FOR I = 1 TO 5: READ TM(I): NEXT I
16 NO = KA:NT = 4 * Q + KA:D1 = 8 * Q + KA:D2 = 12 * Q + KA:MT = 16 * Q + KA:MN = MT + Q:NP = MN + Q:XP = NP + Q:YP = XP + Q:TR = YP + Q:XR = TR + Q:YR = XR + Q:KB = KA + 2251: FOR I = 1 TO 60: POKE KA - I,0: NEXT I
17 X1 = YR + Q:X2 = X1 + 2 * Q:Y1 = X2 + 2 * Q:Y2 = Y1 + 2 * Q:TN = Y2 + 2 * Q:TV = TN + 10:TP = TV + 10:TG = TP + 10:TW = TG + 10:TS = TW + Q2:UL = TS + Q2:UA = UL + Q1:UP = UA + Q1:US = UP + Q1:UD = US + Q1:UC = UD + Q1:UR = UC + Q1:UI = UR + Q1:UW = UI + Q1:UV = UW + Q1
20 BL = 84:ZD(0) = 3:ZA(1) = 0:ZD(1) = 0:ZA(2) = 3:ZD(2) = 3:ZA(3) = - 6:ZD(3) = - 2:ZD(4) = 5:ZD(5) = 5:SE = 100
25 DATA "FLAME","DUST","MOLD","PIT","","SPEAR","NEEDLE","XBOW","CAVEIN","CEILING"
30 FOR I = 0 TO 9: READ T$(I): NEXT I
To make things a bit clearer, this is what the first map related part of this file looks like.
{
// NO
byte RoomNumberToNorth[60];
byte RoomNumberToEast[60];
byte RoomNumberToSouth[60];
byte RoomNumberToWest[60];
// NT ( 0 = None, 1= Open, 2 = Door, 3 = Secret Door )
byte ExitToNorth[60];
byte ExitToEast[60];
byte ExitToSouth[60];
byte ExitToWest[60];
// D1 ( offset relative to room origin )
byte NorthExitMin[60];
byte EastExitMin[60];
byte SouthExitMin[60];
byte WestExitMin[60];
// D2 ( offset relative to room origin )
byte NorthExitMax[60];
byte EastExitMax[60];
byte SouthExitMax[60];
byte WestExitMax[60];
// Monster Id ( Index into monster table )
byte MT[60];
// Number of Monsters
byte MN[60];
// Trap Id ( Index into treasure table
byte NP[60];
// Trap X ( relative to room origin )
byte XP[60];
// Trap Y ( relative to room origin )
byte YP[60];
// Treasure Id ( Index into treasure table )
byte TR[60];
// Treasure X ( relative to room origin )
byte XR[60];
// Treasure Y ( relative to room origin )
byte YR[60];
// X1 ( = Low + High * 128 )
byte RoomMinXLow[60];
byte RoomMinXHigh60];
// X2 ( = Low + High * 128 )
byte RoomMaxXLow[60];
byte RoomMaxXHigh60];
// Y1 ( = Low + High * 128 )
byte RoomMinYLow[60];
byte RoomMinYHigh60];
// Y2 ( = Low + High * 128 )
byte RoomMaxYLow[60];
byte RoomMaxYHigh60];
// Followed by Traps, Treasures and Monsters ..,
}
The map adopts a graph-like approach where each room is a node with four compass-aligned edges. These are the routes to other rooms. Each exit has a label (NT) indicating whether it is open, closed, a normal door or a secret door. The locations of each room are specified as points (X1,Y1 and X2,Y2) with X and Y in the range (0-33023). This makes the potentially largest room about 6 square miles in size! The location and width of each exit is given by D1 and D2. These are values in the range 0-1, so specify its size and location relative to the room.
The remainder of the room definition specifies its contents: the id of any monster, the number of monsters, the id and location of any trap and the id and location of any treasure. There can only be one trap, one treasure and one type of monster. This is level two of the dungeon, rendered here in the style of early D&D modules. Each grid square is 5 square feet.
Non-rectangular areas, or areas where multiple doors on a wall are desired, must be made from multiple rooms. E.g the long east-west corridor has multiple exits to the south and north so must be composed from multiple rooms.
Note that the data format does not prevent rooms from overlapping, or require that exits take one to adjacent rooms. We can see that room 25 and 30 overlap slightly - 25 is being rendered over the top of 30. This is likely a minor bug, and the original game does not take advantage of this flexibility. It is exploited in the expansions to add elevation ( rooms above other others ) and other special map effects.
The other levels of the dungeon are similar
The first level
The third level. This is a mine, so there are few doors and a large number of cave-in traps.
The fourth level, containing the temple and priests quarters.
ToA is not quite entirely written in BASIC. There is one short machine code routine stored at 7936. This is used to partially clear the display, erasing the left hand dungeon view while leaving the status column unaffected. This is simply because the way that text is rendered is very slow - refreshing the status column taking seconds. On the Apple II, the out-of-the-box options for writing graphics to the hi-res screen were limited. You could draw points and lines with HPLOT and draw "shapes" with DRAW. Shapes are simple vector graphic objects and could be scaled and rotated. There is no support for bitmap graphics. Or text. Drawing vector shapes is very slow on the Apple's 1 Mhz 6502, but this is the only BASIC supported method of drawing something other than a point or a line. Consequently, this is used to print all the text in the game as well as the players and monsters. The machine code helps a bit by allowing the play area to be cleared independently of the text, which therefore needs only to be rendered once.
The final line jumps to 5000 where the main game loop starts.
35 DATA 169,32
36 DATA 133,25,169,0,133,24,170,168,169,0,145,24,200,192,27,208,249,177,24,41,192,145,24,160,0,165,24,24,232,224,3,208,4,162,0,105,7,105,40,133,24,176,4,144,219,144,217,230,25,165,25,201
37 DATA 64,208,209,96
40 FOR I = 7936 TO 7993: READ J: POKE I,J: NEXT I
45 J9 = 191:J6 = 64: GOTO 5000
Energy Drain. If the player is hit by undead, they take 1 point of damage and their constitution is reduced permanently. This might seem pretty harsh, but is consistent with the 1st edition D&D heritage of the game. In 1st edition D&D, being hit by some undead would cause a character to drop an experience level ( or be instantly killed at 1st level ).
50 PC = PC - 1: POKE KB + 24, PEEK (KB + 24) - 1: POKE KA - 92, PEEK (KA - 92) - 1: RETURN
This is also a good time to introduce a summary of the memory layout. From this we can see that KB+24 and KA-92 are the consitution stat. The rest of memory is laid out as follows:
KA - 96 - stat copy
KA - 90 - 10 special item flags - skull ring, talisman
KA - 80 - 20 Treasures counts
KA - 1 - 60 flags indicating rooms have been drawn
KA + 0 - Dungeon Data ( 2238 bytes )
KB - 12 - Magic Number 123, used to flag coming from DM
KB - 1 - Character Name [11]
KB + 0 - Shield Level
1 - Experience
2 - Experience
3 - Experience
4 - Shield Points
5 - Unused
6 - Healing Potions
7 - Weapon Max Damage
8 - Armour
9 - monster speed
10 - Weapon Plus
11 - Healing Salves
12 - Weight Carried
13 - Arrows
14 - Magic Arrows
15 - Level
16 - Armour Plus
17 - Weapon
18 - Player Bonus
19 - Player Armour
20 - Intelligence
21 - Intuition
22 - Ego
23 - Strength
24 - Constitution
25 - Dexterity
This subroutine clamps the monster location to bounds of the current room. These bounds are stored in W1,W2 and V1,V2.
55 YY = W1 - W2 - 5: IF YM > YY THEN YM = YY
56 IF YM < 5 THEN YM = 5
57 XX = V2 - V1 - 5: IF XM > XX THEN XM = XX
58 IF XM < 5 THEN XM = 5
59 RETURN
This prints a number of arrows at the appropriate location in the status column. The subroutine at 75 handles printing text to the screen. This is used to tell the player how many arrows they have obtained from treasure.
60 QX = 195:QY = 111:Q$ = STR$ (J) + " ARROWS": GOSUB 75: RETURN
This routine flashes the screen, by briefly switching to text mode and then returning to graphics. This is used when the player takes damage. -16304 is $C050, which is a memory mapped switch that can be addressed to change the graphics mode (see e.g. http://apple2.org.za/gswv/a2zine/faqs/csa2pfaq.html#004 ). This is used instead of the BASIC HGR command to avoid clearing the screen ( which HGR does ).
65 HOME : TEXT : FOR I = 1 TO 45: NEXT I: POKE - 16304,0: RETURN
Read a digit from the keyboard; supports actions that require the player to enter a number. The routine to read a character is at 500.
70 GOSUB 500: IF L = 0 THEN 70
71 J = ASC (C$) - 48: IF J < 0 OR J > 9 THEN 70
72 RETURN
This prints the number of normal arrows remaining
73 QX = 255:QY = 56: GOSUB 15000:QX = 267:QY = 56:Q$ = STR$ ( PEEK (KB + 13)): GOSUB 75: RETURN
This prints the number of magic arrows remaining
74 QX = 255:QY = 64: GOSUB 15000:QX = 267:QY = 64:Q$ = STR$ ( PEEK (KB + 14)): GOSUB 75: RETURN
This is the text printing routine.
75 IF LEN (Q$) = 0 THEN RETURN
76 FOR QI = 1 TO LEN (Q$):QQ = ( ASC ( MID$ (Q$,QI,1)) - 32): IF QQ = 0 THEN 78
77 IF QQ > 0 THEN DRAW QQ AT QX + ((QI - 1) * 6),QY: GOTO 79
78 HCOLOR= 4: FOR QJ = 0 TO 6: HPLOT QX + ((QI - 1) * 6),QY + QJ TO QX + (QI * 6),QY + QJ: NEXT QJ: HCOLOR= 7
79 NEXT QI: RETURN
Clear visible rooms. More than one room can be visible at a time as the layout of adjacent rooms ( although not their contents ) is displayed if rooms are connected via an open exit. The game keeps track of which rooms have been drawn in order to avoid the expense of redrawing rooms.
80 FOR I = 1 TO 60: POKE KA - I,0: NEXT : RETURN
This is used to track the state of secret doors.
82 FOR I = 0 TO 3:RN(I) = 0: IF RF(I) > 0 THEN POKE NT + KR + I * Q,3: POKE KA - 61 + KR,0:RN(I) = 1:RF(I) = 0
84 NEXT I:I = KF + 1: IF I > 3 THEN I = I - 4
85 IF RN(KF - 1) > 0 THEN RF(I) = 1
86 RETURN
This seems to be an unused subroutine that increments the value at a memory address.
90 POKE N, PEEK (IA + NN):NN = NN + 1: RETURN
This draws a vertical wall, clipping it to the screen
100 L1 = YY:L2 = YY + L - 1: IF L1 < 0 THEN L1 = 0: IF L2 < 0 THEN RETURN
101 IF L2 > 47 THEN L2 = 47: IF L1 > 47 THEN RETURN
104 FOR II = XX TO XX + 1: IF II < 0 OR II > 47 THEN RETURN
105 I = II: HCOLOR= 6: FOR QI = 0 TO 3: HPLOT I * 4 + QI,L1 * 4 TO I * 4 + QI,L2 * 4 + 3: NEXT QI: HCOLOR= 7
110 NEXT II: RETURN
This partially erases a vertical wall. This is used to create exits. The amount of wall erased is passed in L1 and set by the exit type.
120 L1 = YY:L2 = YY + L - 1: IF L1 < 0 THEN L1 = 0: IF L2 < 0 THEN RETURN
121 IF L2 > 47 THEN L2 = 47: IF L1 > 47 THEN RETURN
122 FOR I = 0 TO I1:NX = 2 * XX + I: IF NX < 0 OR NX > 96 THEN RETURN
125 HCOLOR= 4: FOR QI = 0 TO 1: HPLOT NX * 2 + QI,L1 * 4 TO NX * 2 + QI,L2 * 4 + 3: NEXT QI: NEXT I: HCOLOR= 7: RETURN
This draws a horizontal wall, clipping it to the screen
140 L1 = 2 * XX:L2 = L1 + L - 1: IF L1 < 0 THEN L1 = 0: IF L2 < 0 THEN RETURN
141 IF L2 > 95 THEN L2 = 95: IF L1 > 95 THEN RETURN
142 FOR I = 0 TO I1:NY = YY + I: IF NY < 0 OR NY > 47 THEN RETURN
145 HCOLOR= 6: FOR QI = 0 TO 3: HPLOT L1 * 2,NY * 4 + QI TO L2 * 2 + 1,NY * 4 + QI: NEXT QI: HCOLOR= 7: NEXT I: RETURN
This erases a horizontal wall. The amount of wall erased is passed in I1 and set by the exit type.
150 L1 = 2 * XX:L2 = L1 + L - 1: IF L1 < 0 THEN L1 = 0: IF L2 < 0 THEN RETURN
151 IF L2 > 95 THEN L2 = 95: IF L1 > 95 THEN RETURN
152 FOR I = 0 TO I1:NY = YY + I: IF NY < 0 OR NY > 47 THEN RETURN
155 HCOLOR= 4: FOR QI = 0 TO 3: HPLOT L1 * 2,NY * 4 + QI TO L2 * 2 + 1,NY * 4 + QI: NEXT QI: HCOLOR= 7: NEXT I: RETURN
This draws/erases the player character. Line 205 converts from the games coordinate system to screen space. The players location ( in feet ) is in (XA, YA). The origin of the viewport is in (XB,YB). Y is flipped to convert from the mathematical convention (+Y is up ) to the display convention ( +Y is down ). KF stores the player orientation - this is converted into a rotation for three of the four player orientations. This sets the special variable ROT which is read by DRAW to rotate the shape. The fourth case ( 209) - facing left - uses a separate shape as it is the mirror of facing right, which cannot be obtained by a rotation alone.
200 I1 = 1
205 XX = (2 * (2 * (XA - XB) - 1)) - 1:YY = (4 * (YB - YA)) + 1: ON KF GOTO 206,207,208,209
206 ROT= 48:QZ = 76: GOTO 210
207 ROT= 0:QZ = 76: GOTO 210
208 ROT= 16:QZ = 76: GOTO 210
209 ROT= 0:QZ = 77
210 DRAW QZ AT XX,YY: ROT= 0: RETURN
Draw/Erase monster. Unlike player, monsters are not rotated. MQ is the monster ID and the 12 shapes from 64-75 hold the monster images. The variable I1 is used to control drawing vs erasing. There can only be one monster visible at a time. Some encounters do have multiple monsters, though, but these attack sequentially.
300 I1 = 1
305 XX = 2 * (XM - XB + V1) - 1:YY = YB - YM - W2:QX = XX * 2:QY = YY * 4
306 IF I1 = 0 THEN 340
307 IF I1 = 2 THEN 330
310 DRAW MQ + 63 AT QX,QY: RETURN
This erases a monster
330 HCOLOR= 4: GOSUB 310: HCOLOR= 7: RETURN
345 IF NB = 0 THEN RETURN
350 I1 = 2: GOTO 305
This draws a discovered trap as a small 4x4 pixel square. The player can search for traps as they explore the dungeon.
400 IF I < 0 OR I > 94 OR IK < 0 OR IK > 47 THEN RETURN 402 FOR J = 0 TO 3: HPLOT (I) * 2,IK * 4 + J TO (I) * 2 + 3,IK * 4 + J: NEXT J: RETURN
Erase a drawn trap or treasure
410 IF I < 0 OR I > 94 OR IK < 0 OR IK > 47 THEN RETURN
412 HCOLOR= 4: FOR J = 0 TO 3: HPLOT (I) * 2,IK * 4 + J TO (I) * 2 + 3,IK * 4 + J: NEXT J: HCOLOR= 7: RETURN
Monster death animation
440 FOR L = 1 TO 100: NEXT L: HCOLOR= 4: RETURN
450 QX = XX * 2:QY = YY * 4: HCOLOR= 7: DRAW 80 AT QX,QY: GOSUB 440: DRAW 80 AT QX,QY: HCOLOR= 7: DRAW 81 AT QX,QY: GOSUB 440: DRAW 81 AT QX,QY: HCOLOR= 7: DRAW 82 AT QX,QY: GOSUB 440: DRAW 82 AT QX,QY: HCOLOR= 7
455 DRAW 83 AT QX,QY: GOSUB 440: DRAW 83 AT QX,QY: RETURN
Read input with timeout. Applesoft BASIC does not have a non-blocking method of polling the keyboard, so the game resorts to reading the memory mapped IO address at -16384/$C000. This routine sets L to 1 if input was received within the interval set by LE, and ultimately determined by monster speed SE. There is longer to respond if there are no monsters present ( NB is 0 ).
500 LE = SE / (NB + 1): FOR IC = 1 TO LE
510 IF PEEK ( - 16384) > = 127 THEN 550
520 NEXT IC:L = 0: RETURN
550 C$ = CHR$ ( PEEK ( - 16384) - 128): POKE - 16368,0:L = 1: RETURN
Draw player attack action. This is similar to the other drawing code above and draws the player image in its attack pose.
600 GOSUB 250
605 ON KF GOTO 606,607,608,609
606 ROT= 48:QZ = 78: GOTO 610
607 ROT= 0:QZ = 78: GOTO 610
608 ROT= 16:QZ = 78: GOTO 610
609 ROT= 0:QZ = 79
610 IF K = 0 THEN DRAW QZ AT XX,YY: ROT= 0: RETURN
620 HCOLOR= 4: DRAW QZ AT XX,YY: HCOLOR= 7: ROT= 0: GOSUB 200: RETURN
Update and print fatigue - movement and combat actions have a cost, which is passed into this routine as M and scaled by the constant 1/MM (4 ). This is then scaled according to how healthy the player is ( PC/PH - current hit points over maximum ) and how encumbered they are ( WC/WT - weight carried relative to encumberance threshold ). Finally 11% is added to this as stamina/fatigue recovered.
Moving one 1 foot cost has a cost of 1; from this formula a fully fit, lightly burdened character with average constitution/PH ( i.e. 10 ) can move 9 feet a turn without losing stamina.
650 PH = PEEK (KB + 24):TA = TA - ( ABS (M) / MM * (100 / PH + 5 - 5 * PC / PH) * (1 + WC / WT * 3) / 2) + 11: IF TA > 100 THEN TA = 100
660 QX = 249:QY = 16: GOSUB 15000:Q$ = STR$ ( INT (TA)) + "%": GOSUB 75: RETURN
Draw a discovered trap, flashing a square on and off a few times at the trap location ( YP, XP ) in the current room (KR).
670 Y = YB - PEEK (YP + KR) - W2:X = PEEK (XP + KR) + V1 - XB: FOR L = 1 TO 15:I = 2 * X:IK = Y: GOSUB 400: GOSUB 410: NEXT L: RETURN
Draw any treasure in the current room.
679 IF PEEK (TR + KR) = 0 THEN RETURN
680 I = 2 * (V1 + PEEK (XR + KR) - XB):YY = YB - W2 - PEEK (YR + KR): FOR IK = YY - 1 TO YY: IF PEEK (TR + KR) > 0 THEN HCOLOR= 5: GOSUB 400: HCOLOR= 7: GOTO 690
685 GOSUB 410
690 NEXT IK: RETURN
Comments