The DH executable DHUNTERxxx.PRG file is self-contained, which means the game contains both its own Data and its own Code. The advantage is that the program is easy to transport and load. Specifically, it can be loaded easily (linearly) via a tape, and does not require any complex or expensive external disk drive. This is similar to how some early arcade games operated, as the game needed to be accessible immediately, and requiring something like a disk drive was too impractical (slow access and moving parts are prone to wear and tear over time). Once the software development was perfected, an arcade game could be “burned to ROM” and set to be auto-loaded. However, the disadvantage is that this severely limits the scope of the game (how extensive it can be), since the presence of the “Data” reduces the amount of room for Code-Logic (as they both occupy the available address space of the system).
In 1977, disk drives were certainly available and understood, but had not yet been quite standardized and certainly weren’t yet affordable (nor reliable). In other words, they weren’t quite yet “domesticated” for practical home/office use. Interestingly, this situation happened again 15 years later with CD-ROMs, as optical drives were initially thousands of dollars and multiple competing media formats (and again later with HD vs Blu-ray).
While Personal Computing had arrived, they were still more akin to an interactive calculator, and it would be several years before disk drives were affordable and common place within the Home Computing environment. Therefore, this is why DH deliberately limits itself to that scope: a game that could have been experienced in an “out of the box” Commodore PET of late 1977 and does not rely on multiple files. However, DH was not written in BASIC, and a C compiler for the 6502 was not available in 1977; therefore, in those days it would have had to have been written in assembly.
The following page is a “memory map” of the DH software, which is intended to show why the game is limited to 8 stages, and that there is truly no room for expansion on the features of the program (aside from hardware changes; i.e. expansion RAM or external disk files).
When a C program is compiled, typically there is an option to produce a map file that shows a kind of overview-map of how that program is being assembled (to become an executable within the host environment). An annotated version of that file for the FINAL build of DH is available here.
A summary of the more significant aspects is as follows:
- 20,030 bytes for the main “game engine” (stage map handling, draw the map, animating the challenges, applying challenge movement and game attack/defense rules, maintaining states like inventory, steps, time, bow direction, etc.)
- 507 bytes for core functions like writing binary to decimal converted integers to the screen, clearing the screen, getting key-press, monitoring system time Jiffies
- 1127 bytes of UTILITY code (drawing banners with option to invert, random number generator, waiting for ENTER with option to flush keyboard)
- 1059 for Destiny Selection (direction, chime handling)
- 1504 for persona selection (showing the options, stat graph, maintain database of the actual persona stats)
- 258 bytes for the INTRO (to show instructions and map tile symbols)
DATA (4K – consumes same RAM that would otherwise be available for the CODE)
- 1933 bytes DATA for map buffer, “div/mod” tables, animation/redraw location buffers, stage names
- 1385 bytes for STAGE maps and blocker codes
- 627 bytes for all the internal game strings
- 1779 bytes for PET.lib, core functions that the compiler uses to manage calling functions and managing the stack
- 1200 bytes System Reserve (executable starts at 0x0400, plus a reserved stack size) There are two 192 byte tape buffers. DH uses Buffer #1 for SNES GAME PAD state data, and Buffer #2 to support the flicker-free animation (redraw buffer).
25K (code) + 4K (data) + 3K (system) = 32K !
The Map Strategy
The map of each STAGE is RLE (runtime length encoded) in an 8-bit sequence.
[ _ _ _ | _ _ _ _ _ ]
The top 3-bits represent the terrain type. The lower 5-bits is the terrain count, with a maximum of 32 (i.e. binary sequence “00000” implies 1, and sequence “11111” implies a count of 32). A long sequence of WATER, say 45 characters, would need 4 bytes to represent, such as: [W|32] [W|13]. That’s a lot better than needing 45 bytes to express the same information.
Note that when encoded, I don’t actually use “W” – but rather 0 = WATER, 1 = GRASS, 2 = LAND, etc. 3-bits allows for up to 7 different terrain types. Using RLE, a single STAGE can be represented in ~100 bytes instead of 819 bytes (since the top 2 and bottom 2 rows of the screen are used for overlay and not the map, this means the map is only 21 rows x 39 cols bytes = 819; it’s a long story on why I didn’t use the 40th column: originally I was using printf and it moved the cursor to the next row at the 40th column, causing weird problems – I moved away from printf and just started POKEing directly to the screen, but I didn’t go back to update things to use the 40th column during the STAGES – it ends up saving some memory anyway, and most folks probably will never notice).
When a STAGE is started, the RLE encoding for that STAGE is “uncompressed” into a full screen map. At this point, the RLE symbol code (e.g. “0”) becomes whichever symbol is desired to represent (e.g. “WATER”), repeated the RLE count number of times (e.g. 13, 32, or even just 1). Some map symbols are a single option (such as GRASS is always the same symbol). But some other map symbols are RANDOMIZED when the map is created: WATER and ROCKS are like this, which is why during each game, they are a different arrangement each time.
Any type of terrain can be blocked, not just ROCKS. There are separate BLOCKER tables for each STAGE. This is encoded using a 21×5 array (115 bytes) encoded per STAGE. The “x5” is since 5*8 = 40, the width of the screen. The “21x” is since there are only 21 rows to the map itself (the 4 other rows being informational screen overlays). Each individual bit in a row indicates when that corresponding column in the row is BLOCKED. If so, then the PLAYER can’t or move or shoot onto a BLOCKED section, or CHALLENGES (except the HYDRA, who is above the ground).
This allows things like force-fields or “player-keep-out” areas to be expressed, for any terrain type. It also allows for secret passages! It turns out Destiny Hunter just uses the “secret passage” feature, to hide the orb. But the flexibility represents a “game engine” that can easily scale to other types of STAGE acts.
As I progressed further into the game development, I realized I needed an animation strategy. Repainting the whole screen and re-drawing CHALLENGES at new locations was extremely FLICKERY. It might have worked if the whole background was always black/blank, but I had fully populated STAGE maps. And if I didn’t do anything at all, the CHALLENGES would smear all across the map like using a rubber stamp.
So, I rationalized that each CELL on the screen really has 3 states: 1) NORMAL, 2) DRAWN-ON PREVIOUSLY, 3) DRAWN-ON NOW. If a cell is marked as DRAW-ON NOW, then keep it as-is. If a cell remains in the DRAWN-ON PREVIOUSLY even after an animation cycle, it wasn’t retained and therefore reverts back to NORMAL (the NORMAL background map symbol). But I don’t want to redraw all the NORMAL cells all the time. I only want to draw cells during that transition from DRAWN-ON PREVIOUSLY to NORMAL.
I thought about that for awhile longer (a couple days), and decided to code that approach, using compact bit-mask where 2-bits (“half-nibble”) is used for each CELL on the screen. In the code, this became the 23×10 cell_state (where x10 corresponds to 40 columns, with 4-cells per 8-bit byte). But I found the MODULUS and DIVIDE math operations to be very slow. Then I realized I’m applying MOD and DIV with the same denominator of 40 (the screen width), so really I just needed 2 tables: MOD_TABLE and DIV_TABLE, so I could just lookup the results without actually doing a MOD or DIV operation. And this was key to making it fast enough to be presentable, as a flicker-free animation strategy. The approach is a sequence of MARK-QUEUE-SCRUB-COMMIT actions. I describe the sequence in full detail in the following presentations (also available in PDF format):