top of page
Search
Writer's pictureTom Grove

The Dungeon

Updated: Jun 29, 2020

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 


20 views0 comments

Recent Posts

See All

Flying Shark : Object Logic

Every object has a type field ( 0x5 ) that selects a tick routine. The tick routine for a bomb power-up is shown below (link to video)....

Comments


bottom of page