Restructuring a 1K ZX-81 BASIC Game
Tony Lyon, a fellow heavily into retro video gaming, tweeted the above image of a listing of a ZX-81 BASIC “Space Invader” (note the singular!) game that fits in 1K. Erico Patricio Monteiro posted it to the Facebook CoCo group, and CoCo users have been looking at it since. I succumbed to temptation, and here we’ll take a shot at restructuring it, ultimately rewriting it in BASIC09.
First, let’s get this in text format. This photo is of a listing from the days of low-res dot matrix printers, so I may make a mistake. I’ll use lower case, and will use strings of question marks as a first cut for the laser cannon, the ship (again, note the singular), and the like. We can fill in something plausible later. For now I will put a tab between line numbers and lines; this is for human understanding, as far as that is possible in street BASIC[1].
10 let a=pi/pi |
Street BASIC, like some other early interactive languages of the time (e.g. JOSS and FOCAL), requires a line number on each and every line. This is evil.
Why? It’s dense camouflage hiding the control flow. Here’s a line of BASIC, with a number. Quick! How many other places in the program end up here? Until you read the entire program, you don’t know. Any other line might branch to it.
So, here’s the program with only the line numbers that are actually used. We’ll insert a blank line before each numbered line.
let a=pi/pi |
Hey, the last couple of lines before line 500 are
if s=1000 then goto 500 |
so they could just as well be the single line
If s<>1000 then goto 80
eliminating any need for the line number 500. Even better, then you could do all that as
if s<>1000 then goto 80
let e$="???"
let d=d+d
goto 80
We see the common idiom of using inkey$ to get a character indicating the player’s intention, but why three times? Surely
let k$=inkey$
if k$="A" then...
etc. would be smaller and faster, and would prevent your keystroke from being read, compared with something other than what you typed, and discarded for the next inkey$ call.
There are many articles telling you how to mangle street BASIC code into unintelligibility to somewhat compensate for the interpreter’s extreme inefficiency. The sort that stands out here is saving constants in variables to avoid the overhead of the interpreter converting it from human-readable to internal format over and over and over… For even more fun, the code takes advantage of this particular BASIC interpreter setting pi to an approximation of pi, so the first two lines set a to 1 and b to 0. You can’t do that in Color BASIC, where uninitialized variables default to 0.
We will ultimately move to BASIC09, which is definitely not “street BASIC”. In particular, code is converted to an internal form as soon as possible. That internal form
so to make the code more understandable for us humans rather than better for a primitive interpreter, we will get rid of these variables that just hold constants, except for h. (We have plans for h.) When the smoke clears…
let c=15 |
There’s a lot of repeated evaluation in those three lines starting with “if y=h”:
if y=h and x=c or y=h and x=c-1 or y=h and x=c+1 then goto 600 |
Thanks to the line number exorcism, we know nobody will change any of these variables and jump into the middle, so we can just as well rewrite it as
if y<>h then goto 130 |
and then put line number 130 back on the start of the inkey$ checking.
A couple of lines after line 80, y is set to a random value that surely won’t exceed h, so why is there that test that bounds y to be no more than h? It’s only modified on line 170. Since that’s the only place it can ever exceed h, let’s move that check after line 170.
Let’s see what we’ve come to.
let c=15 |
Apparently ZX-81 BASIC doesn’t let you run multiple statements together on a line. Color BASIC does, and it even has an ELSE. Let’s sort of switch to Color BASIC (still leaving off unused line numbers for now, and ignoring the PAUSE, which waits a number of “ticks”) and see if things can be made clearer. (And we’ll READ those variables rather than assign them one per line.)
read c,d,h,s,e$ |
That’s a good question for any street BASIC program. The final PRINT statement gives away[2] that s is the score. x and y suggest coordinates in a two-dimensional space, but we need more evidence.
Next clue: the player can move the defensive weapon. The only variable changing depending on inkey$ values is c, and considering the print at statements starting at line 110, I’m guessing the invader is at (y,x) and the defender at (h,c), or perhaps it’s (x, y) and (c, h); maybe ZX-81 flips the coordinates. h is set once and doesn’t change, consistent with Space Invaders, where you can only move your defensive weapon left and right. (Further evidence: code comparing y and h and x and c. It’s a collision check. )
Enough one-letter variable names. Time for BASIC09, because it has capabilities this program desperately needs:
Let’s start with TYPEs. We are dealing with three kinds of things here:
which suggest the following types:
TYPE POINT=x,y:INTEGER |
We’re wimping out a bit on REGION by assuming the minimum coordinates are zero; we really should have a min in it as well. One could also argue about dead. Does the invader really die when it goes low enough, and do projectiles or beams die?
Curiously, the original has some constants actually appearing in code rather than being assigned to variables:
I guess the overhead of converting them wasn’t considered worth avoiding. Good practice these days considers such constants in code “magic numbers” and recommends giving them names to indicate their meaning. Writing street BASIC? Good luck finding a meaningful one- or two-letter variable name for them. In BASIC09? No problem.
So here’s a map from some of the random single-letter variables to what you’ll see below:
TYPEs let you name things consistently, in a way that shows their purpose and what they belong to. In street BASIC, you get to infer that kind of information yourself, unless the author has added comments (but, but… with comments you couldn’t fit it in 1K!) or a separate explanatory text that you hope is kept up to date with the code.
There’s no variable in the original program that corresponds to the dead field in an OBJECT. So how is that information represented in the street BASIC version? It’s represented by where you are in the code. You test the expression that corresponds to it and conditionally branch. In general, the current line number can thus represent an arbitrary amount of implicit state. Good luck keeping it all straight.
This is just refighting the structured programming wars, which structure long ago won. In 1966, Böhm and Jacopini proved that any program can be cast in a structured form, if you’re willing to put up with some additional variables[3]. The dead fields are just that. collisionCheck sets them to TRUE if needed, and the main loop checks them. Back in the day, GOTO fans[4] claimed that setting and testing those added variables was just too inefficient… but I think it’s worth it for clarity. How long does the loop run? Until the defender’s dead.
So here’s an initial cut of… OK, it’s not the initial cut. You’ll see the offsets that appear in BASIC09 listings, so this is code that basic09 actually accepts and can run. Apologies for the funky colors; Code Blocks does nicely for setting source code, but… (I told it it was BASIC, but it got confused with the strings somehow.)
PROCEDURE spaceInvader |
So… we’ve gotten rid of all the line numbers save those on the subroutines. Alas, BASIC09 doesn’t have the TrueBASIC SELECT…CASE that would let us avoid them.
Why the explicit check for an empty string coming back from inkey? Think about it. I claim that SUBSTR(“”, s) will return 1 no matter what string s is. Why? Every character in the empty string can be found, in order, at the beginning of s, because there are no characters in the empty string. Therefore, it gives us a false positive on the ON…GOSUB.
What have we forgotten?
Those, and picking a decent set of characters to press for left, right and fire, we’ll leave as an exercise to the reader.
If you have the urge to extend it, it’s easy to write something like
DIM defender,invader(10):OBJECT
along with adding a variable to track the current number of invaders, code to increase the number of invaders, and code to iterate over them if you want to make life tougher for the player. (Try that in street BASIC.)
[1] “Street BASIC” is how Kemeny and Kurtz, the inventors of BASIC, referred to the primitive BASIC interpreters prevalent on microcomputers of the 1980s, mostly variants of Microsoft’s BASIC interpreter originating with the version for the Altair. I would’ve sworn I saw “gutter BASIC”, but I can’t confirm it, so I’ll stick with “street BASIC”.
[2] “Gives away” are les mots justes. For programs of any significance or size, street BASIC actively conceals the meaning of the code, due to constraints on variable names, lack of reasonable control structures, and the active obfuscation programmers are forced into to make up for the interpreter’s inefficiency.
[3] Knuth points out in "Structured programming with goto statements" that this doesn’t mean much. Any program can be turned into a state machine and written as a loop around a switch statement, encoding everything just like the street BASIC space invader encodes whether the defender is dead. SINO (structured in name only).
[4] Yes, there were some back then.