Sword of Fargoal
Sword of Fargoal on the Speccy?!
Rather than "caving in" (giggle) and spending a bit too much time with my C64 friends, I did the only feasible thing: to develop a ZX Spectrum version of Sword of Fargoal. Similar to the Vic-20 version, I coded it in Basic (started Z80 assembler coding the year after), but it still contained a lot of the original game: procedural-random generated maps, multiple enemies, hitpoints, skills, spells, multiple levels, and fights with several of the classical enemies from the Commodore versions of the game.
Playing the game
To play Sword of Fargoal, download it using the links at the top and load it in your favorite emulator. Greeted by the title screen with my fake Epyx copyright notice, you will find your little character in a dungeon, surrounded by monsters, traps, treasure and - if you are good enough - the legendary Sword of Fargoal.
CONTROLS: Left, Down, Up, Right: 5, 6, 7, 8 (cursor keys) Teleport: T Climb up/down: C Use magic bag: B Light spell: L
Aim of the game? Walk around, fight monsters, get experience points, raise your experience level, climb down the dungeon. Collect gold, sacrifice it at temples for extra experience points. Explore everything - try finding the Sword of Fargoal. Explore mystery squares, which could be floor or ceiling traps - or useful spells, magic bags, healing potions and "exchanted weapons". Give a 13 year-old non-native english speaker a break, will ya?!
It's terrible easy to get killed immediately. If so, run the game again (once ended, press R - RUN and a flashing C should appear - and press Enter). One of the first things you might want to do is to modify the code a bit, making fights to your advantage. After the various screenshots below, we will delve into the code itself.
Screenshots: Sword of Fargoal on the ZX Spectrum 48k
Sword of Fargoal: dissecting the game
The game consists of the following three files:
- Program: FARGOAL
A short loader in BASIC that takes care of loading the custom graphics data file (see below, initiating these character graphics, displaying a little loading screen, and loading the main Basic program.
- Bytes: FARGOAL
A custom graphics file, storing a custom-made set of 92 characters
- Program: MAIN-FRGL
The main game, written in Basic
Program: FARGOAL (The loader)
When loaded, this program will auto-run from the start (line 10 below):
10 BORDER 0: PAPER 0: INK 7: CLS
15 LOAD "FARGOAL" CODE 60256+33*8
19 POKE 23606,96: POKE 23607,235
20 FOR t=1 TO 19: PRINT AT t,0; INK 6;"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": NEXT t
30 PRINT AT 0,0; INK 7;"MAIN PROGRAM LOADING-PLEASE WAIT"
40 PRINT AT 7,10; INK 7;"bbbbbbbbbb": FOR t=8 TO 13: PRINT AT t,10; INK 7;"b b": NEXT t: PRINT AT 13,10; INK 7;"bbbbbbbbbb"
50 PRINT AT 9,19;"bbbbbbbbb";AT 10,19;" ";AT 11,19;"bbbbbbbbb"
60 PRINT AT 10,14; INK 5;"f";AT 8,11; INK 7;"c";AT 11,12; INK 6;"i"
70 PRINT AT 19,0;: LOAD ""
After setting the background to black and default text output to white, the program loads the custom graphics data, storing this at RAM address 60520 onwards. This set actually starts with ASCII character 33 (the exclamation mark), meaning that the actual character set actually starts at address 60256. To use the custom character data instead than the default, we need to change this in the system variables. The two values in 23606 and 23607 contain the 16-bit address to where the character set starts, and these are stored in big-endian format, i.e. where the "high" byte being stored in 23607 and the "low" byte in 23606. So, to use the character graphics that start at 60256, we need to poke the value 235 into address 23607 and 96 into address 23606. Why 235? Because if you divide 60256 with 256, you get 235.375 - so 235 by rounding to nearest integer. Why 96? Because that is what is left from 60256-(235*256)=96. So by putting 96 in memory cell 23606 and 235 in memory cells 23607, we are telling the Speccy to use the character sets that starts at 96+256*235, i.e. cell 60256.
POKE 23606,0: POKE 23607,60.
Once the new character set is activated, lines 20-60 draws up a little dungeon scenario, also informing the user that the main program is about to load - which is happening on line 70.
Bytes: FARGOAL (The custom character set)
This file contains the custom character set. As mentioned above, the custom characters loaded starts with the exclamation mark, loaded at 60520. Each character is 8 bytes long (representing the classical 8x8 character sizes of the Spectrum). With reference to the specific ASCII codes used in the Spectrum, the following custom graphic characters are included in this file:
Symbols for punctuation and mathematics are kept the same, although slightly modified in looks. Same with all capital letters (char 65-90) - although bolder, they correpond to the capital letters in the standard set. All small letters, from a to z, are however here replaced with game graphics and sprites. 'a' corresponds to an undiscovered tile, 'b' to a wall segment, 'c' to a temple, and so on. The protagonist is 'f', letters 'g' to 'n' are various foes, and the graphics for the Sword of Fargoal is stored in letter 'o'.
Program: MAIN-FRGL (The actual game code)
Having initialized the custom graphics, the last file loaded is the actual game. It can thus be loaded and run separately, but well, all graphics would be replaced with the default lower-case letters! Once the program is loaded, it starts running immediately. However, to gain access to the Basic code, all you need to do is to 'break' the program, i.e. holding down Shift and Space simultaneously. You then have access to the complete game in Basic in the emulator.
In what follows, I will go through the code in more detail, describing what each part does. Rather than following the code line-by-line below, I will instead follow the execution thread. You can also download the complete Basic code as a simple text file in the URL below.
Initializing monsters and map area
The first line that is run is the one at the top. This line simply makes a subroutine jump to line 9000:
1 GO SUB 9000
Going down to line 9000, there is a chunk of lines that takes care of initializing some data structures that will be used throughout the game:
9000 RESTORE 9000: DIM M$(20,20): DIM M(20): FOR t=1 TO 20: READ M$(T),M(T): NEXT t 9010 DATA "gSHADOW DRAGON",19,"hMONK",16,"gGARGOYLE",9,"hCLERIC",7,"iDWARF GUARDIAN",8,"iWARLORD",12,"jSNAKE",11,"jSCORPION",13,"kFLOOR SLIME",17,"kWALL CREEPER",12 9020 DATA "lGIANT SPIDER",9,"lWEB SPIDER",11,"mFROGMAN",10,"mGOBLIN",13,"nSMALL DEVIL",9,"nLARGE DEVIL",11 9030 DATA "iHOBGOBLIN",9,"iMURDERER",8,"hDARK WARRIOR",20,"lGREAT ANT",18 9040 DIM A$(21,32) 9050 DIM P$(5,6): FOR T=1 TO 5: READ P$(T): NEXT T 9060 DATA "THUD!","CLINK!","SLAFS","GROWL!","SWISCH" 9070 RETURN
On line 9000, the read pointer is reset to start reading data from this line onwards (with the data stored in lines 9010-9030). This is followed by setting up two arrays - M$ and M - that are used to store static data about the 20 types of monsters that can be encountered. M$ holds the name of the monsters, each name being a maximum of 20 characters, and M holds the skill level for each type of monster (this also being the initial hitpoint of the monster). M$ is actually a 2-dimensional array, which is needed as we are storing strings. Note that the first character in the monster name is its graphical character, i.e. character 'g' depicts a shadow dragon, 'k' depicts a floor slime and also a wall creeper etc. This is a condensed way of storing both the name and the graphics of the monster. (And yes, this means that a monster's name can be a maximum of 19 characters, excluding this initial character for its graphics).
Line 9040 initializes a 2-dimensional array with fixed dimensions: A$(21,32). This data structure stores the actual level content, consisting of 21 rows and 32 columns of 'tiles'. The Speccy screen is actually 24 rows, but I left out the bottom 2 as they are often used for input/output. I also left out the first line (row 0) as this is used to display various in-game messages. As this playing area is stored as characters, the dollar sign is used (similar to the monster names in the previous paragraph). A$ will later on be populated with random content and monsters, and this is what we will be using when the player interacts and encounters various objects and monsters.
Lines 9050-9060 creates a P$ array containing five 'onomatopoeias', i.e. written representations of sounds, in this case fighting sounds. 'Slafs" is, I believe, somewhat swedish. The five 6-character sounds are in the 9060 DATA statement. Finally, on 9070, the program returns to the line that called the subroutine, so we jump back to the top again, continuing after line 1.
Welcome message and initializing variables
Lines 10 to 70 initializes variables for the particular game, and prepares output.
10 BORDER 0: PAPER 0: INK 7: CLS 15 LET SWORD=INT (RND*50)+50 20 PRINT AT 5,0;"EPYX PRESENTS...";AT 11,7;"bSWORD OF FARGOALb";AT 10,7;"bbbbbbbbbbbbbbbbbb";AT 12,7;"bbbbbbbbbbbbbbbbbb" 30 PRINT AT 14,6;"COPYRIGHT © 1985 EPYX" 40 RESTORE 50 50 READ a,b: IF a<>-1 THEN BEEP a,b: GO TO 50: DATA .1,0,.2,5,.1,0,.1,5,.2,9,.1,5,.1,9,.2,12,.1,9,.1,5,.2,0,-1,0 60 LET EX=0: LET EL=1: LET MHP=INT (RND*9)+6: LET HP=MHP: LET BS=8: LET DL=1: LET MS=0 70 LET G=0: LET SHS=0: LET LL=0: LET MBS=0: LET PO=1: LET MB=0: LET SH=0: LET TE=1: LET LI=0
Line 10 sets the background and ink color and clears the screen. That is, CLS, rather than CLEAR, as we don't want to clear the arrays we defined in the previous subroutine.
The SWORD variable is a random value between 50-100, this being the dungeon level where the Sword of Fargoal will be located. This is immensely deep down in the dungeon and should likely be adjusted!
Lines 20-30 prints out a welcome message. In line 20, note the mixing of lower- and upper-case letters. In the custom character set, uppercase (capital) letters are the actual letters, whereas lower-case letters will represent the various 8x8-pixel game blocks in the game. Thus, although you see the 'b' character there on line 20, this will be a piece of wall when you have the system variables at 23606 and 23607 pointing to the correct start address for the custom graphics (see previous section). ALSO: in the original code on the Spectrum, the wall block character (i.e. 'b') is here printed in yellow, whereas the text parts are written in white. However, instead of using INK 6 and INK 7 for these parts, the characters are instead colored directly in the source code. This is done by first getting into the E cursor mode (press Shift+Symbol shift simultaneously), and then hold Shift while pressing a number between 0 and 7. This allows for changing the color of a text in-between characters.
Lines 40-50 plays the classical intro jingle from the original game. Reading both length and tone of each beep from the DATA at the end of this line, the line keeps on reading and beeping until encountering a -1 as the length value. Note that the reading point is set to line 50 in line 40.
Line 60-70 initializes a lot of in-game variables, as follows: EX=experience, EL=experience level, MHP=maximum hit points (random between 6 and 15), HP=current hit points (starts off at max), BS=battle skill (starts at 8), DL=dungeon level (starts at 1), MS=counter of monsters slain, G=gold, SHS=no idea, LL=flag if light spell is active, MBS=active magic bags, PO=healing potions (starts with 1), MB=magic bags (unused), SH=shield spells (unused), TE=teleport spells (unused), LI=light spells (unused).
Starting a new (or the first) level
Line 100 is the entry point for starting a new level.
100 LET SHS=0: LET MBS=0: CLS : PRINT AT 5,4;"EXPERIENCE POINTS: ";ex;AT 6,4;"EXPERIENCE LEVEL: ";EL;AT 8,4;"MAXIMUM HITPOINTS: ";MHP;AT 9,4; "BATTLE SKILL: ";BS;AT 11,4;"DUNGEON LEVEL: ";DL;AT 12,4;"MONSTERS SLAIN: ";MS 110 PRINT AT 17,1;"WAIT . . ." 120 GO SUB 8000
Once again, the two of the variables above are initialized again: the mysterious SHS and the MBS. I guess that the SHS is an indicator (binary flag) variable whether a shield spell is active or not, meaning that it can only last for a maximum of one level. However, the shield spell is not (yet!) implemented. It is however strange that the active magic bags only last one level, as they are also reset here. (The default gold carrying capacity is 100, each active magic bag increasing the gold carrying capacity by 100, so seems weird that the magic bags are even treated as spells).
Main purpose of line 100 is however to clear the screen and display current player statistics: experience points, level, hit points, battle skills, dungeon level and number of monsters slain so far. Line 110 prints out a wait message before line 120 calls a subroutine on line 8000 - so let's jump ahead to see what happens there.
Generating a dungeon layout
The lines between 8000-8030 are where the basic dungeon layout is created:
8000 FOR t=1 TO 2: LET A$(t)="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": NEXT t 8001 FOR t=20 TO 21: LET A$(t)="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": NEXT t 8002 FOR T=3 TO 19: LET A$(T)="bb bb": NEXT t 8005 FOR Z=1 TO 25 8010 LET X=INT (RND*3)+1: LET Y=INT (RND*3)+1: LET XX=INT (RND*18)+4-X: LET YY=INT ((RND*28)+3)-Y 8020 FOR T=XX TO XX+X: FOR U=YY TO YY+Y: LET a$(t,u+1)="b": NEXT U: NEXT T 8025 NEXT z 8030 RETURN
When creating a dungeon level, we thus populate the A$ array with either spaces or character 'b', the latter representing a wall in the custom character set. Lines 8000-8002 first create 2-square thick walls on the border of the game area, with the main area being filled with spaces (i.e. the space character). This would however be quite a boring dungeon, so we need to fill it with rooms and corridors.
The code for the random generation of a dungeon is in the four lines 8005-8025. First, an outer loop (Z) is set to create a total of 25 rectangular wall areas. The size and position of each of these wall areas are decided in line 8010. The width and height of each of these areas are between 1-3, respectively, given by X and Y, and their top-left location, given by XX and YY, are adjusted according to their sizes. On line 8020, the specific wall area is filled with wall characters ('b'), and on line 8025, the code continues with the next wall area. Having done all 25 wall segments, line 8030 returns to the calling GO SUB statement.
Going back to the calling code, lines 130-140 display some more information about your inventory, before line 150 calls a new subroutine:
130 CLS 140 PRINT AT 2,4;"HEALING POTION ";PO;AT 3,4;"MAGIC BAGS ";MB;AT 5,1;"SPELLS...";AT 6,3;"SHIELD ";SH;AT 7,3;"TELEPORT ";TE;AT 8,3;"LIGHT ";LI;AT 10,5;"WAIT..." 150 GO SUB 8100
Populating the dungeon with monsters and objects
Lines 8100-8350 initialize and populate the current dungeon with monsters, staircases up and down, a temple, bags of gold, mysteries and, possibly, the Sword of Fargoal. It starts with creating a random number of monsters:
8100 LET MONSTER=INT (RND*4)+2+DL 8110 DIM N(MONSTER,4) 8120 FOR T=1 TO MONSTER 8130 LET MONSTERVAL=INT (RND*20)+1: IF M(MONSTERVAL)>DL+8 THEN GO TO 8130 8140 LET N(T,1)=MONSTERVAL: LET N(T,4)=M(MONSTERVAL) 8150 LET XX=INT (RND*20)+1: LET YY=INT (RND*30)+1: IF A$(XX,YY)<>" " THEN GO TO 8150 8160 LET N(T,2)=XX: LET A$(XX,YY+1)=M$(N(T,1),1): LET N(T,3)=YY: NEXT T
Randomizing the number of monsters on line 8100, this being between 2-5, plus one per level, an array N is initialized on line 8110, holding data for each of the active monsters. Looping through the monsters (loop from 8120 to the end of 8160), one of the 20 random types are first selected on line 8130. However, the higher level monsters only appear as the player advances deeper down (i.e. as DL increases). Once a monster has been found, its type (index number) and level (obtained from the constant M vector with monster levels) are stored in the current-monster-data array N (in the first and fourth cells). The initial location for the monster is randomized on line 8150, repeating this until a "free" cell in the map is found. On line 8160, the coordinate for the monster is stored in the N array (in the second and third cells). The actual monster graphics is also stored at the correct location in the map area, where the monster graphic character is the first character in the M$ array. This line ends by ending the loop, so that all this is repeated for each of the monsters that will be on this level.
Lines 8170-8190 is used to randomly locate two downward staircases. Contrary to the original game, this is simply depicted with the capital letter 'D'. A random location is picked for each of these, first checking that the location is empty before placing it.
8170 FOR t=1 TO 2 8180 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY)<>" " THEN GO TO 8180 8190 LET A$(XX,YY)="D": NEXT T
Lines 8200-8230 is used to randomly locate two upward staircases. Represented by capital letter 'U', the code is quite identical for those of downward staircases. However, if the player is on dungeon level 1, no upward staircases will be generated. Thus, even if you find the sword of fargoal, there is currently no escape (and thus no ending) from the game!
8200 IF DL=1 THEN GO TO 8250 8210 FOR T=1 TO 2 8220 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY)<>" " THEN GO TO 8220 8230 LET A$(XX,YY)="U": NEXT T
On line 8250, a suitable location for the temple is found, and subsequently placed on line 8260.
8250 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY)<>" " THEN GO TO 8250 8260 LET A$(XX,YY)="c"
Lines 8270-8290 places a random number (1-7) of gold bags on the map. Location and amount of gold of each bag is also stored in a separate G array.
8270 LET GOLD=INT (RND*7)+1: DIM G(7,3): FOR t=1 TO GOLD 8280 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY)<>" " THEN GO TO 8280 8290 LET G(T,1)=XX: LET A$(XX,YY+1)="r": LET G(T,2)=YY: LET G(T,3)=INT (RND*25)+25: NEXT T
Lines 8300-8320 places a random number of 'mysteries' on the map. The graphics for these mysteries is the same as for the undiscovered map, i.e. a chequred 8x8-pixel character ('a'). These mysteries are not determined when being placed; rather, their content is randomized when the player moves over them. Thus, there is no need to store them in a separate array. (As will be explained later, we can't treat bag of golds in the same manner, i.e. randomly rolling the amount of gold they contain as we pick them up. This will be explained below in the interaction section).
8300 FOR T=1 TO INT (RND*10)+1+(3*DL) 8310 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY)<>" " THEN GO TO 8310 8320 LET A$(XX,YY)="a": NEXT T
Finally, in the content generation subroutine, line 8330 checks if the current level is the level where the Sword of Fargoal should be placed. If not, the subroutine ends and returns to the main code; otherwise, a suitable random location for this final treasure is found and the sword – character 'o' – is placed on the map before returning to the calling code.
8330 IF DL<>SWORD THEN RETURN 8340 LET XX=INT (RND*21)+1: LET YY=INT (RND*31)+1: IF A$(XX,YY+1)<>" " THEN GO TO 8340 8350 LET A$(XX,YY+1)="o": RETURN 8999 STOP
Clouding the sight, placing the player...
Back to line 160, the screen is filled with yellow 'undiscovered' area (with a somewhat different filler effect than what is found in the Commodore versions). Line 180 creates a suitable starting coordinate for our adventurer, followed by a line that calls a subroutine on line 7900.
160 FOR T=1 TO 21: PRINT AT T,0; INK 6;"aaaaaaaaaaaaaaaa";AT 22-t,16;"aaaaaaaaaaaaaaaa": NEXT t 180 LET X=INT (RND*21)+1: LET Y=INT (RND*31)+1: IF A$(X,Y+1)<>" " THEN GO TO 180 190 GO SUB 7900
Revealing your surroundings...
This subroutine is called to reveal the nearby area of the player:
7900 PRINT AT X-1,Y-1;A$(X-1,Y TO Y+2);AT X,Y-1;A$(X,Y);"f";A$(X,Y+2);AT X+1,Y-1;A$(X+1,Y TO Y+2);AT 0,0;" ": IF LL=0 THEN RETURN 7905 PRINT AT X-2,Y-2;A$(X-2,Y-1 TO Y+3);AT X+2,Y-2;A$(X+2,Y-1 TO Y+2): FOR T=-1 TO 1: PRINT AT X+T,Y-2;A$(X+T,Y-1);AT X+T,Y+2;A$(X+T,Y+3): NEXT T: RETURN 7906 RETURN
On line 7900, the near area surrounding the player and the actual player – character 'f' – is displayed. Using the player coordinates, the nearby area is obtained from the A$ 2-dimensional array, i.e. that holds the game objects by coordinate. This will thus display both walls, enemies, objects, staircases etc – everything that is stored in the A$ matrix. It also clears the top line, dedicated for various status messages. If the light spell is NOT active (i.e. if LL equals to zero), this subroutine will return immediately. Otherwise, i.e. if the light spell is active, it will also display a slightly larger area (line 7905), subsequently returning. (It would probably be more effective to start off with the LL conditional, and then have two functions for displaying surrounding areas, depending on whether the LL is 1 or 0).
Starting the main game loop
Line 200 is where the central game loop starts, i.e. the loop that is constantly iterated during gameplay. The gameloop is constructed so the player first gets the opportunity to move and control the character for a total of 6 steps (as given in the loop on line 200, ending on line 300). This is followed by each monster getting the chance to move one step. Having iterated all active monsters, the program jumps back to line 200, repeating the central game loop.
200 FOR U=1 TO 6
Line 205 increases the experience level (EL) if the current experience point (EX) is has passed a new 1000 threshold. This would perhaps be better as a subroutine, called each time the player gains experience points, rather than checking at every single possible player action.
205 IF EX/1000>EL THEN LET EL=EL+1: PRINT AT 0,0;"LEVEL RAISED TO ";EL: PAUSE 10
Line 206 takes care of hit point regeneration. If the current hit point (HP) is less than the maximum(MHP), there is a chance (65%) that the player's hit points will increase by one.
206 IF HP<MHP AND RND>.65 THEN LET HP=HP+1
Line 210 captures the currently pressed key. If no key is pressed, the player has forfeited to do anything in this turn, so it jumps ahead to the next of the six action opportunities the player has between the turn that the monsters have.
210 LET K$=INKEY$: IF INKEY$="" THEN GO TO 300
Lines 250-253 checks for movement up, down, right, and left, also checking that there is no wall unit ('b') blocking that path. Note that these wall checks are done in the A$ array storing the current dungeon, i.e. not checking on the screen using SCREEN$ or similar. If all ok, the player coordinate (X,Y) is updated, the draw-near-area subroutine on line 7900, and the program jumps to line 7910 where would-be object interaction is handled.
250 IF K$="7" AND A$(X-1,Y+1)<>"b" THEN PRINT AT X,Y;" ": LET X=X-1: GO SUB 7900: GO TO 7910 251 IF K$="6" AND A$(X+1,Y+1)<>"b" THEN PRINT AT X,Y;" ": LET X=X+1: GO SUB 7900: GO TO 7910 252 IF K$="8" AND A$(X,Y+2)<>"b" THEN PRINT AT X,Y;" ": LET Y=Y+1: GO SUB 7900: GO TO 7910 253 IF K$="5" AND A$(X,Y)<>"b" THEN PRINT AT X,Y;" ": LET Y=Y-1: GO SUB 7900: GO TO 7910
Staying in the player loop, non-movement-related control actions are checked on lines 260-264. On line 260, we check if a teleport spell is cast and that the player has any teleport spells left (TE>0). If so, the number of teleport spells are decreased by one, the current position of the player is blanked out, and the code jumps to line 180 (which is where the original starting position of the player was randomized).
260 IF K$="T" AND TE>0 THEN LET TE=TE-1: PRINT AT X,Y;" ": GO TO 180
And here is a drawback with the code: when teleporting while standing on a temple or a staircase, this object will disappear on the screen, only being visible again once the player gets closer to this temple or staircase again! Of course, rather than just blanking out, it is better to print out what is stored in the A$ data structure!
Line 261 and 262 is for climbing down (261) and up (262) a level. Note that both pits and ceiling traps can be climbed. Whereas the pits could lead to the player falling multiple levels down, here it is just equivalent to a single level. Changing a level is easy: the DL is adjusted and a new dungeon is initialized by jumping to line 100
261 IF K$="C" AND (A$(X,Y+1)="D" OR A$(X,Y+1)="p") THEN PRINT AT 0,0;"GOING DOWN A LEVEL": LET DL=DL+1: FOR T=0 TO -4 STEP -1: BEEP .1,T: NEXT T: GO TO 100 262 IF K$="C" AND (A$(X,Y+1)="U" OR A$(X,Y+1)="q") THEN PRINT AT 0,0;"GOING UP A LEVEL": LET DL=DL-1: FOR T=-4 TO 0: BEEP .1,T: NEXT T: GO TO 100
Similar to using a teleport spell, line 263 is for using a magic bag and 264 is for using a light spell.
263 IF K$="B" AND MB>0 THEN PRINT AT 0,0;"USE MAGIC BAG": LET MB=MB-1: LET MBS=MBS+1 264 IF K$="L" THEN PRINT AT 0,0;"LIGHT SPELL USE": LET LL=1
These act a bit weird in the current code. First, magic bags were not technically spells in the original game; rather, when picked up, they were immediately used to expand the gold carrying capacity with 100 each, for all levels. This doesn't seem to be the case here. Light spells were however temporary, but whereas they only lasted a single level in the original games, this one lasts forever. Thus, there is no real need to store multiple light spells!
Finally, on line 300, the player-only loop ends, providing a new iteration of player actions by repeating from line 205 for a total of 6 times before the monsters get a chance to move.
300 NEXT U
Monster movement is handled on lines 310-330, with a loop on line 310 going through all monsters in the current dungeon as stored in the 2-dimensional array N. It ignores all the already-killed monsters, whose monster type variable – N(T,1) - is set to zero if that's the case.
310 FOR T=1 TO MONSTER: IF N(T,1)=0 THEN NEXT T: GO TO 200
For each of the currently active monsters, line 320 calculates a new preferred X and Y position for the specific monster, based on the X and Y position of the player. If this new position turns out to be the actual position of the player, a fight emerges, which is resolved on line 340. If not, it first checks that the new preferred moster position is empty on line 322. If not, the monster stands still and it is the next monster's turn, followed by a return to the start of the game loop, and the six player actions starting on line 200.
320 LET X1=N(T,2)-(N(T,2)>X)+(N(T,2)
Y)+(N(T,3)<Y): IF X1=X AND Y1=Y THEN GO TO 340 322 IF A$(X1,Y1+1)<>" " THEN NEXT T: GO TO 200
On lines 325-330, the actual monster movement takes place, to the new preferred position. First, as the monster potentially could move in the yet-unexplored areas, line 325 checks that the monster is actually visible on the screen at its old location. If so, it replaces that graphics with a blank space. On line 327, it checks whether the new screen area is an empty space: if so, it draws the monster sprite on that square. Note how the monster is drawn using the first character in its name string. This means that a monster indeed could move into the shade, making it no longer visible until that area is explored. Finally, on line 330, the move takes place. First, the game board in A$ is updated, removing the monster graphics from its previous coordinates. Then the coordinates of this monster are updated. And finally, the monster graphc is added to the new position on the game board A$. Line 330 ends by looping to the next monster, and finally returning to the start of the main game loop.
325 IF SCREEN$ (N(T,2),N(T,3))=M$(N(T,1),1) THEN PRINT AT N(T,2),N(T,3);" " 327 IF SCREEN$ (X1,Y1)=" " THEN PRINT AT X1,Y1;M$(N(T,1),1) 330 LET A$(N(T,2),N(T,3)+1)=" ": LET N(T,2)=X1: LET N(T,3)=Y1: LET A$(X1,Y1+1)=M$(N(T,1),1): NEXT T: GO TO 200
Lines 340 to 410 takes care of a fight between a monster and the player. Line 350 initializes two variables. FIG is the hig point of the monster, initialized when the monster is created. FIGHT is a value that compares the battle skill of the monster (this being equal to its initial hit points) and the battle skill of the player. In the next line, the fight balance value is however decreased by a random value, plus the experience level of the player. On line 360: if the FIGHT value is negative, the player loses this number of hit points (and the monster also run the risk of losing a singular hit point). On line 370: if the FIGHT value is positive, the monster loses this difference (and similarly, the player could lose a hit point if unlucky). This is followed on line 380 by a random fight message and sound – Thud! Growl! Slafs!
340 PRINT AT 0,0;"FIGHT WITH A ";M$(N(T,1),2 TO ): PRINT AT N(T,2),N(T,3);" " 350 LET FIG=N(T,4): LET FIGHT=BS-N(T,4) 355 LET FIGHT=FIGHT-INT (RND*3)+EL 360 IF FIGHT<0 THEN LET HP=HP+FIGHT: LET FIG=FIG-(1 AND RND>.5) 370 IF FIGHT>=0 THEN LET FIG=FIG-FIGHT: LET HP=HP-(1 AND RND>.6) 380 PRINT AT 0,0;" ": PRINT AT 0,0;P$(INT (RND*5)+1);" ";HP: FOR Q=1 TO 3: BEEP .01*RND*3,RND*30+30: NEXT Q
If the player's hit points are below zero, line 390 checks if the player is dead – and the game stops.
390 IF HP<0 THEN PRINT AT 0,0;"SLAIN BY A ";M$(N(T,1)): STOP
Line 400 checks if the monster is slain. If so, the slain-monster-counter is increased, a victorious message is displayed, the monster graphics is removed from the A$ play area, the player's experience point is increased, the monster is removed from the N array, and the game returns to the main game loop. If neither the player nor the current monster is dead, the fighting continues by going back to line 355.
400 IF FIG<0 THEN LET MS=MS+1: PRINT AT 0,0;"YOU SLAINED A ";M$(N(T,1),2 TO ): LET A$(N(T,2),N(T,3)+1)=" ": LET EX=EX+N(T,4)*100:: LET N(T,1)=0: BEEP .1,19: BEEP .3,24: LET A$(X,Y+1)=" ": GO TO 200 410 PAUSE 10: GO TO 355
Object interaction by player
In the code for movement (line 250-253), after the reveal-near-area subroutine has been called, the program jumps to line 7910 to check for would-be interaction with objects on the map.
If the player is on the temple but lacks gold, line 7910 displays a message and a 2-note jingle before returning to the next step in the game loop (and then actually restarts the 6-step player-only loop).
7910 IF A$(X,Y+1)="c" AND G=0 THEN PRINT AT 0,0;"TEMPLE!": BEEP .05,40: BEEP .05,40.8: NEXT U: GO TO 200
If the player walks into a free slot, the standard status text is displayed, while jumping to line 300 where the next 6-step player loop is executed (followed by monster movement). Note that it would be more accurate to also do this in line 7910.
7920 IF A$(X,Y+1)=" " THEN PRINT AT 0,0;"HP: ";HP;" EX: ";EX;" GOLD: ";G;" ": GO TO 300
If the player enters the temple while having gold, this is sacrificed in exchange for some experience points. Once that is done, the player loop continues (which should be a GO TO 300 instead)
7930 IF A$(X,Y+1)="c" AND G>0 THEN PRINT AT 0,0;"SACRIFICE OF GOLD!": FOR T=1 TO 5: BEEP .05,RND*30: NEXT T: LET EX=EX+G*5+INT (RND*10): LET G=0: NEXT U: GO TO 200
If the player walks over a bag of gold - 'r' - or a batch of gold that the player previously had to leave behind due to exceeded carrying capacity - 'd' - a short jingle is played and the game jumps to line 7800, where the gold-picking is resolved.
7940 IF A$(X,Y+1)="r" OR A$(X,Y+1)="d" THEN PRINT AT 0,0;"HIDDEN TREASURE!": FOR T=1 TO 6: BEEP .01,RND*20+40: NEXT T: GO TO 7800
If the player encounters a stairway down or an old floor pit going down, a downward-oriented jingle is played, and the game continues. Similar for stairways up and previously triggered ceiling traps, where the jingle is more upward-oriented.
7950 IF A$(X,Y+1)="D" OR A$(X,Y+1)="p" THEN PRINT AT 0,0;"TUNNEL GOING DOWN": FOR T=0 TO -4 STEP -1: BEEP .1,T: NEXT T: NEXT U: GO TO 200 7955 IF A$(X,Y+1)="U" OR A$(X,Y+1)="q" THEN PRINT AT 0,0;"TUNNEL GOING UP": FOR T=-4 TO 0: BEEP .1,T: NEXT T: NEXT U: GO TO 200
If the player encounters a mystery, this having the same graphics as the hidden squares, i.e. 'a' (and recall that the hidden squares are not actually stored in the map), the code jumps to line 7700 where this is resolved.
7960 IF A$(X,Y+1)="a" THEN GO TO 7700
If the player were to encounter the Sword of Fargoal, execution jumps to line 9500.
7965 IF A$(X,Y+1)="o" THEN GO TO 9500
The player could also walk past a monster. To check for this, the code iterates through all monster coordinates, checking if the screen for that is the player graphics 'f'. If not, it continues with the next monster and then jumps back to the main game loop. If it is, line 7971 displays this encounter. Note however that no battle is initiated here: battle with monsters only happen when monsters move onto the player.
7970 FOR T=1 TO MONSTER: IF SCREEN$ (N(T,2),N(T,3))<>"f" THEN NEXT T: NEXT U: GO TO 200 7971 PRINT AT 0,0;"A ";M$(N(T,1),2 TO ): BEEP .1,30: GO TO 210
When the player walks into a mystery square (line 7960), the content of this mystery box is resolved on the lines 7700-7783. If this turns out to be a trap – a floor trap, a ceiling trap or an explosion – the player will lose some hit points, continuing on line 7790. If the player still has hit points, the game goes back to the main game loop. If not, the game checks if the player is out of healing potions. If so, the program goes to line 9400 informing the player about his/her demise and stopping the game. If the player does have at least one healing potion, this is taken, restoring the hit points to half of the max, reducing the number of healing potions and returning the game to the main player loop.
7700 LET A$(X,Y+1)=" ": LET PRYLAR=INT (RND*100)+1: IF PRYLAR>50 THEN LET PRYL=INT (RND*5)+1: GO TO 7710 7705 LET PRYL=INT (RND*5)+6 7710 IF PRYL<=2 AND DL>=2 THEN PRINT AT X,Y;"q": PRINT AT 0,0;"CEILING TRAP": FOR T=40 TO 0 STEP -2: BEEP .001,T: NEXT T: LET A$(X,Y+1)="q": LET HP=HP-(5+INT (RND*5)): GO TO 7790 7720 IF PRYL<=4 THEN PRINT AT X,Y;"e": PRINT AT 0,0;"FLOOR TRAP": FOR T=40 TO 0 STEP -2: BEEP .001,T: NEXT T: LET A$(X,Y+1)="p": LET HP=HP-(5+INT (RND*5)): GO TO 7790 7730 IF PRYL=5 THEN PRINT AT X,Y; INK 2;"q": PRINT AT 0,0;"EXPLOSION!": FOR T=1 TO 5: BEEP .05,-30: NEXT T: LET A$(X,Y+1)=" ": LET HP=HP-10: GO TO 7790 7740 IF PRYL>=6 AND PRYL<=8 THEN GO TO 7780 7750 IF PRYL=9 THEN PRINT AT 0,0;"EXCHANTED WEAPON": BEEP .1,10: BEEP .1,15: BEEP .1,25: BEEP .1,20: LET BS=BS+1: GO TO 210 7760 IF PRYL=10 THEN PRINT AT 0,0;"HEALING POTION FOUND": FOR T=30 TO 40: BEEP .005,T: BEEP .005,T+1: BEEP .005,T+2: BEEP .005,T+1: BEEP .005,T: NEXT T: LET PO=PO+1: GO TO 210 7770 IF PRYL=11 THEN PRINT AT 0,0;"A MAGIC BAG FOUND!": BEEP .01,40: BEEP .01,45: BEEP .01,55: BEEP .01,50: BEEP .01,30: BEEP .01,25: BEEP .01,30: BEEP .01,0: LET MB=MB+1: GO TO 210 7780 FOR T=1 TO 10: BEEP .01,T: BEEP .01,T+5: NEXT T: IF PRYL=4 THEN LET TE=TE+1: PRINT AT 0,0;"TELEPORT SPELL FOUND!" 7781 IF PRYL=5 THEN LET SH=SH+1: PRINT AT 0,0;"SHIELD SPELL FOUND!" 7782 IF PRYL=6 THEN LET LI=LI+1: PRINT AT 0,0;"LIGHT SPELL FOUND!" 7783 GO TO 210 7790 IF HP>0 THEN GO TO 210 7791 IF PO=0 THEN GO TO 9400 7792 PRINT AT 0,0;"HEALING POTION TAKEN": LET HP=INT (MHP/2): LET PO=PO-1: BEEP 1,-40: GO TO 210
Lines 7800-7899 contain code for picking up a bag of gold. The code is a bit weird though: instead of checking the player coordinate with the coordinates for the gold bags in the G array, it instead checks the screen memory (using SCREEN$) to see if the player graphics - 'f' - is to be found on the coordinate where there should be a gold bag! If so, it continues to line 7810; otherwise, the program stops, displaying a bug. On line 7810, the player's carried gold is increased by the bag's content, and the gold bag is 'removed' from the gold array G (setting its value to zero) and the gold bag character in A$ is replaced with a space.
Line 7811 checks the player's gold carrying capacity, possibly storing the extra amount into a buried treasure that can be collected later. Such a buried treasure has a different graphical look than a bag of gold - 'd' instead of 'r' - but both have identical functions in the game. The new value of this buried treasure is stored in the same position as the gold bag that was picked up. And this is the reason why the values of the gold bags must be stored and not determined on-the-fly when picking them up, as some of these 'bags' (or buried treasures) have a precalculated value that must be kept.
7800 FOR Z=1 TO GOLD: IF SCREEN$ (G(Z,1),G(Z,2))="f" THEN GO TO 7810 7805 NEXT Z: PRINT "BUGG ": GO TO 210 7810 LET G=G+G(Z,3): LET G(Z,3)=0: LET A$(X,Y+1)=" " 7811 IF G>(100*MBS)+100 THEN PRINT AT 0,0;"GOLD TOO HEAVY!": LET G(Z,3)=G-((100*MBS)+100): LET G=(100*MBS)+100:: LET A$(X,Y+1)="d": GO TO 210 7820 IF G<(100*MBS)+100 THEN GO TO 210 7899 STOP
When finding the Sword of Fargoal, lines 9500 onwards are executed. After a happy message, the battle skill and maximum hit points are increased, and the SWORD level is set to zero. However… the sword is not removed from the play area, making it possible to pick it up for all eternity. So yes, indeed plenty of ways to improvements here!
9500 PRINT AT 0,0;"THE SWORD OF FARGOAL!": FOR T=1 TO 10: BEEP .01,T: BEEP .01,T+2: BEEP .01,T+10: BEEP .01,T+12: NEXT T: PRINT AT 0,0;"BATTLESKILL +3 MAX HITPOINTS +5": LET BS=BS+3: LET MHP=MHP+5: LET SWORD=0: GO TO 210
The final two lines were likely for debug reasons:
9900 FOR t=1 TO 21: FOR u=1 TO 32: PRINT AT t,u-1;a$(t,u): NEXT u: NEXT t 9910 FOR T=1 TO MONSTER: PRINT AT N(T,2),N(T,3);M$(N(T,1),1): NEXT T
This code reveals the map (line 9900) and prints out the monsters (9910). The latter line is a bit of a mystery though, as monster graphcs are already placed on the actual map (i.e. the A$ array). However, this would be a useful function if the player were to find a map of the current level, i.e. when the level would reveal itself.
Finding this software on one of the cassette tapes found on my mom's attic, this was clearly work-in-progress, making me wonder what it was that made me stop working on it. Perhaps I encountered a dilemma I couldn't really solve. Perhaps I couldn't figure out a better version of the flawed battle mechanics above, maybe I didn't get the shield function to work, or similar. Maybe I realized that Epyx probably wouldn't be interested! :)
A more likely explanation, based on how I tend to operate when I code as an adult, is that I might have decided to redo everything from scratch. Realizing that there is a particular data structure or code flow that is more optimal than the onw I started working on, I do have the tendency to rewrite, rather than patch. Also, I wrote this code just as I was getting into Z80 assembler - I began working on my first 3d isometric drawing routines a year after - so it is not impossible that I wanted to redo it in machine code instead. Or that I simply continued with other projects, like Ragnar (that I will eventually dissect as well on this site).
Still, having found this on my old cassette tapes, I must admit that I am quite inspired to actually finish coding this game for the ZX Spectrum - and then of course do it in Z80 assembler instead! It shouldn't be that hard: I need to devise a better battle mechanics, but it shouldn't be too tricky to come up with something that could work (and simulate it extensively in Python before coding it up in Z80). And of course, it would then perhaps make sense to have a unified data structure where I store all static interactive objects on the screen – stairs, gold bags, temples – as separate from, respectively, the wall game area and the dynamic monster data. And likely lots more to come up with!