Akalabeth Main Game File

This is basically a diary of my attempts to understand the BASIC code, in the order I went through it.

Over time, I may break things off into more of a reference structure.

Adventure Shop

The very last routine is a good place to start. It's the adventure shop.

First we clear the screen and print the title.

60200  HOME : PRINT "WELCOME TO THE ADVENTURE SHOP"

Next we ask the player what item they want to buy (they just press a key).

60210  PRINT "WHICH ITEM SHALT THOU BUY ";: GET Q$: IF Q$ = "Q" THEN  PRINT : PRINT "BYE": FOR Z = 1 TO 1000: NEXT : TEXT : HOME : RETURN

If "Q" is pressed, we say BYE, pause, clear the screen again and return.

Next, given what was pressed, we work out the corresponding index Z into some array and the price P.

We initially set Z to -1 so we know if we've fallen through the if statements without matching anything.

60215 Z =  - 1
60220  IF Q$ = "F" THEN  PRINT "FOOD":Z = 0:P = 1
60221  IF Q$ = "R" THEN  PRINT "RAPIER":Z = 1:P = 8
60222  IF Q$ = "A" THEN  PRINT "AXE":Z = 2:P = 5
60223  IF Q$ = "S" THEN  PRINT "SHIELD":Z = 3:P = 6
60224  IF Q$ = "B" THEN  PRINT "BOW":Z = 4:P = 3
60225  IF Q$ = "M" THEN  PRINT "AMULET":Z = 5:P = 15

If Z is still -1, we did match anything so display and message and try again:

60226  IF Z =  - 1 THEN  PRINT Q$: PRINT "I'M SORRY WE DON'T HAVE THAT.": GOTO 60210

Next we check if the player's class allows them to have a rapier or bow. Class is stored in PT$ and M corresponds to a Mage.

60227  IF Q$ = "R" AND PT$ = "M" THEN  PRINT "I'M SORRY MAGES": PRINT "CAN'T USE THAT!": GOTO 60210
60228  IF Q$ = "B" AND PT$ = "M" THEN  PRINT "I'M SORRY MAGES": PRINT "CAN'T USE THAT!": GOTO 60210

Next we check if the player can afford the price P. C(5) seems to be the amount of gold the player has.

60230  IF C(5) - P < 0 THEN  PRINT "M'LORD THOU CAN NOT AFFORD THAT ITEM.": GOTO 60210

PW seams to be an array of how many items of each type the player has. Normally buying an item increments its count by 1 but food is incremented by 10 so in that case we first increment it by 9 before the general case increments it by another one. We also subtract the price P from C(5).

60235  IF Z = 0 THEN PW(Z) = PW(Z) + 9
60236 PW(Z) = PW(Z) + 1:C(5) = C(5) - P

Finally, we update the display with the new counts:

60237  VTAB (10): HTAB (16): PRINT C(5);"  "
60240  VTAB (5 + Z): HTAB (25 -  LEN ( STR$ (PW(Z)))): PRINT PW(Z);: HTAB (1): VTAB (14): PRINT

and go back to asking if they want to buy anything else:

60250  GOTO 60210

So, in summary:

  • C(5) seems to contain the amount of gold the player has
  • PW() seems to contain the count of various items the player has

The index of PW() seems to range over:

  • 0 = FOOD
  • 1 = RAPIER
  • 2 = AXE
  • 3 = SHIELD
  • 4 = BOW
  • 5 = AMULET

On the screen:

  • (16, 10) seems to hold the amount of gold
  • (5 + z, 25 - len(PW(z))) holds PW(z)

Stats and Inventory Display

The routine above the Adventure Shop seems to be for displaying stats and inventory.

60080  TEXT : HOME : PRINT : PRINT : PRINT "     STAT'S              WEAPONS": PRINT : FOR X = 0 TO 5: PRINT C$(X);C(X); TAB 24);"0-";W$(X): NEXT : POKE 34,12: HOME : POKE 35,15

This suggests that C$() contains the name of the stats and C() the value (which would make 5 correspond to gold (see above). It also suggests W$() contains the names of the items.

The POKE 34,12 and POKE 35,15 set the top and bottom of the text window.

The TAB 24) without opening parentheses is odd and may be a bug in my de-tokenizer.

We show the key to quit:

60081  VTAB (11): HTAB (18): PRINT "Q-QUIT"

If the amount of food is more than zero, we clear the hires screen (not sure why only if the amount of food is more than zero):

60082  IF PW(0) > 0 THEN  CALL 62450

Next we display the item counts, as we saw in the shop code:

60085  FOR Z = 0 TO 5: VTAB (5 + Z): HTAB (25 -  LEN ( STR$ (PW(Z)))): PRINT PW(Z);: NEXT

Next we print the PRICE, DAMAGE, and ITEM headers:

60090  VTAB (17): HTAB (5): PRINT "PRICE";: HTAB (15): PRINT "DAMAGE";: HTAB (25): PRINT "ITEM"

The names of the items:

60100  FOR X = 0 TO 5: VTAB (19 + X): HTAB (25): PRINT W$(X): NEXT

And various hard-coded values:

60110  VTAB (19): HTAB (5): PRINT "1 FOR 10": HTAB (15): PRINT "N/A": VTAB (20): HTAB (5): PRINT "8": HTAB (15): PRINT "1-10": VTAB (21): HTAB (5): PRINT "5": HTAB (15): PRINT "1-5"
60120  VTAB (22): HTAB (5): PRINT "6": HTAB (15): PRINT "1": VTAB (23): HTAB (5): PRINT "3": HTAB (15): PRINT "1-4": VTAB (24): HTAB (5): PRINT "15": HTAB (15): PRINT "?????": HOME
60130  RETURN

So far:

  • C() seems to contain the player stats (include 5 = GOLD)
  • C$() seems to contain the names of the stats
  • PW() seems to contain the count of various items the player has
  • W$() seems to contain the names of the items

Item Name Setup

The routine at 60070 sets up the item names, then GOSUBs to the stat/inventory display and adventure shop.

60070  DIM W$(5): DATA    "FOOD","RAPIER","AXE","SHIELD","BOW AND ARROWS","MAGIC AMULET": FOR X = 0 TO 5: READ W$(X): NEXT
60075  GOSUB 60080: GOSUB 60200: RETURN

Note that 60080 and 60200 are GOSUB'd to elsewhere so this is not the only entry point to those routines.

Character Creation

The last (or actually first) of the 60000-series routines seems to be character creation and initialization.

First we ask for the player's lucky number which is put in LN and will be used to generate a random ZZ.

60000  TEXT : HOME : VTAB (5): INPUT "TYPE THY LUCKY NUMBER.....";Q$:LN =  VAL (Q$)

Next we ask for the level of play LP and repeat until the value is between 1 and 10:

60005  VTAB (7): INPUT "LEVEL OF PLAY (1-10)......";Q$:LP =  INT ( VAL (Q$))
60006  IF LP < 1 OR LP > 10 THEN 60005

We generate a random ZZ based on the player's lucky number LN.

60010 ZZ =  RND ( -  ABS (LN))

Here are our stat names. Note they are padded to length 15:

60020  DATA   "HIT POINTS.....","STRENGTH.......","DEXTERITY......","STAMINA........","WISDOM.........","GOLD..........."

Next we declare PW() (item/weapon counts), declare and initialize C$() (stats names) and declare C() (stats values):

60025  DIM PW(5)
60030  DIM C$(5): FOR X = 0 TO 5: READ C$(X): NEXT
60040  DIM C(5)

Next we declare M$(), ML% and MZ%:

60041  DIM M$(10),ML%(10,1),MZ%(10,1)

And initialize M$ with the monster names:

60042  DATA       "SKELETON","THIEF","GIANT RAT","ORC","VIPER","CARRION CRAWLER","GREMLIN","MIMIC","DAEMON","BALROG"
60043  FOR X = 1 TO 10: READ M$(X): NEXT

Next we initialize the player stats:

60050  FOR X = 0 TO 5:C(X) =  INT ( SQR ( RND (1)) * 21 + 4): NEXT X

And display them, asking the player to accept them:

60060  HOME : VTAB (8): FOR X = 0 TO 5: PRINT C$(X),C(X): NEXT : PRINT : PRINT "SHALT THOU PLAY WITH THESE QUALITIES?": HTAB (20): GET Q$: IF Q$ <  > "Y" THEN 60050

Finally, we ask if they want to be a fighter or a mage (putting M or F in PT$):

60061  VTAB (15): PRINT : PRINT "AND SHALT THOU BE A FIGHTER OR A MAGE?": HTAB (20): GET PT$

When we're done, we go to the routine at 60070 that sets up the item names and GOSUBs to the stat/inventory display and adventure shop. The RETURN in 60075 will effectively act as the RETURN from this routine.

60062  IF PT$ = "M" OR PT$ = "F" THEN 60070

Or, if the class selection was invalid, we ask again:

60063  GOTO 60061

The global variables we know so far:

  • C$() player stat/quality names (padded to 15)
  • C() player stat/quality values
  • W$() weapon/item names
  • PW() player weapon/item counts
  • M$ monster names
  • ML% don't know yet
  • MZ% don't know yet
  • PT$ player type/class (F for Fighter, M for Mage)
  • LN lucky number (used in generation of ZZ)
  • ZZ don't know yet

Stats / Qualities

  • 0 = HIT POINTS
  • 1 = STRENGTH
  • 2 = DEXTERITY
  • 3 = STAMINA
  • 4 = WISDOM
  • 5 = GOLD

Weapons/Items

  • 0 = FOOD
  • 1= RAPIER
  • 2= AXE
  • 3= SHIELD
  • 4 = BOW AND ARROWS
  • 5 = AMULET

Monsters

  • 1 = SKELETON
  • 2 = THIEF
  • 3 = GIANT RAT
  • 4 = ORC
  • 5 = VIPER
  • 6 = CARRION CRAWLER
  • 7 = GREMLIN
  • 8 = MIMIC
  • 9 = DAEMON
  • 10 = BALROG

Weapon/Items Prices and Damage

(displayed on lines 19–24 and horizontal tabs at 5 and 15)

1 FOR 10  N/A
8         1-10
5         1-5
6         1
3         1-4
15        ?????

Subroutines

There are 15 GOSUBs in Akalabeth.

They go to:

  • 100 (two times)
  • 200
  • 500 (four times)
  • 2000
  • 4000

And the three we've already seen:

  • 60000
  • 60080 (three times)
  • 60200 (two times)

There are only 8 RETURNs. They are at:

  • 190 (return for 100 although it's possible to start at 90)
  • 490 (return for 200 although line 491 should form part of this section too)
  • 590 (return for 500)
  • 2090 (return for 2000)
  • 4999 (return for 4000)
  • 60075 (return for 60000)
  • 60130 (return for 60080)
  • 60210 (60200 actually goes until 60250 but that last line is a go to back to 60210 which returns if the player quits from the shop)

Other code sections not covered by this:

  • 1000–1700
  • 3087–3089
  • 6000–6060 (player death)
  • 7000–7990 (Lord British's Castle, including quest bestowal, quest hand-in and game completion)

Lord British's Castle

This code is lines 7000–7990. We get there from line 1515.

We clear the screen and go to text mode.

7000  HOME : TEXT : HOME
7001  CALL 62450

CALL 62450 clears the hires screen to black.

If the player has a name (in PN$) they've been here before so we skip the next part.

7010  IF PN$ <  > "" THEN 7500

However, if the player is new we welcome them.

Newcomer

7020  PRINT : PRINT : PRINT "     WELCOME PEASANT INTO THE HALLS OF": PRINT "THE MIGHTY LORD BRITISH. HEREIN THOU MAYCHOOSE TO DARE BATTLE WITH THE EVIL": PRINT "CREATURES OF THE DEPTHS, FOR GREAT": PRINT "REWARD!"

And ask their name:

7030  PRINT : PRINT "WHAT IS THY NAME PEASANT ";: INPUT PN$

We next ask if they want grand adventure. If they don't answer "Y" we remove their name and GOTO 1090.

7040  PRINT "DOEST THOU WISH FOR GRAND ADVENTURE ? ";: GET Q$: IF Q$ <  > "Y" THEN  PRINT : PRINT "THEN LEAVE AND BEGONE!":PN$ = "": PRINT : PRINT "         PRESS -SPACE- TO CONT.";: GET Q$: GOTO 1090

However, if they accept, we set them a task:

7045  PRINT
7050  PRINT : PRINT "GOOD! THOU SHALT TRY TO BECOME A ": PRINT "KNIGHT!!!": PRINT : PRINT "THY FIRST TASK IS TO GO INTO THE": PRINT "DUNGEONS AND TO RETURN ONLY AFTER": PRINT "KILLING A(N) ";:TASK =  INT (C(4) / 3): PRINT M$(TASK)

TASK is initially calculated from the player's WISDOM stat and indicates the monster to kill (equivalent to the index into M$ which gives us the monster's name).

At quest bestowal, the player gains 1 point on all stats (including GOLD). Again we exit with a GOTO 1090.

7060  PRINT : PRINT "     GO NOW UPON THIS QUEST, AND MAY": PRINT "LADY LUCK BE FAIR UNTO YOU.....": PRINT ".....ALSO I, BRITISH, HAVE INCREASED": PRINT "EACH OF THY ATTRIBUTES BY ONE!"

7070  PRINT : PRINT "         PRESS -SPACE- TO CONT.";: GET Q$: FOR X = 0 TO 5:C(X) = C(X) + 1: NEXT : HOME : GOTO 1090

Return

First we check if TASK is still positive (indicating a quest was assigned but not completed).

If so, we remind the player of their task and send them back with a GOTO 1090.

7500  IF TASK > 0 THEN  PRINT : PRINT : PRINT PN$;" WHY HAST THOU RETURNED?": PRINT "THOU MUST KILL A(N) ";M$(TASK): PRINT "GO NOW AND COMPLETE THY QUEST!": PRINT : PRINT "         PRESS -SPACE- TO CONT.";: GET Q$: HOME : GOTO 1090

If ABS(TASK) = 10 then, at this point, TASK must be -10. If this is the case, the player has become a knight.

7510  PRINT : PRINT : PRINT : PRINT "AAHH!!.....";PN$: PRINT : PRINT "THOU HAST ACOMPLISHED THY QUEST!": IF  ABS (TASK) = 10 THEN 7900

If the player hasn't done enough to become a knight, we give them a new quest (with TASK += 1).

We then GOTO 7060 (the last part of the initial quest bestowal) where stats are increased and we leave the castle (GOTO 1090):

7520  PRINT "UNFORTUNATELY, THIS IS NOT ENOUGH TO": PRINT "BECOME A KNIGHT.":TASK =  ABS (TASK) + 1: PRINT : PRINT "NOW THOU MUST KILL A(N) ";M$(TASK)
7530  GOTO 7060

We can see from the above that a positive TASK means "kill monster M$(TASK)" whereas a negative TASK means, "player has successfully killed monster M$(ABS(TASK))".

Knighthood

Now, if the player has completed TASK 10 (the BALROG), they become a knight.

7900  TEXT : HOME : PRINT : PRINT : PRINT :PN$ = "LORD " + PN$: PRINT "     ";PN$;","
7910  PRINT "       THOU HAST PROVED THYSELF WORTHY": PRINT "OF KNIGHTHOOD, CONTINUE PLAY IF THOU": PRINT "DOTH WISH, BUT THOU HAST ACOMPLISHED": PRINT "THE MAIN OBJECTIVE OF THIS GAME..."

We skip if the level being played at was 10.

7920  IF LP = 10 THEN 7950

If the level wasn't 10, we suggest them trying at one higher level than they just played.

7930  PRINT : PRINT "   NOW MAYBE THOU ART FOOLHEARTY": PRINT "ENOUGH TO TRY DIFFICULTY LEVEL ";LP + 1

We exit with a GOTO 7070 which still ups the stats but doesn't show the message saying that's what's being done.

7940  GOTO 7070

However, if the BALROG is killed on level 10, we reach here, with a suggestion to call California Pacific Computer to report the accomplishment.

7950  PRINT : PRINT "...CALL CALIFORNIA PACIFIC COMPUTER": PRINT "AT (415)-569-9126 TO REPORT THIS": PRINT "AMAZING FEAT!"

We still bump the stats on exit from the castle (although again, without mentioning it).

7990  GOTO 7070

So three new global variables are now known:

  • PN$ player name
  • LP difficulty level being played at
  • TASK if positive, the monster the player is on a quest to kill, if negative then ABS(TASK) gives the quest monster just killed

Player Death

Player death is dealt with in lines 6000–6060. 6000 is called from line 1093 if HIT POINTS (C(0)) hits or drops below 0.

First we set the window width to 40 then print the death announcement.

6000  POKE 33,40: PRINT : PRINT : PRINT "        WE MOURN THE PASSING OF"

If the player name is longer than 22 characters or there isn't a player name, we just call the player "THE PEASANT":

6005  IF  LEN (PN$) > 22 THEN PN$ = ""
6010  IF PN$ = "" THEN PN$ = "THE PEASANT"

We then add " AND HIS COMPUTER" to the name.

6020 PN$ = PN$ + " AND HIS COMPUTER"

We centre the name:

6030  HTAB (20 -  INT ( LEN (PN$) / 2)): PRINT PN$

And offer a resurrection via the ESC key:

6035  PRINT "  TO INVOKE A MIRACLE OF RESSURECTION"
6040  PRINT "             <HIT ESC KEY>";

We loop until we get the ESC key:

6050  IF  PEEK ( - 16384) = 155 THEN 1
6060  GOTO 6050

On ESC we go back to the start of the program on line 1.

So far, I've noticed the following typos:

  • ACOMPLISHED (line 7510)
  • RESSURECTION (line 6035)

Commands

The command loop and command handling is done in lines 1000–1700.

First we go to the bottom of the screen and print the prompt. CALL -868 clears the line to the right of the cursor:

1000  VTAB (24): PRINT "COMMAND? ";: CALL  - 868

Then we loop until we get a key press:

1001 X =  PEEK ( - 16384): IF X < 128 THEN 1001

We see how much memory is available (not yet sure why):

1002 Q =  FRE (0)

and reset the keyboard:

1010  POKE  - 16368,0

Command Dispatch

Many commands are handled differently depending on whether we're outside or inside.

These commands are:

  • CR (141) NORTH 1100 or FORWARD 1150
  • RIGHT ARROW (149) EAST 1200 or TURN RIGHT 1250
  • LEFT ARROW (136) WEST 1300 or TURN LEFT 1350
  • / (175) SOUTH 1400 or TURN AROUND 1450
  • X (216) GO 1500 or 1550
  • A or 27 (193 or 155) ATTACK 1600 or 1650

SGN(INOUT) will be 0 if we're outside and 1 if we're inside.

1030  IF X = 141 THEN  ON  SGN (INOUT) + 1 GOTO 1100,1150
1040  IF X = 149 THEN  ON  SGN (INOUT) + 1 GOTO 1200,1250
1050  IF X = 136 THEN  ON  SGN (INOUT) + 1 GOTO 1300,1350
1060  IF X = 175 THEN  ON  SGN (INOUT) + 1 GOTO 1400,1450
1070  IF X = 216 THEN  ON  SGN (INOUT) + 1 GOTO 1500,1550
1080  IF X = 193 OR X = 155 THEN  ON  SGN (INOUT) + 1 GOTO 1600,1650

SPACE passes:

1081  IF X = 160 THEN  PRINT "PASS": GOTO 1090

S shows stats/inventory:

1085  IF X = 211 THEN 1700

P toggles PAUSE ON/OFF which we store as PA (1 for ON, 0 for OFF):

1086  IF X = 208 THEN  IF PA = 1 THEN PA = 0: PRINT "PAUSE OFF": GOTO 1000
1087  IF X = 208 THEN  IF PA = 0 THEN PA = 1: PRINT "PAUSE ON": GOTO 1000

Note that PAUSE ON/OFF doesn't call the Post-Command code (so doesn't reduce food, etc)

Any other keys and we print "HUH?" and go back to the start of this routine:

1089  PRINT "HUH?": GOTO 1000

Post-Command

Once the command is executed we normally GOTO line 1090.

First we reduce food by 1 (or 0.1 if we're inside). If FOOD hits 0 then we set HIT POINTS to 0 too, announce the player has starved and go to line 1093 (which in turn will send us to the Death routine at 6000)

1090 PW(0) = PW(0) - 1 +  SGN (INOUT) * .9: IF PW(0) < 0 THEN C(0) = 0: PRINT : PRINT "YOU HAVE STARVED!!!!!": GOTO 1093

If we haven't starved, though, we update our FOOD / HIT POINTS / GOLD display:

1091  POKE 33,40: VTAB (22): HTAB (30): PRINT "FOOD=";PW(0);: CALL  - 868: VTAB (23): HTAB (30): PRINT "H.P.=";C(0);: CALL  - 868: VTAB (24): HTAB (30): PRINT "GOLD=";C(5);: CALL  - 868: POKE 33,29: HTAB (1)

We round our FOOD to one decimal place.

1092 PW(0) =  INT (PW(0) * 10) / 10

If HIT POINTS are zero or less, we go to the Death handling routine:

1093  IF C(0) <  = 0 THEN 6000

If we're in a dungeon (IN is same as INOUT) we move the monsters around. If this has resulted in HIT POINTS being zero or less, we go back to the previous line to go to the Death handling routine.

1095  IF IN > 0 THEN  GOSUB 4000: IF C(0) <  = 0 THEN 1093

The following is the same as line 1091. I guess we do it again because the monster movement in the previous line may have changed some values.

1096  POKE 33,40: VTAB (22): HTAB (30): PRINT "FOOD=";PW(0);: CALL  - 868: VTAB (23): HTAB (30): PRINT "H.P.=";C(0);: CALL  - 868: VTAB (24): HTAB (30): PRINT "GOLD=";C(5);: CALL  - 868: POKE 33,29: HTAB (1)

Not yet sure what GOSUB 100 or GOSUB 200 do. Perhaps draw.

1097  IF INOUT = 0 THEN  GOSUB 100: GOTO 1000
1098  IF INOUT > 0 THEN  GOSUB 200: GOTO 1000

NORTH (Outside)

If there's not a mountain in the way, move north.

1100  PRINT "NORTH": IF TER%(TX,TY - 1) = 1 THEN  PRINT "YOU CAN'T PASS THE MOUNTAINS": GOTO 1090
1110 TY = TY - 1: GOTO 1090

FORWARD (Inside)

If it's possible to move forward, make the move:

1150  IF DNG%(PX + DX,PY + DY) <  > 1 AND DNG%(PX + DX,PY + DY) < 10 THEN PX = PX + DX:PY = PY + DY
1155  PRINT "FORWARD"

First we check if the location the player moved to has a TRAP. If it does, the player loses a random amount of HIT POINTS based on dungeon level, MR is set to 1 (don't know what that is yet), the dungeon level is incremented and we GOSUB to 500 to generate a new dudgeon map. Note that INOUT and IN are the same variable.

1160  IF DNG%(PX,PY) = 2 THEN  PRINT "AAARRRGGGHHH!!! A TRAP!":C(0) = C(0) -  INT ( RND (1) * INOUT + 3):MR = 1:INOUT = INOUT + 1: PRINT "FALLING TO LEVEL ";IN: GOSUB 500: GOTO 1090

If there's a chest where the player is located, we take the chest, randomly give an amount of GOLD based on dungeon level as well as an item.

1165 Z = 0
1170  IF DNG%(PX,PY) = 5 THEN DNG%(PX,PY) = 0: PRINT "GOLD!!!!!":Z =  INT ( RND (1) * 5 * INOUT + INOUT): PRINT Z;"-PIECES OF EIGHT":C(5) = C(5) + Z
1175  IF Z > 0 THEN Z =  INT ( RND (1) * 6): PRINT "AND A ";W$(Z):PW(Z) = PW(Z) + 1: GOTO 1090
1190  GOTO 1090

EAST (Outside)

Moving EAST is the same as moving NORTH but we increment TX instead of decrementing TY.

1200  PRINT "EAST": IF TER%(TX + 1,TY) = 1 THEN  PRINT "YOU CAN'T PASS THE MOUNTAINS": GOTO 1090
1210 TX = TX + 1: GOTO 1090

TURN RIGHT (Inside)

Rotate the player:

1250  PRINT "TURN RIGHT"
1255  IF DX <  > 0 THEN DY = DX:DX = 0: GOTO 1090
1260 DX =  - DY:DY = 0: GOTO 1090

WEST (Outside)

Moving WEST is the same as moving EAST but we decrement TX instead of incrementing it.

1300  PRINT "WEST": IF TER%(TX - 1,TY) = 1 THEN  PRINT "YOU CAN'T PASS THE MOUNTAINS": GOTO 1090
1310 TX = TX - 1: GOTO 1090

TURN LEFT (Inside)

Rotate the player:

1350  PRINT "TURN LEFT"
1355  IF DX <  > 0 THEN DY =  - DX:DX = 0: GOTO 1090
1360 DX = DY:DY = 0: GOTO 1090

SOUTH (Outside)

Moving SOUTH is the same as moving NORTH but we increment TY instead of decrementing it.

1400  PRINT "SOUTH": IF TER%(TX,TY + 1) = 1 THEN  PRINT "YOU CAN'T PASS THE MOUNTAINS": GOTO 1090
1410 TY = TY + 1: GOTO 1090

TURN AROUND (Inside)

Flip the direction of the player:

1450  PRINT "TURN AROUND":DX =  - DX:DY =  - DY: GOTO 1090

GO (Outside)

If it's a town, enter the shop:

1500  IF TE%(TX,TY) = 3 THEN  GOSUB 60080: GOSUB 60200: GOTO 1090

If it's a dungeon and INOUT is 0 (not sure when it wouldn't be at this point), enter the dungeon. Set INOUT to 1, randomize the dungeon map (GOSUB 500) then initialize the player location and direction.

1510  IF TE%(TX,TY) = 4 AND INOUT = 0 THEN  PRINT "GO DUNGEON": PRINT "PLEASE WAIT ":INOUT = 1: GOSUB 500:DX = 1:DY = 0:PX = 1:PY = 1: GOTO 1090

If it's Lord British's castle, enter:

1515  IF TE%(TX,TY) = 5 THEN 7000

Otherwise print "HUH?" and loop back (although don't do post-command):

1520  PRINT "HUH?": GOTO 1000

GO (Inside)

Skip ahead if it's not a ladder down (not yet sure distinction between 7 and 9):

1550  IF DNG%(PX,PY) <  > 7 AND DNG%(PX,PY) <  > 9 THEN 1580

Otherwise say we're going down a level, increment INOUT and randomize the next level (GOSUB 500):

1555  PRINT "GO DOWN TO LEVEL ";INOUT + 1
1560 INOUT = INOUT + 1: GOSUB 500:MR = 1: GOTO 1090

So here's where we go if it's not a down ladder (7 or 9). If it's also not an up ladder (8), print "HUH?"

1580  IF DNG%(PX,PY) <  > 8 THEN  PRINT "HUH?": GOTO 1090

But if it's an up ladder...

If the player was already at the first level, print that they're leaving and change IN (INOUT) to 0:

1581  IF IN = 1 THEN  PRINT "LEAVE DUNGEON":IN = 0: GOTO 1586

Otherwise say they're going up a level, decrement INOUT and randomize the dungeon level (GOSUB 500). Also set MR to 1 (not yet sure what that's for).

1584  PRINT "GO UP TO LEVEL ";INOUT - 1
1585 INOUT = INOUT - 1: GOSUB 500:MR = 1

If we've left the dungeon, increment the HIT POINTS (C(0)) by LK (which is incremented on monster kill below).

1586  IF IN = 0 THEN  PRINT "THOU HAST GAINED": PRINT LK;" HIT POINTS":C(0) = C(0) + LK:LK = 0
1587  GOTO 1090

ATTACK (Outside)

Attack is a no-op outside (although we run the post-command to reduce food, etc)

1600  GOTO 1090

ATTACK (Inside)

See separate analysis below.

STATISTICS / INVENTORY

Call the stats/inventory routine then prompt to hit CR to go back.

1700  GOSUB 60080: HOME : PRINT "PRESS -CR- TO CONTINUE";: INPUT Q$: TEXT : HOME : GOTO 1090

What we learnt from the command handling routines above:

  • TER%() contains the terrain type at an outside location
  • DNG%() contains what is at a particular location in a dungeon
  • TX and TY are the player's location outside
  • PX and PY are the player's location inside
  • DX and DY are how PX and PY will change if the player steps forward (i.e. they give the direction the player is facing in the dungeon)

Values of TER%() are:

  • 1 = MOUNTAINS
  • 3 = TOWN (SHOP)
  • 4 = DUNGEON
  • 5 = LORD BRITISH'S CASTLE

Values of DNG%() are:

  • 1 = WALL?
  • 2 = TRAP
  • 5 = CHEST
  • 7 or 9 = DOWN LADDER
  • 8 = UP LADDER

Attack

First we initialize MN (the monster being attacked) and DAM (the damage being done) to 0. Then we ask what weapon the player wants to use:

1650 MN = 0:DAM = 0: PRINT "ATTACK": PRINT "WHICH WEAPON ";: GET Q$

Then, depending on what they select, we set the appropriate damage DAM. But if the player does not own the item, say so and go back to asking which weapon:

1651  IF Q$ = "R" THEN DAM = 10: PRINT "RAPIER": IF PW(1) < 1 THEN  PRINT "NOT OWNED": GOTO 1650
1652  IF Q$ = "A" THEN DAM = 5: PRINT "AXE": IF PW(2) < 1 THEN  PRINT "NOT OWNED": GOTO 1650
1653  IF Q$ = "S" THEN DAM = 1: PRINT "SHIELD": IF PW(3) < 1 THEN  PRINT "NOT OWNED": GOTO 1650
1654  IF Q$ = "B" THEN DAM = 4: PRINT "BOW": IF PW(4) < 1 THEN  PRINT "NOT OWNED": GOTO 1650

If MAGIC AMULET has been selected, skip down to line 1680:

1655  IF Q$ = "M" THEN  PRINT "MAGIC AMULET": GOTO 1680

Check player class ($PT) and, if player is a mage and they picked a BOW or RAPIER, tell them they can't use it and go back to asking which weapon again:

1656  IF Q$ = "B" AND PT$ = "M" THEN  PRINT "MAGES CAN'T USE BOWS!": GOTO 1650
1657  IF Q$ = "R" AND PT$ = "M" THEN  PRINT "MAGES CAN'T USE RAPIERS!": GOTO 1650

If none of the weapons matched, DAM will still be 0 and we'll assume HANDS.

1659  IF DAM = 0 THEN  PRINT "HANDS"

If it's an AXE or BOW, we skip ahead to line 1670:

1660  IF DAM = 5 OR DAM = 4 THEN 1670

Melee Weapon

This next line is unique to Melee Weapons. We work out which monster type is in the direction the player is facing. (Looks like DN%() might contain 10 x the monster level/type at that location).

1661 MN = DN%(PX + DX,PY + DY) / 10:MN =  INT (MN)

Common Attack Code

But from this point on, the code is shared with Ranged weapons too...

If there are no monsters, MN will be < 1 (not sure if it will ever be negative). The hit chance is based on DEXTERITY, the monster level and the dungeon level.

1662  IF MN < 1 OR C(2) -  RND (1) * 25 < MN + INOUT THEN  PRINT "YOU MISSED": GOTO 1668

If we hit, we work out actual damage (based on DAM and STRENGTH) and put it back in DAM and reduce the HIT POINTS accordingly. MZ%(MN,1) must contain the HIT POINTS of the monster of type MN.

1663  PRINT "HIT!!! ":DAM = ( RND (1) * DAM + C(1) / 5):MZ%(MN,1) = MZ%(MN,1) - DAM

We display the monster's new HIT POINTS:

1664  PRINT M$(MN);"'S HIT POINTS=";MZ%(MN,1)

If that has dropped to 0 or less, it's a kill. The reward is an amount of gold equal to the monster level + dungeon level.

1665  IF MZ%(MN,1) < 1 THEN  PRINT "THOU HAST KILLED A ";M$(MN): PRINT "THOU SHALT RECEIVE":DA =  INT (MN + IN): PRINT DA;" PIECES OF EIGHT"

We add the gold to the player inventory. We then remove the monster from the DNG% map and set MZ%(MN,0) to 0. ML%(MN,0)must contain the X-cordinate of the monster and ML%(MN,1) the Y-coordinate.

1666  IF MZ%(MN,1) < 1 THEN C(5) =  INT (C(5) + DA):DNG%(ML%(MN,0),ML%(MN,1)) = DNG%(ML%(MN,0),ML%(MN,1)) - 10 * MN:MZ%(MN,0) = 0

LK is the number of HIT POINTS the player will gain when they leave the dungeon. We increment LK by half the monster level MN times the dungeon level IN. If the monster was the quest monster, we mark the quest done (by negating TASK).

1667 LK = LK +  INT (MN * IN / 2): IF MN = TASK THEN TASK =  - TASK

Regardless of hit or miss, we end up here...

If PAUSE is ON, we wait for a CR to continue:

1668  IF PA = 1 THEN  PRINT "-CR- TO CONT. ";: INPUT Q$

And we're done.

1669  GOTO 1090

Axe or Bow

If it's an AXE, we give the player the option to THROW (ranged) or SWING (melee). If they SWING, we just go back to the Melee Weapon section above.

1670  IF DAM = 5 THEN  PRINT "TO THROW OR SWING:";: GET Q$: IF Q$ <  > "T" THEN  PRINT "SWING": GOTO 1661

If it's an AXE (and we've already established at this point we're throwing it) we reduce the count by one:

1671  IF DAM = 5 THEN  PRINT "THROW":PW(2) = PW(2) - 1

Ranged Attack

We iterate over the 5 squares in the direction the player is facing. If we go off the map (a coordinate hit 0 or 10), we go back to Common Attack Code with MN still 0.

If DNG%(x, y) has a monster, it will contain monster level * 10.

1672  FOR Y = 1 TO 5: IF PX + DX * Y < 1 OR PX + DX * Y > 9 OR PY + DY * Y > 9 OR PY + DY * Y < 0 THEN 1662
1673 MN = DNG%(PX + DX * Y,PY + DY * Y):MN =  INT (MN / 10): IF MN > 0 THEN 1662
1674  NEXT : GOTO 1662

Magic Amulet

If there's no magic amulet in the player's possession, we go back to asking them which weapon to use.

1680  IF PW(5) < 1 THEN  PRINT "NONE OWNED": GOTO 1650

If the player is a fighter, the amulet's action is random:

1681  IF PT$ = "F" THEN Q =  INT ( RND (1) * 4 + 1): GOTO 1685

If the player is a mage, they get to pick the amulet's action:

1682  PRINT "1-LADDER-UP","2-LADDER-DN": PRINT "3-KILL","4-BAD??": PRINT "CHOICE ";: GET Q$:Q =  VAL (Q$): PRINT Q: IF Q < 1 OR Q > 4 THEN 1682

But in that case, there's a 25% chance the amulet will be on its last charge (and will be removed from the inventory).

1683  IF  RND (1) > .75 THEN  PRINT "LAST CHARGE ON THIS AMULET!":PW(5) = PW(5) - 1

Regardless of fighter or mage, we end up here.

1685  ON Q GOTO 1686,1690,1691,1692

Q = 1. Make an up-ladder:

1686  PRINT "LADDER UP":DNG%(PX,PY) = 8: GOTO 1090

Q = 2. Make a down-ladder:

1690  PRINT "LADDER DOWN":DNG%(PX,PY) = 7: GOTO 1090

Q = 3. A magic attack. Set the damage to 10 + dungeon level and go to Ranged Attack.

1691  PRINT "MAGIC ATTACK":DAM = 10 + INOUT: GOTO 1672

Q = 4. Random badness.

1692  ON  INT ( RND (1) * 3 + 1) GOTO 1693,1695,1697

a) get turned into a TOAD and have STRENGTH, DEXTERITY, STAMINA and WISDOM all turned to 3.

1693  PRINT "YOU HAVE BEEN TURNED": PRINT "INTO A TOAD!"
1694  FOR Z2 = 1 TO 4:C(Z2) = 3: NEXT Z2: GOTO 1090

b) get turned into a LIZARD MAN and have HIT POINTS, STRENGTH, DEXTERITY, STAMINA and WISDOM all multiplied by 2.5:

1695  PRINT "YOU HAVE BEEN TURNED": PRINT "INTO A LIZARD MAN": FOR Y = 0 TO 4:C(Y) =  INT (C(Y) * 2.5): NEXT : GOTO 1090

c) backfire and halve HIT POINTS

1697  PRINT "BACKFIRE":C(0) = C(0) / 2: GOTO 1090

More global variables, we've learnt about in the attack code:

  • DNG%() (or DN%()) stores the monster type/level in the tens column. So 37 would mean a GIANT RAT (3) AND a DOWN LADDER (7).
  • MZ%(MN,0) unsure but is set to 0 when monster killed.
  • MZ%(MN,1) stores the HIT POINTS of the monster of type MN at this dungeon level
  • ML%(MN,0), ML%(MN,1) location of monster of type MN at this dungeon level
  • LK hit points to gain on leaving dungeon

Generating a Dungeon Level

Dungeon level generation is a routine from lines 500–590 (with a call out to 2000).

The dungeon level information is primarily stored in DNG%() (sometimes referred to as just DN%()) which is an 11 x 11 matrix.

We generate a ZZ but I still don't know what that's for.

500 ZZ =  RND ( -  ABS (LN) - TX * 10 - TY * 1000 + INOUT * 31.4)

Next we zero out the inside of DNG%():

501  FOR X = 1 TO 9: FOR Y = 1 TO 9:DNG%(X,Y) = 0: NEXT : NEXT

Then we set the outsides to WALLS (1):

510  FOR X = 0 TO 10:DNG%(X,0) = 1:DNG%(X,10) = 1:DNG%(0,X) = 1:DNG%(10,X) = 1: NEXT

Then we draw rows of walls on the even coordinates.

520  FOR X = 2 TO 8 STEP 2: FOR Y = 1 TO 9:DNG%(X,Y) = 1:DNG(Y,X) = 1: NEXT : NEXT

The result at this point is:

11111111111
10101010101
11111111111
10101010101
11111111111
10101010101
11111111111
10101010101
11111111111
10101010101
11111111111

Then, for each element of those rows of inside walls, we randomly replace the wall with a TRAP (2), ? (3), ? (4), CHEST (5) or DOWN LADDER? (9).

530  FOR X = 2 TO 8 STEP 2: FOR Y = 1 TO 9 STEP 2
540  IF  RND (1) > .95 THEN DNG%(X,Y) = 2
541  IF  RND (1) > .95 THEN DNG%(Y,X) = 2
542  IF  RND (1) > .6 THEN DNG%(Y,X) = 3
543  IF  RND (1) > .6 THEN DNG%(X,Y) = 3
544  IF  RND (1) > .6 THEN DNG%(X,Y) = 4
545  IF  RND (1) > .6 THEN DNG%(Y,X) = 4
546  IF  RND (1) > .97 THEN DNG%(Y,X) = 9
547  IF  RND (1) > .97 THEN DNG%(X,Y) = 9
548  IF  RND (1) > .94 THEN DNG%(X,Y) = 5
549  IF  RND (1) > .94 THEN DNG%(Y,X) = 5
568  NEXT : NEXT

(2,1) is always cleared. If the player is on an even level, (7,3) is a DOWN LADDER and (3, 7) is an UP LADDER. If the player is on an odd level, (7,3) is an UP LADDER and (3,7) is a DOWN LADDER (of course).

569 DNG%(2,1) = 0: IF INOUT / 2 =  INT (INOUT / 2) THEN DNG%(7,3) = 7:DNG%(3,7) = 8
570  IF INOUT / 2 <  >  INT (INOUT / 2) THEN DNG%(7,3) = 8:DNG%(3,7) = 7

If the player is on the first level of the dungeon (closest to surface) then (1,1) is an UP LADDER instead of (7,3). Presumably this is where the player starts.

580  IF INOUT = 1 THEN DNG%(1,1) = 8:DNG%(7,3) = 0

We call out to 2000 to place monsters.

585  GOSUB 2000

And we're done.

590  RETURN

Monster Placement

We initialize NM (the number of monsters created for this dungeon level) to 0 and loop 10 times.

2000 NM = 0: FOR X = 1 TO 10

X, our loop variable will be the monster level/type.

We set MZ%(X,0) for the monster to 0 and the number of HIT POINTS it has (MZ%(X,1)) to 3 + monster level + dungeon level (although oddly we override this once we actually place the monster below).

2005 MZ%(X,0) = 0:MZ%(X,1) = X + 3 + INOUT

If the monster is more than two levels higher than the dungeon level, we skip it and continue the loop. Even if the monster in consideration is an okay level, there's still only a 40% chance we have one.

2010  IF X - 2 > INO OR  RND (1) > .4 THEN 2090

Next we pick a location for the monster.

2020 ML%(X,0) =  INT ( RND (1) * 9 + 1):ML%(X,1) =  INT ( RND (1) * 9 + 1)

If it's already occupied (not just by a monster but anything, like a wall) we go back and pick a new location:

2030  IF DNG%(ML%(X,0),ML%(X,1)) <  > 0 THEN 2020

If it's the same location as the player, we go back and pick a new location:

2040  IF ML%(X,0) = PX AND ML%(X,1) = PY THEN 2020

Now that we've established we definitely have a monster, we place it in DNG%() (by putting the monster type/level in the tens column):

2050 DNG%(ML%(X,0),ML%(X,1)) = X * 10

We set MZ%(X,0) to 1 for the monster and increment our monster count for the dungeon level.

2051 MZ%(X,0) = 1
2052 NM = NM + 1

Finally we set the HIT POINTS to be 2 * (monster level + dungeon level * player difficulty level).

2055 MZ%(X,1) = X * 2 + IN * 2 * LP

And we loop back for the next monster type before returning.

2090  NEXT : RETURN

Monster Movement

We iterate through the 10 types of monsters and check if any of them exist at this level of the dungeon. If not, we skip this entire routine.

4000  FOR MM = 1 TO 10: IF MZ%(MM,0) = 0 THEN 4999

So, there's one at level MM. It's location is currently given by ML%(MM,0) and ML%(MM,1).

First we calculate the distant between the monster and the player:

4010 RA =  SQR ((PX - ML%(MM,0)) ; 2 + (PY - ML%(MM,1)) ; 2)

(is ; 2 really how you square? Is that a de-tokenization bug?)

If the HIT POINTS is less than the dungeon level * the player difficulty level, skip to 4030:

4011  IF MZ%(MM,1) < IN * LP THEN 4030

If the distance is < 1.3 (i.e. it's adjacent), then skip to Monster Attack:

4020  IF RA < 1.3 THEN 4500

If it's a MIMIC within a distance of 3, it won't do anything and skip to the next monster.

4025  IF MM = 8 AND RA < 3 THEN 4999

Calculate the direction the player is in from the monster:

4030 X1 =  SGN (PX - ML%(MM,0)):Y1 =  SGN (PY - ML%(MM,1))

If the HIT POINTS is less than the dungeon level * the player difficulty level, flip the directions (i.e. plan to move away from the player):

4031  IF MZ%(MM,1) < IN * LP THEN X1 =  - X1:Y1 =  - Y1

If the the player is due east or west (i.e. the change in Y is 0), skip the next part.

4035  IF Y1 = 0 THEN 4045

(the player isn't due east or west so we're going to need to move north or south) Look at what's in the north/south direction of desired travel. If it's a WALL (1) or another monster or TRAP (2), skip to 4045.

4040 D = DNG%(ML%(MM,0),(ML%(MM,1) + Y1 + .5)): IF D = 1 OR D > 9 OR D = 2 THEN 4045

But if the monster can move in that north/south direction, set X1 to 0 (as it won't be moving east/west this turn).

4042 X1 = 0: GOTO 4050

We get here if the player is due east/west or if something it blocking the monster from going north/south. As the monster won't be moving north/south this turn, we set Y1 to 0. If the player is still due north/south, though, we skip the next line and go to 4050.

4045 Y1 = 0: IF X1 = 0 THEN 4050

The monster desires to go east/west. It looks to see what's in the desired direction. If it's a WALL (1) or another monster or a TRAP (2), then set X1 to 0 (as it can't move in that direction) and go to 4081.

4046 D = DN%((ML%(MM,0) + X1 + .5),ML%(MM,1)): IF D = 1 OR D > 9 OR D = 2 THEN X1 = 0: GOTO 4081

We remove the monster from its existing place in DNG%():

4050 DNG%(ML%(MM,0),ML%(MM,1)) = DNG%(ML%(MM,0),ML%(MM,1)) - 10 * MM

If the move would place the monster on the place as the player, we skip the rest and go to the next monster (note they would leave it removed it from DNG%(), though):

4055  IF ML%(MM,0) + X1 = PX AND ML%(MM,1) + Y1 = PY THEN 4999

Otherwise, we move the monster:

4060 ML%(MM,0) = ML%(MM,0) + X1:ML%(MM,1) = ML%(MM,1) + Y1

and update the DNG%() to include it at its new location:

4080 DNG%(ML%(MM,0),ML%(MM,1)) = (DNG%(ML%(MM,0),ML%(MM,1)) + 10 * MM + .5)

(not sure what the + .5 here and above is for).

If we get here and there was a movement, we skip to the next monster:

4081  IF X1 <  > 0 OR Y1 <  > 0 THEN 4999

If the HIT POINTS is less than the dungeon level * player difficulty level and the player is adjacent to the monster, go to Monster Attack:

4082  IF MZ%(MM,1) < IN * LP AND RA < 1.3 THEN 4500

Otherwise, if the HIT POINTS is less than the dungeon level * player difficulty level, the HIT POINTS rise by monster level + dungeon level (healing):

4083  IF MZ%(MM,1) < IN * LP THEN MZ%(MM,1) = MZ%(MM,1) + MM + IN

And we move to the next monster:

4499  GOTO 4999

Monster Attack

If the monster is a THIEF or a GREMLIN, it steals rather than attacks to we skip to Monster Theft:

4500  IF MM = 2 OR MM = 7 THEN 4600

We tell the player what they are being attacked by:

4509  PRINT "YOU ARE BEING ATTACKED": PRINT "BY A ";M$(MM)

We determine whether they hit based on whether the player has a shield, the player stamina, the monster level and the dungeon level:

4510  IF  RND (1) * 20 -  SGN (PW(3)) - C(3) + MM + IN < 0 THEN  PRINT "MISSED": GOTO 4525

If it's a hit, the HIT POINTS lost is base on monster level + dungeon level:

4520  PRINT "HIT":C(0) = C(0) -  INT ( RND (1) * MM + IN)

If PAUSE is ON we wait for the player to hit CR:

4525  IF PA = 1 THEN  PRINT "-CR- TO CONT. ";: INPUT Q$

Then go to the next monster to move:

4530  GOTO 4999

Monster Theft

There is a 50% chance the thief or gremlin will just attack anyway:

4600  IF  RND (1) < .5 THEN 4509

But if they steal...

If they are a gremlin, half the food will be gone:

4610  IF MM = 7 THEN PW(0) =  INT (PW(0) / 2): PRINT "A GREMLIN STOLE SOME FOOD": GOTO 4525

Otherwise (i.e. they are a thief), one of the possessions the player has will be stolen at random:

4620 ZZ =  INT ( RND (1) * 6): IF PW(ZZ) < 1 THEN 4620
4630  PRINT "A THIEF STOLE A ";W$(ZZ):PW(ZZ) = PW(ZZ) - 1: GOTO 4525

And finally we go to the next monster then return:

4999  NEXT : RETURN

Drawing Terrain

The terrain drawing routine appears to be lines 100–190.

As we've already seen, the terrain is stored in TER%() and we know from the outside movement commands that TX and TY store the location of the player.

We further more know that the values of TER%() are:

  • 1 = MOUNTAINS
  • 3 = TOWN (SHOP)
  • 4 = DUNGEON
  • 5 = LORD BRITISH'S CASTLE

I presume therefore that 2 must be the little squares (never knew what they were supposed to be).

We switch to hires graphics mode and loop over [-1, 0, 1] x [-1, 0, 1] to draw the terrain tile the player is on and the eight surrounding.

100  HGR : FOR Y =  - 1 TO 1: FOR X =  - 1 TO 1

We next draw the crosshairs indicated the player's location:

105 HPLOT 138,75 TO 142,75: HPLOT 140,73 TO 140,77

We retrieve the terrain type and put it in ZZ then work out the top left corner of the tile to draw. The tiles are 50 x 50 and start at (65, 0).

110 ZZ = TER%(TX + X,TY + Y):X1 = 65 + (X + 1) * 50:Y1 = (Y + 1) * 50

"Little Square" (2)

120  IF ZZ = 2 THEN  HPLOT X1 + 20,Y1 + 20 TO X1 + 30,Y1 + 20 TO X1 + 30,Y1 + 30 TO X1 + 20,Y1 + 30 TO X1 + 20,Y1 + 20

Town/Shop (3)

130  IF ZZ = 3 THEN  HPLOT X1 + 10,Y1 + 10 TO X1 + 20,Y1 + 10 TO X1 + 20,Y1 + 40 TO X1 + 10,Y1 + 40 TO X1 + 10,Y1 + 30 TO X1 + 40,Y1 + 30 TO X1 + 40,Y1 + 40 TO X1 + 30,Y1 + 40 TO X1 + 30,Y1 + 10 TO X1 + 40,Y1 + 10 TO X1 + 40,Y1 + 20 TO X1 + 10,Y1 + 20 TO X1 + 10,Y1 + 10

Dungeon (4)

140  IF ZZ = 4 THEN  HPLOT X1 + 20,Y1 + 20 TO X1 + 30,Y1 + 30: HPLOT X1 + 20,Y1 + 30 TO X1 + 30,Y1 + 20

Lord British's Castle (5)

150  IF ZZ = 5 THEN  HPLOT X1,Y1 TO X1 + 50,Y1 TO X1 + 50,Y1 + 50 TO X1,Y1 + 50 TO X1,Y1: HPLOT X1 + 10,Y1 + 10 TO X1 + 10,Y1 + 40 TO X1 + 40,Y1 + 40 TO X1 + 40,Y1 + 10 TO X1 + 10,Y1 + 10 TO X1 + 40,Y1 + 40: HPLOT X1 + 10,Y1 + 40 TO X1 + 40,Y1 + 10

Mountains (1)

160  IF ZZ = 1 THEN  HPLOT X1 + 10,Y1 + 50 TO X1 + 10,Y1 + 40 TO X1 + 20,Y1 + 30 TO X1 + 40,Y1 + 30 TO X1 + 40,Y1 + 50: HPLOT X1,Y1 + 10 TO X1 + 10,Y1 + 10: HPLOT X1 + 50,Y1 + 10 TO X1 + 40,Y1 + 10: HPLOT X1,Y1 + 40 TO X1 + 10,Y1 + 40: HPLOT X1 + 40,Y1 + 40 TO X1 + 50,Y1 + 40
170  IF ZZ = 1 THEN  HPLOT X1 + 10,Y1 TO X1 + 10,Y1 + 20 TO X1 + 20,Y1 + 20 TO X1 + 20,Y1 + 30 TO X1 + 30,Y1 + 30 TO X1 + 30,Y1 + 10 TO X1 + 40,Y1 + 10 TO X1 + 40,Y1

Then we loop back for the next tile and then return when all 9 are done:

190  NEXT : NEXT : RETURN

Initialization

It's about time we got to the first 30 lines of the program.

In case of error...

0  ONERR  GOTO 4
1  REM

Reset input / output

4  PR # 0: IN # 0

Set HIMEM to $BFFF (which I guess would effectively remove DOS)

5  HIMEM: 49151

Clear all variables and GOSUB to the Character Creation routine.

7  CLEAR : GOSUB 60000

Get a random number based on the player's lucky number.

8 ZZ =  RND ( -  ABS (LN))

Can't fine where a variable LE is used anywhere.

9 LEVEL = 0

Switch to text mode and print the welcome message:

10  TEXT : HOME : NORMAL : VTAB (12): PRINT " WELCOME TO AKALABETH, WORLD OF DOOM!"

Initialize the arrays:

20  DIM DN%(10,10),TE%(20,20),XX%(10),YY%(10),PER%(10,3),LD%(10,5),CD%(10,3),FT%(10,5),LAD%(10,3)
  • DN%(10,10) is for dungeon level maps
  • TE%(20,20) is for terrain map
  • XX%(10) unknown
  • YY%(10) unknown
  • PER%(10,3) unknown
  • LD%(10,5) unknown
  • CD%(10,3) unknown
  • FT%(10,5) unknown
  • LAD%(10,3) unknown

Given we haven't seen any of the unknown ones anywhere else yet, they must be related to drawing the dungeon images.

Randomize the Terrain Map

Set the outsides to mountains:

30  FOR X = 0 TO 20:TE%(X,0) = 1:TE%(0,X) = 1:TE%(X,20) = 1:TE%(20,X) = 1: NEXT

Warn the player that this next bit could take a while...

35 : VTAB (23): PRINT "  (PLEASE WAIT)";

Completely randomize the inside 19 x 19 terrain tiles (except for Lord British's Castle). I'm still not sure what the ; 5 * 4.5 in the INT would do:

40  FOR X = 1 TO 19: FOR Y = 1 TO 19:TE%(X,Y) =  INT ( RND (1) ; 5 * 4.5)

However, if we roll a town/shop, there's a 50% chance it will be changed to empty land:

41  IF TE%(X,Y) = 3 AND  RND (1) > .5 THEN TE%(X,Y) = 0
42  NEXT : PRINT ".";: NEXT

We pick one place to put Lord British's Castle. We then randomly pick a starting location and make that position a town/shop.

50 TE%( INT ( RND (1) * 19 + 1), INT ( RND (1) * 19 + 1)) = 5:TX =  INT ( RND (1) * 19 + 1):TY =  INT ( RND (1) * 19 + 1):TE%(TX,TY) = 3

I guess that makes it possible (1 in 361 chance) that there'll be no Lord British in a game because the starting location (and hence the starting town) overwrote Lord British's Castle.

Lines 51–62 seem to be initializing some lookup tables for the dungeon drawing so we'll get to those in a bit. The rest of the initialization code is...

Clear the screen and set the graphics color to white:

68  HOME : HCOLOR= 3

Then set the top edge and the width of the text area to be 20 and 29 and go to the top of that area:

69  POKE 34,20: POKE 33,29: HOME

Finally we draw the surrounding terrain and go to the command loop:

70  GOSUB 100: GOTO 1000

So, we just have the following left:

  • an explanation of XX%(10), YY%(10), PER%(10,3), LD%(10,5), CD%(10,3), FT%(10,5), LAD%(10,3)
  • the initialization in lines 51–62
  • the routines in lines 200–491 and 3087–3089

all of which are almost certainly to do with drawing the dungeons.

Because it's hard to understand what is being initialized in lines 51–62 without seeing how it's later used, let's take a look into the routines starting at line 200.

We enter hires graphics mode, set DIS (distance?) to zero and set the plot colour to white:

200  HGR :DIS = 0: HCOLOR= 3

Next we calculate CENT, LEFT and RIGH (RIGHT). Recall that PX and PY are the player's location and DX and DY give the direction they are looking.

202 CENT = DNG%(PX + DX * DIS,PY + DY * DIS):LEFT = DNG%(PX + DX * DIS + DY,PY + DY * DIS - DX):RIGH = DNG%(PX + DX * DIS - DY,PY + DY * DIS + DX)

We'll come back to line 204 .

Now CENT, LEFT and RIGHT contain what is in front and to the left and right. The tens column is each case is used for the monster (if any) and the ones column for the tile. We put into MC the monster in front and change CENT to just be the tile. Similarly we change LEFT and RIGHT to just contain the tile (with no monster info):

205 CENT =  INT (CENT):LEFT =  INT (LEFT):RIGHT =  INT (RIGHT)
206 MC =  INT (CENT / 10):CENT = CENT - MC * 10:LEFT =  INT ((LEFT / 10 -  INT (LEF / 10)) * 10 + .1):RIGH =  INT ((RIGH / 10 -  INT (RIG / 10)) * 10 + .1)

If DIS is zero then we skip to 216...

208  IF DIS = 0 THEN 216

Otherwise...

We're still not sure what 3 or 4 correspond to (doors?) but if we have a WALL (1) or a 3 or 4 we draw a line from (L1,T1) to (R1,T1) to (R1,B1) to (L1,B1) to (L1,T1).

210  IF CENT = 1 OR CENT = 3 OR CENT = 4 THEN  HPLOT L1,T1 TO R1,T1 TO R1,B1 TO L1,B1 TO L1,T1

And if it's a WALL or 3, that's it, we go on to line 260.

212  IF CENT = 1 OR CENT = 3 THEN EN = 1: GOTO 260

If it is a 4 we additionally draw 4 more lines.

214  IF CENT = 4 THEN  HPLOT CD%(DIS,0),CD%(DIS,3) TO CD%(DIS,0),CD%(DIS,2) TO CD%(DIS,1),CD%(DIS,2) TO CD%(DIS,1),CD%(DIS,3):EN = 1: GOTO 260

So does this tell us anything about these variables?

L, T, R and B almost certainly stand for left, top, right and bottom. L1, T1, R1 and B1 must be coordinates of the rectangle for a wall straight in front of the player.

They were initialized as:

204 L1 = PER%(DIS,0):R1 = PER%(DIS,1):T1 = PER%(DIS,2):B1 = PER%(DIS,3):L2 = PER%(DIS + 1,0):R2 = PER%(DIS + 1,1):T2 = PER%(DIS + 1,2):B2 = PER%(DIS + 1,3)

So we seem to have unlocked what PER%(10,3) is for. PER%(d, n) tells us the n-th coordinate used to draw a wall a distance d away. I suspect PER may be short for "perspective".

Let's take a look at the initialization:

Into XX%(0) and YY%(0) we put our vanishing point (roughly center of the screen):

51 XX%(0) = 139:YY%(0) = 79

Next we step 2, 4, 6, ... 20 and calculate XX%(X/2) and YY%(X/2) (i.e. 1, 2, ... 10):

52  FOR X = 2 TO 20 STEP 2:XX%(X / 2) =  INT ( ATN (1 / X) /  ATN (1) * 140 + .5):YY%(X / 2) =  INT (XX%(X / 2) * 4 / 7)

I'll explain the trigonometry in a little bit but note here that the 4 / 7 is just because that's the ratio of the viewport height to the viewport width and so once we've worked out the XX for a given distance, we can just get the YY by multiplying by this.

This enables us to calculate our perspective. 0 is left, 1 is right, 2 is top, 3 is bottom.

53 PER%(X / 2,0) = 139 - XX%(X / 2):PER%(X / 2,1) = 139 + XX%(X / 2):PER%(X / 2,2) = 79 - YY%(X / 2):PER%(X / 2,3) = 79 + YY%(X / 2): NEXT

And finally we set our zero-distance coordinates to be the edge of the screen:

54 PER%(0,0) = 0:PER%(0,1) = 279:PER%(0,2) = 0:PE%(0,3) = 159

And what about CD%(10,3) seen in line 214?

It is initialized on line 55:

55  FOR X = 1 TO 10:CD%(X,0) = 139 - XX%(X) / 3:CD%(X,1) = 139 + XX%(X) / 3:CD%(X,2) = 79 - YY%(X) * .7:CD%(X,3) = 79 + YY%(X): NEXT : PRINT ".";

This is very similar to PER but (X,0) and (X,1) are scaled by 1/3, and (X,2) by 0.7.

The only thing CD%() is used for is line 214 (drawing tile 4).

If there's a wall on the left, we draw the part where it hits the ceiling and the floor:

216  IF LEFT = 1 OR LEFT = 3 OR LEFT = 4 THEN  HPLOT L1,T1 TO L2,T2: HPLOT L1,B1 TO L2,B2

And likewise the right:

218  IF RIGH = 1 OR RIGH = 3 OR RIGH = 4 THEN  HPLOT R1,T1 TO R2,T2: HPLOT R1,B1 TO R2,B2

If there's a 4 on the left (we still don't know what 4 is) we draw most based on LD%:

220  IF LEFT = 4 AND DIS > 0 THEN  HPLOT LD%(DIS,0),LD%(DIS,4) TO LD%(DIS,0),LD%(DIS,2) TO LD%(DIS,1),LD%(DIS,3) TO LD%(DIS,1),LD%(DIS,5)
222  IF LEFT = 4 AND DIS = 0 THEN  HPLOT 0,LD%(DIS,2) - 3 TO LD%(DIS,1),LD%(DIS,3) TO LD%(DIS,1),LD%(DIS,5)

And similarly on right, but mirror image:

224  IF RIGH = 4 AND DIS > 0 THEN  HPLOT 279 - LD%(DIS,0),LD%(DIS,4) TO 279 - LD%(DIS,0),LD%(DIS,2) TO 279 - LD%(DIS,1),LD%(DIS,3) TO 279 - LD%(DIS,1),LD%(DIS,5)
226  IF RIGH = 4 AND DIS = 0 THEN  HPLOT 279,LD%(DIS,2) - 3 TO 279 - LD%(DIS,1),LD%(DIS,3) TO 279 - LD%(DIS,1),LD%(DIS,5)

We need to draw some extra lines if we're not a WALL (or 3 or 4):

228  IF LEFT = 3 OR LEFT = 1 OR LEFT = 4 THEN 234

If the distance we're drawing is not zero, we draw a vertical line on the left:

230  IF DIS <  > 0 THEN  HPLOT L1,T1 TO L1,B1

and regardless of distance we draw another trapezoid:

232  HPLOT L1,T2 TO L2,T2 TO L2,B2 TO L1,B2

and we do the same for the right:

234  IF RIGH = 3 OR RIGH = 1 OR RIGH = 4 THEN 240
236  IF DIS <  > 0 THEN  HPLOT R1,T1 TO R1,B1
238  HPLOT R1,T2 TO R2,T2 TO R2,B2 TO R1,B2