-
Notifications
You must be signed in to change notification settings - Fork 0
Technical Details
This document deals with the game from a programmer's perspective. If anyone is interested in playing the game, this may contain minor spoilers.
Realm uses a combination of AppleSoft BASIC and MERLIN assembly language. Party and configuration data is saved as text, while maps, sprites, and artwork are encoded as binary files.
Realm works with either DOS 3.3 or ProDOS. The DOS 3.3 version is distributed on four 5.25" floppy disks (or images). It also works with Sider small volumes. The ProDOS version can be run on a variety of mass storage devices.
Realm requires 48K of RAM. Memory management is often explicit. Memory usage is extremely tight, and programs have to be frequently swapped in and out from disk.
The upper 16K of the Apple ][ address space is occupied by I/O and ROM, leaving the lower 48K for RAM. Additional RAM can only be accessed via bank switching, which Realm does not do (the ProDOS version can use a RAM disk, however). Memory maps for the DOS 3.3 and ProDOS versions are slightly different, as shown below.
| Address | Usage | Format |
|---|---|---|
| $00-$FF | All free ZP bytes are used | Binary Data |
| $100-$1FF | Stack | System |
| $200-$2FF | Input Buffer | System |
| $300-$3B3 | SOUND |
Machine Code |
| $400-$7FF | Text Screen | System |
| $800-$9B0 | Outdoor Sprites | Binary Data |
| $800-$AE8 | Town Sprites | Binary Data |
| $800-$FFB | Dungeon Sprites + Brushes | Binary Data |
| $801-$FFF | Large Merchant Programs | AppleSoft BASIC |
| $B01-$FFF | Small Merchant Programs | AppleSoft BASIC |
| $801-$3FFF | Character Utilities | AppleSoft BASIC |
| $1001-$14B0 |
CHAIN(1) |
AppleSoft BASIC |
| $15AC-$163D | Artwork Painter, SDP.INTRP
|
Machine Code |
| $163E-$1EFF | Map Painter, MAP.INTRP
|
Machine Code |
| $1F00-$1FA9 | Submap Buffer for MAP.INTRP
|
Binary Data |
| $1FC0-$1FD9 | Sprite Pointers for MAP.INTRP
|
Binary Data |
| $1FEE-$1FFF | Parameters for MAP.INTRP
|
Binary Data |
| $2000-$3FFF | Graphics Screen | Binary Data |
| $4000-$6CFF | Main Programs | AppleSoft BASIC |
| $6D00-$7FFF | BASIC Variables(2) | AppleSoft BASIC |
| $8000-$8C7F | Map Data(3) | Binary Data |
| $8C80-$9A98 | Monster Artwork(4) | Binary Data |
| $9AA5-$BFFF |
DOS 3.3(5) |
System |
| $C000-$CFFF | Input/Output | System |
| $D000-$FFFF | Apple ][ ROM | System |
(1) This is not Apple's CHAIN
(2) This is set using LOMEM and HIMEM
(3) Largest range, outside maps
(4) Largest range, HYDRA
(5) Realm sets MAXFILES=1, thereby gaining a little memory back from DOS
ProDOS consumes more memory than DOS 3.3. As a result, when running under ProDOS, maps and artwork use overlapping memory, so that the dungeon map has to be saved and read back in before and after combat. ProDOS also forbids loading binary files higher than BASIC variables.
| Address | Usage | Format |
|---|---|---|
| $00-$FF | All free ZP bytes are used | Binary Data |
| $100-$1FF | Stack | System |
| $200-$2FF | Input Buffer | System |
| $300-$3B3 | SOUND |
Machine Code |
| $400-$7FF | Text Screen | System |
| $800-$9B0 | Outdoor Sprites | Binary Data |
| $800-$AE8 | Town Sprites | Binary Data |
| $800-$FFB | Dungeon Sprites + Brushes | Binary Data |
| $801-$FFF | Large Merchant Programs | AppleSoft BASIC |
| $B01-$FFF | Small Merchant Programs | AppleSoft BASIC |
| $801-$3FFF | Character Utilities | AppleSoft BASIC |
| $1001-$14B0 |
CHAIN(1) |
AppleSoft BASIC |
| $15AC-$163D | Artwork Painter, SDP.INTRP
|
Machine Code |
| $163E-$1EFF | Map Painter, MAP.INTRP
|
Machine Code |
| $1F00-$1FA9 | Submap Buffer for MAP.INTRP
|
Binary Data |
| $1FC0-$1FD9 | Sprite Pointers for MAP.INTRP
|
Binary Data |
| $1FEE-$1FFF | Parameters for MAP.INTRP
|
Binary Data |
| $2000-$3FFF | Graphics Screen | Binary Data |
| $4000-$6CFF | Main Programs | AppleSoft BASIC |
| $6E00-$7A7F | Map Data(2) | Binary Data |
| $6D00-$7B18 | Monster Artwork (3) | Binary Data |
| $7C00-$91FF | BASIC Variables (4) | AppleSoft BASIC |
| $9200-$95FF | ProDOS file buffer (5) | System |
| $9600-$BFFF | BASIC.SYSTEM |
System |
| $C000-$CFFF | Input/Output | System |
| $D000-$FFFF | Apple ][ ROM | System |
(1) This is not Apple's CHAIN
(2) Largest range, outside maps
(3) Largest picture, HYDRA, 3608
(4) We only set LOMEM and let ProDOS manage HIMEM
(5) ProDOS moves HIMEM down $400 while a file is opened
Realm sometimes switches between BASIC programs loaded into disjoint regions of memory. This allows large programs to stay in memory while running a smaller program, e.g., merchant programs can be run without having to subsequently reload the larger TOWN program. This optimization matters when real floppy disk access times are involved.
The Applesoft interpreter uses little-endian addresses stored in zero page to find the start and end of a BASIC program. The start of the program is stored at location 103, and the end of the program is stored at location 175. The interpreter does not seem to need the end of the program to run it, but it is definitely needed when editing or saving. In order to chain one program into another (load a new program that remembers the variables from the old one), we load a binary file that contains a BASIC program, and spoof the interpreter into branching to the new program:
100 PRINT CHR$(4);"BLOAD NEXTPROGRAM,A$4001": REM LOAD PROGRAM AT ADDRESS $4001
110 POKE 16384,0: POKE 103,1: POKE 104,64: GOTO 110Here, the BLOAD is only necessary if the program is not already loaded. Note line 110 is not an infinite loop, it branches to a different line 110 from another program. For this to work the following must be adhered to:
- the
GOTOcommand should follow thePOKEcommands on the same line - make sure every program is preceded by zero in memory
- this is
POKE 16384,0in the example
- this is
- line numbers involved in the branch must satisfy
INT(L1/256) >= INT(L2/256), whereL1is the current line andL2is the destination- this assures the interpreter will start the line number search at (103)
To save a BASIC program as a binary file, first get the starting address using PEEK(103) and PEEK(104). Get the ending address from PEEK(175) and PEEK(176). Calculate the length by subtracting the start from the end and type BSAVE MYPROGRAM,A$XXXX,L$YYYY, where XXXX and YYYY are the hex starting address and length, respectively. The project includes deployment scripts to do this automatically.
In AppleSoft BASIC, variables are scoped by program. Apple provided the CHAIN program to hand off variables to a new program. Rather than use Apple's CHAIN, Realm relies on the code relocation strategy discussed above, which preserves variables as long as they don't point to program space.
An important consequence of this is that all variables are essentially global. Scoping is entirely dependent on programmer discipline. To make things more difficult, only the the first two letters of a variable name are significant in AppleSoft.
| Identifier (1) | Purpose | Indexing | Scope |
|---|---|---|---|
| AA% | Auto Attack | COMBAT | |
| AB%(4) | Hit Points | Character | global |
| AC%(4) | Max Hit Points | Character | global |
| AD% | Gold | global | |
| AE | Food | global | |
| AF%(4) | Strength | Character | global |
| AG%(4) | Intelligence | Character | global |
| AH%(4) | Wisdom | Character | global |
| AI%(4) | Agility | Character | global |
| AJ%(4) | Stamina | Character | global |
| AK%(4) | Charisma | Character | global |
| AP$(4,8) | Items | Character,Item | global |
| AX$(4,16) | Spells | Character,Spell | global |
| BP$ | Program directory | global | |
| CL$(4) | Class | Character | global |
| CL%(4) | Class Code | Character | global |
| DR%(4,3) | Index Ready | Character,Slot | global |
| DT%(3) | Index Ready Temporary | Slot | local |
| DV$(4) | Armor Ready | Character | global |
| DW$(4) | Weapon Ready | Character | global |
| DX$(4) | Spell Ready | Character | global |
| D | Disk Number | local | |
| FG%(6) | Flags (2) | Story Elements | global |
| FL% | Mode of Travel | global | |
| FO% | Mode of Travel on Entry | global | |
| FS% | Fast Status Mode | global | |
| GA | Hit Die | Monster | COMBAT |
| GB | Damage | Monster | COMBAT |
| GC$ | Special Attack | Monster | COMBAT |
| GD$ | Element Type (3) | Monster | COMBAT |
| GE | To Hit | Monster | COMBAT |
| GF | Spell Damage | Monster | COMBAT |
| GH | Spell Area of Effect | Monster | COMBAT |
| GI$ | Spell Name | Monster | COMBAT |
| GK$ | Plural Suffix | Monster | COMBAT |
| GM | Number | Monster | COMBAT |
| GN(*) | Hit Points | Monster | COMBAT |
| GO$ | Name | Monster | COMBAT |
| GP%(4) | Front Lines | Character | COMBAT |
| GS | Being Attacked | COMBAT | |
| MA | Multiple Attack Counter | COMBAT | |
| MD% | Map Dirty | OUTSIDE,CHAIN | |
| NA$ | Party Name | global | |
| NM$(4) | Name | Character | global |
| PC%(4) | Paralysis | Character | COMBAT |
| PG$ | Program to Chain | local | |
| PM%(10) | Paralysis | Monster | COMBAT |
| PN$(10) | Saved Parties | CHARACTER | |
| RA$(4) | Race | Character | global |
| RB$ | RAM Disk or BIN | global | |
| RD$ | RAM Disk or PROG | global | |
| RM$ | Realm | global | |
| SC$ | Attack Type | COMBAT | |
| SD$ | Attack Verb | COMBAT | |
| SE$ | Attack Element (3) | COMBAT | |
| TV$ | Trove | DUNGEON,COMBAT | |
| WA | Attack Damage | COMBAT | |
| WB | Base To Hit Chance | COMBAT | |
| WC | Total To Hit Chance | COMBAT | |
| WD%(4) | Drive Mappings | Disk | global |
| WR | Attack Range | COMBAT | |
| WS%(4) | Slot Mappings | Disk | global |
| WT%(4) | Max Level | Character | global |
| WT% | Denizen Code | COMBAT,DUNGEON,TOWN | |
| WU%(7,7) | Proficiency Table | class code,weapon code | global |
| WV%(4) | Volume Mappings | Disk | global |
| WX% | Safe Dungeon Level (4) | DUNGEON | |
| WX% | Town Hostile | TOWN | |
| WY% | Dungeon Level | DUNGEON | |
| WY% | Ship Owner | TOWN | |
| X,Y | Outside Coordinates | global | |
| XT,YT | Inside Coordinates | DUNGEON/TOWN | |
| ZA%(4) | Experience | Character | global |
(1) Array dimensions are given as the number elements.
(2) 0=Fonkrakis, 1=Abyss, 2=Wornoth, 3=enlightened, 4=power of light, 5=ethereal
(3) F=fire, I=ice, L=lightning, R=radiation, U=undead, M=psychic, P=poison, A=acid
(4) Above this level monsters flee, below it they attack.
The sign of a number is sometimes used to select from two alternative combat scenarios. Once a negative number triggers an alternative, the sign is often changed back to positive at some point in the calculation.
Spell damage, WA, scales with level if positive. If negative, it does not scale with level. Note that attacks with wands, staves, and rods, are treated as spell attacks.
A weapon's to hit chance, WB, is subjected to a proficiency adjustment if positive. If negative, it is subjected to a spell failure test instead. Wands, staves, and rods, generally have WB negative. The messages that are displayed during the attack are also affected. The adjusted to-hit chance in every case is stored in WC.
The exact calculation of WC is subject to change from version to version for balancing purposes. As of this writing the calculation is
IF WB > 0 THEN WC = (WB - GA + AI%(A) + ZA%*2)*WU%(CL%(A),C)/9: WC = 100*WC/(80+0.4*WC)
IF WB < 0 THEN WC = ABS(WB): IF SC$ = "P" OR SC$ = "T" THEN WC = WC - GA
IF WC > 100 THEN WC = 100The spell attack type SC$ takes values "I" for immediate damage, "T" for temporary status effects, and "P" for permanent status effects. Magical weapons use the same variable.
A weapon's range, WR, takes values -1,0,1. If WR=0, the weapon can only be used on the front lines. If WR=1, the weapon can be used from any range. If WR=-1, the weapon can only be used on the front lines, but can be thrown from the rear once, after which it is lost forever.
The multiple attack counter, MA, can also be positive or negative. If positive, multiple attacks will hit the same monster until it dies, and then proceed to the next monster. If negative, the attacks hit random targets, except for the initial attack.
The monster's base to-hit chance is in GE. This is used for both regular and spell attacks. The local variable I is set to a random number, and adjusted for armor and other protections, where the adjustments depend on the type of attack. The if GE>I the attack succeeds.
Realm sacrifices a few features of Applesoft in order to work the way it does.
The STR$ function cannot be used, because the Realm machine code frequently writes to location $FF in zero page. This location is also used by the Applesoft intepreter, but only if STR$ is called.
The chaining system does not preserve string literals. When assigning a string that needs to be persistent, we use seemingly extraneous expressions, e.g., RM$ = "ARR"+"INEA", or AP$(A,L) = LEFT$(A$+" ",LEN(A$)), to force the interpreter to copy the string from program space to variable space. The following regular expression is useful when using a modern editor to find all AppleSoft string assignments:
[0-9:]\s+\w+\$\s*(\(.+\))?\s+\=
User defined functions DEF FN... are never used, as of this writing. Like strings, user functions exist in program space, but unlike strings, there is no simple way to move them to variable space. After chaining it might or might not be necessary to re-define the function, depending on what has been overwritten.
The garbage collection strategy employed in Realm is to run FRE(0) when swapping programs, and before intensive string operations.
Terrain maps are drawn using 14x12 bitmap sprites. In the Apple II high resolution screen buffer, a byte of memory is organized as seven binary pixels, packed horizontally, plus a color bit. Color is determined by the color bit along with details of pixel placement. The whole sprite takes 24 bytes. The pixels are stored as two columns of bytes, first the left 7x12 set of pixels, followed by the right 7x12 set of pixels.
Sprites are found packed in three files, OUTSPS (outside terrain), TWNSPS (town terrain and denizens), and DNGNSPS (dungeon terrain). There is no header or metadata, the sprites are simply packed in order of ascending terrain code (0-15). In OUTSPS this is followed by two sprites for representing the party, one for flying, one for walking. In TWNSPS, there is a only a sprite for a walking party, followed by sprites for 14 town denizens. In DNGNSPS the walking party is sprite 17. For some reason there is a townsman as sprite 16.
Monster artwork is drawn using basic lines, plus a standard Apple II shape table. The shape table is the basic brush set D0 in SDP. In Realm, the D0 data is actually embedded in DNGNSPS, at offset $200.
Starting with v1.4, machine language routines are derived from MERLIN assembly language. These perform the same functions as the old Monitor routines, but with better efficiency, modularity, and extensibility.
Address space: $300-$3B3
This is the code that produces the sounds. It also contains the stack repair program from the AppleSoft BASIC reference manual.
As of this writing the BASIC code directly calls RPRSTK, UPCHIRP, DNCHIRP, UPFAT, and SIREN.
| Parameter | Address | Note |
|---|---|---|
| LOTONE | $06 | Period of the low tone in a chirp |
| HITONE | $07 | Period of the high tone in a chirp |
| CYCLES | $08 | Number of speaker pulses in a tone |
| REPS | $09 | Repetitions of a chirp or siren |
| RATE | $1E | Rate of change of the tone in a chirp |
The effect of the parameters on the sounds can be explored using SOUND.TEST in the editor folder.
Address space: $15AC-$163D
This is the artwork interpeter from SDP. It renders a squeezed SDP picture by stepping through a list of drawing operations. The only input is the address of the picture, stored in $06,$07. Zero page locations $EB-$EF and $FA-$FB are used as variables.
Address space: $163E-1EFF
This is the most complex of the assembly language programs. It performs the following functions:
- Paint or scroll the player's current view of the map on the screen.
- Allow or forbid the player's movement into a given map square.
- Handle movement or death of town and dungeon denizens.
- Retrieve the pointer to a treasure trove appropriate to the player's location.
- Display the monologue appropriate to the player's location.
The map interpreter can be tested using WALK.ALL in the editor folder. This program can also serve as a terrain editor of sorts.
There are three basic types of terrain: outside, town, and dungeon. However, in a town, there is actually a fourth type of "terrain" used to display the town denizens. In particular, every grid point in the town has a corresponding grid point for the denizens (doubling the storage requirement).
Terrain is saved as a nibble (every bit is precious). The encoding is as follows:
| Code | Town | Denizen | Outside | Dungeon |
|---|---|---|---|---|
| 0 | Food | Guard | Pyramid | Stairs Up |
| 1 | Tree | Human | Forest | Stairs Down |
| 2 | Pub | Elf | Mountain | Earth |
| 3 | Shipyard | Dwarf | Swamp | Corpse |
| 4 | Water | Hobbit | Water | Water |
| 5 | Wall | Adventurer | Ether | Stalagtites |
| 6 | Table | Wizard | Lava | Table |
| 7 | Library | Marine | Castle | Floor |
| 8 | Temple | Dragon | Volcano | Door |
| 9 | Weapons | Special Human | Dormant Volcano | Lava |
| 10 | Door | Special Elf | Door | Chest |
| 11 | Warship | Special Dwarf | Warship | Mordock |
| 12 | Fire | Special Hobbit | Iron Tower | Trap |
| 13 | Road | Special Dragon | Dungeon | Special Monster |
| 14 | Armour | Corpse | Town | High Priest |
| 15 | Field | Nil | Field | Monster |
Each type of map consists of a packed sequence of nibbles which indicate the type of terrain located at a given grid point. The nibbles are arranged like a FORTRAN array, where the first index runs East, the second index runs South, and the third index runs down into the ground if you happen to be in a dungeon. The size of each type of map is as follows:
| Type | Size |
|---|---|
| Outside | 80 x 80 |
| Town | 40 x 40 |
| Dungeon | 20 x 20 x 8* |
* Interpretation of dungeon levels is subtle, see below.
The outside map files, ARRINEA, FONKRAKIS, ABYSS, and WORNOTH, consist simply of an 80 x 80 map. Ships are part of the map. With DOS 3.3, all parties share the same maps, and therefore also each other's ships. With ProDOS, each party has their own maps and ships. In either version, maps are auto-saved upon entering a town or dungeon, while other party data is never auto-saved (this can lead to an inconsistency, but it is tolerable).
The town map consists of a 40 x 40 terrain map, a 40 x 40 people map, the town name, and monologues attached to certain town inhabitants. In particular,
| Offset | Data | Length |
|---|---|---|
| 0 | Terrain Map | 800 |
| 800 | Name | 50 |
| 850 | People Map | 800 |
| 1650 | Monologues | 120x7 |
There are seven monologues each occupying 120 bytes. They are associated with people codes 9-14 (“specials”) according to the order in which they appear in memory. Thus, there should be no more than seven specials in a town. The special must be replaced by a corpse if killed. Adding, moving, or deleting corpses outside of this construct will break the monologue system.
Some monologues start with a control character. This indicates that the monologue triggers a dialog subroutine. The control codes are:
| Control Character | Story Element | Realm |
|---|---|---|
| 1 | Sage | Arrinea |
| 2 | Archwizard | Arrinea |
| 3 | Alchemist | Any |
| 4 | Baron X | Fonkrakis |
| 5 | Bakro | Arrinea |
| 6 | Red prism | Fonkrakis |
| 7 | Clear prism | Arrinea |
| 8 | Prodigy | Arrinea |
| 9 | Gaurdian | Wornoth |
| 10 | Gatemaster | Wornoth |
The dungeon map consists of a 20 x 20 x 8 map, the dungeon name, and a list of treasure. The dungeon levels are not strictly limited to the nominal 20 x 20 rectangle, however. The dungeon interpreter views the dungeon as a single 160 x 20 (rows x columns) level, in which the function of a ladder is to teleport the adventurer by 20 rows, either north or south. The dungeon walls prevent the adventurer from simply walking between levels. Although it is not exploited much, this system allows for a complex level topography. It does require, however, limiting the user's view of the map, in order to maintain the illusion.
| Offset | Data | Length |
|---|---|---|
| 0 | Map | 1600 |
| 1600 | Treasure | 80x5 |
| 2000 | Name | 30 |
There are five treasure troves each occupying 80 bytes. Each treasure is a text description of several items separated by slashes. For example,
DAGGER/WAND OF FIRE/LIGHTNING ROD.
Each treasure trove is associated with terrain code 13, “special monster”. After a special monster is killed, it must be replaced by the corpse terrain code.
There are four Realms: Arrinea, Fonkrakis, Wornoth, and Abyss. Travel between realms takes place at Pyramid Gates. To get inside a Pyramid Gate requires a special key, and travel through the gate is only possible with a special prism. The location of towns, castles, and dungeons, is as follows:
| Name | Type | Coordinates |
|---|---|---|
| towne hobulus | Town | 13,67 |
| the city of ophenius | Town | 45,15 |
| towne vodarc | Town | 24,36 |
| towne pleusoria | Town | 34,35 |
| towne sondor | Town | 16,46 |
| towne embius | Town | 45,27 |
| towne moxalia | Town | 69,57 |
| port simboria | Town | 30,65 |
| towne amoria | Town | 61,29 |
| towne raelton | Town | 38,70 |
| tylvon harbour | Town | 42,9 |
| towne kossarc | Town | 21,26 |
| towne lodossia | Town | 60,50 |
| the city of hethor | Town | 9,40 |
| the city of mirrifius | Town | 63,12 |
| enlightenment | Town | 8,21 |
| castle trueblood | Castle | 19,32 |
| castle lemphocym | Castle | 71,35 |
| castle blackmoore | Castle | 45,8 |
| castle nofpheaus | Castle | 34,69 |
| the haunted chasm | Dungeon | 31,34 |
| the gateway to death | Dungeon | 31,13 |
| the dungeon of darkness | Dungeon | 51,18 |
| the well of shadow | Dungeon | 43,38 |
| the dungeon of nihm | Dungeon | 29,49 |
| the dungeon of sedrik | Dungeon | 22,58 |
| the devil's hole | Dungeon | 49,72 |
| the cave of horror | Dungeon | 47,52 |
| the pits of peril | Dungeon | 66,29 |
| the hellish inferno | Volcano | 71,21 |
| Name | Type | Coordinates |
|---|---|---|
| towne ghemalia | Town | 14,35 |
| castle modrosia | Castle | 41,2 |
| the black fortress | Castle | 36,48 |
| the wicked pit | Dungeon | 20,31 |
| Name | Type | Coordinates |
|---|---|---|
| Hellport | Town | 24,22 |
| Damnation Docks | Town | 33,48 |
| Inferno City | Town | 4,3 |
| Iron Tower | Castle | 39,34 |
| Forsaken Mine | Dungeon | 74,5 |
| Dungeon of Mordock | Dungeon | 39,34 |
| Name | Type | Coordinates |
|---|---|---|
| Sulphur City | Town | 56,34 |
| Famine Ridge | Town | 15,50 |
| Vermin Village | Town | 26,64 |
| Brimstone Bay | Town | 11,24 |
| The Depths of Doom | Dungeon | 32,0 |
| The Condemned Caverns | Dungeon | 42,39 |