Blindganger: Fixing bugs like it's 1988
| 10 minutes

I grew up in the 80’s, the decade home computers went from curiosity to mainstream. In primary school we had several Philips P2000T home computers and a pair of Apple Macintosh — ehm, Macintoshes? Two of those, anyway. A friend had a C64 we used to play games on, and at some point my dad bought a C128 for his financial administration. (What I really love is the fact that, to this day, he is still doing his financial administration on a C128, albeit an emulated one. He is an “if it ain’t broke don’t fix it” kinda guy.)

Soon after the C128, we got a C64. Where the C128 was for business, the C64 was for fun. I have fond memories playing games such as Space Taxi, Super Cycle, Velocipede, Last Ninja II, and Electrix, to name a few. It was also the computer that got me into programming.

With the invention of the World Wide Web still a couple of years off, learning to program mostly involved reading books and magazines. Computer magazines would often publish source code listings which the reader could type in. The result could be anything: A game, a disk copy tool, a drawing program for GEOS, or, more often than not, something that didn’t quite work because of typos. At some point, magazines started publishing listings with a checksum added to each line. They offered special tools that would compute the checksum of each line you typed, and checked it against the checksum you typed in at the end of the line. This helped a lot. Still, it must have been one of the slowest and most error prone ways to copy a computer program in the history of computer programs. It was fun though, or at least, that’s what I thought.

One such magazine was Commodore Dossier, a “practical magazine for the active Commodore owner”, which ran from 1984 until 1988. The 17th issue, which was also the last issue, featured a type-in listing for a rather interesting game. It was called Blindganger, which translates to dud, but was used to mean blind person (ganger literally means go-er, someone who goes).

The cover of Commodore Dossier, issue 17.

The cover of Commodore Dossier, issue 17.

In the aftermath of what one can only imagine must have been a proper night on the town, the blind guy in question somehow wakes up inside the city sewers. It’s the player’s job to guide him back to the streets using only the sounds created by his white cane for guidance.

The city’s wastewater division, being exceptionally keen on sewer hygiene, flushes the sewers every five minutes. Each flush lands our blind guy someplace else inside the sewers, from where he has to restart his search for the exit. On the third flush the game ends. A map of the sewers is displayed, showing the location of the exit as well as the locations visited by the player.

Having typed-in the listing sometime in the fall of 1988, I started the game. It had a sprite animation of a bat flying in front of the full moon on an otherwise completely black screen, which I thought was pretty cool. However, I couldn’t figure out the controls. The sewer map should have helped, of course, but something was wrong with it. The locations marked as visited on the map did not seem to match the path I thought I had taken. Also, sections of wall would often be marked as visited.

An example of a sewer map produced by the original game.

An example of a sewer map produced by the original game.

After a couple of tries I gave up and went on to do other things, but this business with the sewer map and the incomprehensible controls somehow stuck with me. And so, when I came across a scan of this particular issue of Commodore Dossier I knew it was time to sort it out once and for all.

Of course, before I could start, I had to type in the listing again. Back in 1988, I lacked the checksum tool published by Commodore Dossier. Maybe the map of the sewers was messed up just because of my not-quite-touch-typing skills?

A quick Google search turned up a disk image that contained a program called “CHECKSUM DOSSIER”. This sounded promising! To confirm, I ran the program and typed a short line from the Blindganger listing: “240 RETURN”, followed by the checksum “7E”. Then I tried inserting a typo either in the line itself or in the checksum. In both cases, this resulted in the message “FOUT IN REGEL”, or “ERROR IN LINE”, being printed on the screen. Oh yeah!

Ain’t that cute, BUT IT’S WRONG!!

Ain’t that cute, BUT IT’S WRONG!!

I typed in the listing using the checksum tool. Parts of the scan were of rather low quality and here the tool really helped a lot. I often found myself trying variants of some line and its checksum until I found a combination that matched. So far, so good. Now I had a copy of the original that was guaranteed to be free of typos. Surely, this copy would not produce the same sewer maps as the copy I typed in back in 1988. But… it did!

To find out why, I needed to locate the code that displays the map of the sewers. The sewer map is displayed when the game ends, which happens when the player finds the exit or when the sewers are flushed for the third time. Concentrating on the second case, I looked through the BASIC listing of the game for a check on zero or three.

Sure enough, on line 1780, the variable MAAL is compared to zero. This variable is initialized to three on line 1550 and counts down on each flush of the sewers.

1780 MAAL=MAAL-1:IFMAAL=0THENGOTO1810

The block of code on lines 1810 – 1840 is executed if MAAL equals zero. Line 1840 is an endless loop. Looking at the other three lines of BASIC, our next target is the machine code subroutine located at memory address 16540 ($409C).

1810 POKECROSS+4096,1           :REM MARK EXIT ON THE MAP
1820 SYS16540                   :REM COPY MAP TO SCREEN
1830 POKESID+11,0:POKE53248+21,0:REM STOP SOUND AND DISABLE SPRITES
1840 GOTO1840                   :REM ENDLESS LOOP

This subroutine consists of two parts. The first part copies 1000 bytes from the memory region starting a $6000 to screen memory ($0400 – $07E7). This fills the entire screen (40 columns times 25 rows = 1000 bytes) with a map of the sewers.

As it turns out, this part does not contain any bugs. The map of the sewers may look strange, until you notice that all of the blocky bits are walls, ‘@’ marks a horizontal section of pipe, ‘A’ marks a vertical section of pipe, ‘B’ through ‘J’ mark different types of corners and crossings, ‘K’ marks a pit, and ‘L’ marks the exit. These characters are not random. They correspond to screen codes 0 through 12. The map is just a dump to the screen of the internal representation of the sewers used in the game.

The second part of the subroutine copies 1000 bytes from the memory region starting at $7000 to color RAM ($D800 – $DBE7). This marks all visited locations with the color yellow.

 1L40CB:  LDA     #$FF    ; >-- initialization --
 2        STA     $FB     ;                      |
 3        LDA     #$6F    ;                      |
 4        STA     $FC     ;                      |
 5        LDA     #$F4    ;                      |
 6        STA     $FD     ;                      |
 7        LDA     #$D7    ;                      |
 8        STA     $FE     ; <--------------------
 9        LDX     #$04    ; >-- outer loop -----------------
10L40DD:  LDY     #$FA    ; >-- inner loop I ---            |
11L40DF:  LDA     ($FB),Y ;     (copy bytes)    |           |
12        STA     ($FD),Y ;                     |           |
13        DEY             ;                     |           |
14        BNE     L40DF   ; <-------------------            |
15        LDY     #$FA    ; >-- inner loop II -----------   |
16L40E8:  INC     $FB     ;     (update base addresses)  |  |
17        BNE     L40EE   ;                              |  |
18        INC     $FC     ;                              |  |
19L40EE:  INC     $FD     ;                              |  |
20        BNE     L40F4   ;                              |  |
21        INC     $FE     ;                              |  |
22L40F4:  DEY             ;                              |  |
23        BNE     L40E8   ; <----------------------------   |
24        DEX             ;                                 |
25        BNE     L40DD   ; <-------------------------------

The bytes are copied in four blocks of 250 bytes. The outer loop uses the X-register to count from 4 down to 0.

1LDX     #$04
2;
3; inner loop
4;
5DEX
6BNE     L40DD

The first inner loop uses the Y-register to count from #$FA (250) down to 0.

1L40DD:  LDY     #$FA
2L40DF:  LDA     ($FB),Y
3        STA     ($FD),Y
4        DEY
5        BNE     L40DF

Indirect-indexed addressing is used to copy bytes in the inner loop. In this addressing mode, the Y-register is used as an offset that is added to a 16-bit base address stored in zeropage.

For example, the instruction LDA ($FB),Y is executed as follows:

  • Read the litte-endian 16-bit base address stored in zeropage locations $FB and $FC. Little endian byte order means the least significant byte is stored at the lowest address ($FB). $FB is initialized to #$FF and $FC to #$6F, so the 16-bit base address is $6FFF.
  • Add the value of the Y-register to the base address to compute the effective address. The Y-register is initialized to #$FA at the beginning of the loop. Adding to $6FFF yields $70F9 as the effective address.
  • Load a byte from the effective address into the accumulator (A-register).

The Y-register counts down from 250 to 0. Note that the smallest value of Y used as an index is 1. (When the DEY instruction decreases Y to zero, the following BNE branch instruction will fall through, exiting the inner loop.)

The first byte to load is located at $7000. Because the minimal value of Y is 1, the source base address needs to be initialized to $7000 - 1 = $6FFF. Similarly, the first byte to store is located at $D800, so the target base address should be initialized to $D800 - 1 = $D7FF. Instead, it is initialized to $D7F4!

Accidentally typing #$F4 instead of #$FF is not very likely. But in decimal, this corresponds to typing 244 instead of 255. It seems the original author accidentally hit the 4 instead of the 5 while typing the constant 255! This shifts the colors used in the sewer map by 11 positions relative to the map itself. In turn, this means the locations marked as visited on the map are wrong.

The offending byte resides in a data statement on line 3670 of the BASIC listing:

3670 DATA 252,169,244,133,253

After changing it to the correct value of 255, the sewer map displays correctly. And that’s that… bug fixed after 29 years!

To celebrate, I’ve put together a 2017 version of the original game that includes this fix, as well as fixes for several other issues I came across while going over the code:

  • Visited locations are correctly displayed on the sewer map shown at the end of the game.
  • The time penalty for falling down a pit is taken into account when computing the amount of time left.
  • The street noise volume is set such that it depends on the distance to the exit (as intended by the original authors).
  • English translation. (Press and hold the H-key during the game to view the online help.)

Blindganger 2017 is also available as a BASIC listing including checksums. For the authentic 80’s experience, just download and run the Commodore Dossier checksum tool (see link below) and start typing!

In the next episode, we will make some improvements to the (now correct) sewer map to make it easier to understand (see the example below).

X marks the spot!

X marks the spot!

Related links: