Sending Modern Languages Back to 1980s Game Programmers

Take a moment away from the world of Ruby, Python, and JavaScript to consider some of the more audacious archaeological relics of computing: the tens of thousands of commercial products written entirely in assembly language.

That's every game ever written for the Atari 2600. Almost all Apple II games and applications, save early cruft written in BASIC (which was itself written in assembly). VisiCalc. Almost all Atari 800 and Commodore 64 and Sinclair Spectrum games and applications. Every NES cartridge. Almost every arcade coin-op from the 1970s until the early 1990s, including elaborate 16-bit affairs like Smash TV, Total Carnage, and NBA Jam (in case you were only thinking of "tiny sprite on black background" games like Pac-Man). Almost all games for the SNES and SEGA Genesis. And I'm completely ignoring an entire mainframe era that came earlier.

(It's also interesting to look at 8-bit era software that wasn't written in assembly language. A large portion of SunDog: Frozen Legacy for the Apple II was written in interpreted Pascal. The HomePak integrated suite of applications for the Atari 8-bit computers was written in a slick language called Action!. The 1984 coin-op Marble Madness was one of the few games of the time written in C, and that allowed it to easily be ported to the Amiga and later the Genesis. A handful of other arcade games used Bliss.)

Back in 1994, I worked on a SNES game that was 100,000+ lines of 65816 assembly language. Oh yeah, no debugger either. It sounds extreme, almost unthinkable, but there weren't good options at the time. You use what you have to. So many guitar players do what looks completely impossible to me, but there's no shortage of people willing to take the time to play like that. Assembly language is pretty straightforward, provided you practice a lot and don't waste time dwelling on its supposed difficulty.

If you want really extreme there were people hand assembling Commodore 64 code and even people writing Apple II games entirely in the machine language monitor (a friend who clued me into this said you can look at the disassembled code and see how functions are aligned to 256 byte pages, so they can be modified without having to shift around the rest of the program).

It's an interesting exercise to consider what it would have been like to write games for these old, limited systems, but given modern hardware and a knowledge of modern tools: Perl, Erlang, Python, etc. No way would I have tried to write a Commodore 64 game in Haskell or Ruby, but having the languages available and on more powerful hardware would have changed everything. Here's what I'd do.

Write my own assembler. This sounded so difficult back then, but that's because parsing and symbol table management were big efforts in 6502 code. Getting it fast would have taken extra time, too. But now writing a cross assembler in Erlang (or even Perl) is a weekend project at best. A couple hundreds lines of code.

Write my own emulator. I don't mean a true, performance-oriented emulator for running old code on a modern PC. I mean a simple interpreter for stepping through code and making sure it works without having real crashes on the target hardware. Again, this would be a quick project. It's what functional languages are designed to do. More than just executing code, I'd want to make queries about which registers a routine changes, get a list of all memory addresses read or modified by a function, count up the cycles in any stretch of code. This is all trivially easy, but it was so out of my realm as a self-taught game author. (And for development tool authors of the time, too. I never saw features like these.)

Write my own optimizers for tricky cases. The whole point of assembly is to have control, but some optimizations are too ugly to do by hand. A good example is realizing that the carry flag is always set when a jump instruction occurs, so the jump (3 bytes) can be replaced with a conditional branch (2 bytes).

Write my own custom language. I used to worship at the altar of low-level optimization, but all of that optimization was more or less pattern recognition or brute force shuffling of code to minimize instructions. I still, even today, cringe at the output I see from most compilers (I have learned that it's best not to look), because generating perfect machine code from a complex language is a tough problem. But given a simple processor like the 6502, 6089, or Z80, I think it would not only be possible to automate all of my old tricks, but to go beyond them into the realm of "too scary to mess with" optimizations. Self-modifying code is a good example. For some types of loops you can't beat stuffing constants into the compare instructions. Doing this by hand...ugh.

Much of the doability of these options comes from the simplicity of 8-bit systems. Have you ever looked into the details of the COFF or Preferred Executable format? Compare the pages and pages of arcana to the six byte header of an Atari executable. Or look at the 6502 instruction set summarized on a single sheet of paper, compared with the two volume set of manuals from Intel for the x86 instructions. But a big part of it also comes from modern programming languages and how pleasant they make approaching problems that would have previously been full-on projects.