Initially, I wanted to look a lot more into the source code of those old QuickBASIC games to see how problems were resolved and how things were working. I thought it would be interesting (although not for many.) Recently, I sort of fell in love with a small exploration game that could really use a rewrite and since the accompanying readme file is so inviting, why not have a look at it?
The thing
Programmed by Darkdread (Matt Zuchowski) as a school project, Helter Skelter is a cute little exploration game about a murderous cult leader from the late 60s (insert Nixon joke here,) in which you take control of an anonymous detective searching a house for evidences as you dodge questions from nosy reporters and attacks from ghosts.
Darkdread was a prolific RPG enthusiast who designed several games in QuickBASIC (Eternal Frost, Mattress Warrior, the Secret of Cooey series, Bombcoo,...) most of which shared the same coding style. In a way it's remarkable, but in another it's frightening. If anything, the source code is a testament to the tenacity of young programmers (I'm having "JAW in Attacking Earth's Food Supplies" flashbacks.)
Helter Skelter uses mode 7 (320x200, 16 colors and multiple pages of video memory) for display. I don't have a huge nostalgia for EGA graphics but everything here looks right, from walls and collectables to the characters sprites (featuring two frames of animation in four facing direction)... however, this is also were things go sour.
Pixel data storage
In order to store sprite data, you need to reserve memory; the easiest way to achieve that in QuickBASIC is to create an array large enough that you can cram all the data in there and then use the built-in PUT instruction to paste the buffer on screen. For instance, a 16x16 sprite is 256 pixels. In mode 7, two pixels are stored per byte, so we should only need 128 bytes of memory to store the whole image data (I'm not counting the 4-byte image descriptor.) If we use an INTEGER array (where each element is 2 bytes long) we only need 64 elements. Instinctively, you'd think that you'd need one array per sprite but that's not the case at all if you know where each image starts in the array. So, the multiple arrays at the beginning:
DIM SHARED PlayerUp(150)
DIM SHARED PlayerDown(150)
DIM SHARED PlayerLeft(150)
DIM SHARED PlayerRight(150)
...
Could be joined together as:
DIM SHARED PlayerSprite(599) AS INTEGER
I use the value 599 instead of 600 (150 x 4) because the first element is located at index 0, not 1 and when only one value is used to set the dimensions of an array, it is the upper boundary, not the number of elements. Anyhow; with this technique each sprite is stored 150 elements from one another (sprite 0 at index 0, sprite 1 at index 150, sprite 2 at index 300, and sprite 3 at index 450.) The main benefit of storing all your data in one area is that you don't have to juggle with multiple variables when trying to draw your character on the screen. Instead of:
IF PlayerDir = 1 THEN
PUT (80, 80), PlayerUp, PSET
ELSEIF PlayerDir = 2 THEN
PUT (80, 80), PlayerLeft, PSET
ELSEIF PlayerDir = 3 THEN
PUT (80, 80), PlayerDown, PSET
ELSEIF PlayerDir = 4 THEN
PUT (80, 80), PlayerRight, PSET
END IF
You could simply:
PUT (80, 80), PlayerSprite((PlayerDir - 1) * 150), PSET
Loading data in memory is just as easy, because again, you don't need to juggle with multiple variables to get the job done.
We could put everything (tiles, props and characters) into one big array and reference their offset to access them faster. Or we could keep the program simpler and at the every least create two arrays (one for characters, another for everything else.) This way, we could easily display objects using their index in the map.
As a side note, sprites in Helter Skelter are 20x20. It was likely done because the screen is 320x200 and both the horizontal and vertical dimensions are multiple of 20; thus, when displayed on a grid, no graphic would be cropped. However, in mode 7, QuickBASIC requires images width to be multiple of 8 so the 20-pixel wide sprite is stored internally as a 24-pixel wide image. It means that 24 x 20 / 4 INTEGERs (120 INTEGERs) are actually required to store pixel data.
Level data
The first floor and basement are 42x42 tile-based maps. Each tile holds a value between 0 and 15 (it seems that 6 is unused.) It means that each level could require 882 bytes of data to be stored (if we only use 4 bits per tile.) However, the files provided with the game are about six times that size because they are in text format, which makes for painfully slow loading times. If we were to add a simple RLE compression scheme to the format, we could probably save one extra third of their size, but let's keep things simple.
There are only a few differences between the two areas: reporters from the first floor are replaced by ghosts and some walls use a different image... we could probably use index 6 to cover the different wall and keep the reporter to ghost conversion.
Internally, maps are stored as two-dimensional arrays, which is notoriously slow in QuickBASIC. It's also done with a large INTEGER array which means that each cell hogs 16 bits of memory when they only need 4. One more thing: the upper boundary is set 50 because each side of the 42x42 map is padded to ensure that the camera never falls out of bounds. It's a way to do it I guess... to sum it up, 5000 bytes are reserved to store each area, when only 882 bytes would actually be required. If I were to write it, I'd probably use 1764 bytes to (say it with me) "keep things simple." And that would still be a waste.
You got the bad ending
The goal of the game is to exit the house with the gold key. But to get the good ending, the player must also collect at least 75% of all evidences lying around and retrieve the gun in the basement. However, that's hardly possible due to some oversight in the "Check.Event" routine: evidences are only counted when they are picked up moving up, and the gun is only registered when moving left. It looks like this:
IF PlayerDir = 1 THEN
' check object located at playerX, playerY - 1
' increment collected loot counter
ELSEIF PlayerDir = 2 THEN
' check object located at playerX - 1, playerY
' set GunFound to 1
ELSEIF PlayerDir = 3 THEN
' check object located at playerX, playerY + 1
ELSEIF PlayerDir = 4 THEN
' check object located at playerX + 1, playerY
END IF
This, right here, is an important lesson: if your code features tons and tons of copy/paste of the same exact thing with only minor differences, chances are you can save a lot of space. And if you don't, next time you need to modify something, you'll forget to fix the various iterations of your code. That whole routine can easily be reduced to a quarter of its size and be much more reliable by simply computing the "forward" tile once and then checking for each object, as so:
SELECT CASE PlayerDir
CASE 1
x = playerX
y = playerY - 1
CASE 2
x = playerX - 1
y = playerY
CASE 3
x = playerX
y = playerY + 1
CASE 4
x = playerX + 1
y = playerY
END SELECT
' check objects located at x, y
' increment collected loot counter
' set GunFound to 1
On the bright side, the hard-coded value for the total number of evidences is only 186 when there's actually three times as much in the house, so it's still technically possible to get the good ending if you pick enough stuff from below.
That was long and boring mike!
Truth be told, despite the messy code I like the game. I have a thing for maze exploration and I enjoyed the setting too. Being questioned by reporters with random Charles Manson trivia was sort of cool in an edutainment kind of way, and the extra spooky ghosts in the basement tickled the survival horror fan in me. If anything, the game deserves to be rewritten and maybe expanded upon a tiny little bit. This download comes with a 1.03 patch that fixes the item collection bug (and nothing else) so you too, can become a famed anonymous detective in the Manson's murder case.