The Let's Play Archive

Compute!'s Gazette

by Chokes McGee

Part 36: Hex Wars: An autopsy (Part 1)

Hex War: An Autopsy: Part 1

So, not too long ago, I posted this, talking about some changes I was making to Hex War:

FredMSloniker posted:

code:
51 P0=4:P1=6:P2=13:P3=7:REM DEFAULT ARMY COLORS
52 PC$="{black}{white}{red}{cyan}{purple}{green}{blue}{yellow}
   {orange}{brown}{lt. red}{gray1}{gray2}{lt. green}{lt. blue}{gray3}"
53 C0$=MID$(PC$,P0+1,1):C1$=MID$(PC$,P1+1,1)
54 C2$=MID$(PC$,P2+1,1):C3$=MID$(PC$,P3+1,1)
55 DATA"BLACK","WHITE","RED","CYAN","PURPLE","GREEN","BLUE","YELLOW"
56 DATA"ORANGE","BROWN","PINK","DK.GRAY","MD.GRAY","LIME","INDIGO","LT.GRAY"
57 DIMPC$(15):FORZZ=0TO15:READPC$(ZZ):NEXT
58 N0$=PC$(P0):N1$=PC$(P1)

To which Grimwit replied:

Grimwit posted:

Uh.....

What?



Oh. Magic.

Why didn't you just say "Magic?"

Commodore 64 BASIC is a bit arcane, as ManxomeBromide touched on. And because of various constraints like a desire to minimize typing, the code for Hex War is nigh-impossible to understand for someone unfamiliar with the lingo. (Indeed, I'm learning what it does on the go and using various on-line references to remind myself of how the C64 works.) So let's take a break from improving the program to study how it works as published. Any bolded headers are general discussion of how the machine works; I'll then use italic headers to indicate I'm talking about Hex War in particular.

The function of the Hex War loader has already been discussed, so let's jump right into Hex War proper. Two important things to note. One, the code is in the C64List format, not the Compute! format. Two, I've inserted a ton of spaces to improve readability; in practice, you wouldn't use them because each C64 BASIC line has an 80-character limit. (Technically the BASIC interpreter can handle longer lines, but you can't type in anything over 80 characters.)

We start the program off with this:
code:
0 POKE 53269, 0: PRINT "{clear}": XC = 14: YC = 12: GOSUB 1: 
  PRINT "PLEASE WAIT": GOTO 9
The Commodore 64 Memory Map

The Commodore 64 has a 16-bit address space. That means that it uses 16-digit binary numbers to refer to memory locations. In 16 digits, a binary number can range from 0 to (2^16)-1, or 65535. That means the maximum number of memory locations a 16-bit system can access is 64 kilobytes (hence the name Commodore 64.) You know how 32-bit Windows can only use 4 gigs of memory? Same deal. In 32 bits of binary, you can only count up to 4 gigs.

So the Commodore 64 has 64k of memory, right? Well, not exactly. Technically it does, but many of those memory addresses are overlaid. The BASIC interpreter, for instance, lives in ROM that appears in memory starting at $A000 (that's hexidecimal for 40960). While you can turn off that overlay to use the RAM underneath, you can only do so if you're not planning on using BASIC. (There's a trick you can pull, though: you can copy the ROM into RAM, then make changes to the RAM, to change the way BASIC works.)

Another important overlay is the I/O area, which starts at $D000 (53248). You could turn off all the overlays and have nearly 64k to play with (some bytes at the very start of the address space are reserved for important things like, say, the status of the serial bus). However, if you do so, you have no way for the program to communicate with the user, or vica versa. The I/O area is where that happens.

How does it work? It's simple enough, even if the details require memorization and/or a reference guide. If you want to get some information from the outside world, your program reads a memory address from the I/O area. Instead of reading RAM, though, it's reading output from one of the C64's I/O chips. For instance, if you want to know what the user is doing with a joystick, you read the value in 56320 (for a joystick in port 2) or 56321 (for a joystick in port 1). The bits of the number you get tell you what's going on; for instance, if bit 4 is 0, that means the joystick's fire button is being pressed.

On the flipside, if the program wants to affect the outside world, it does so by placing a value in one of those overlaid memory addresses. For instance, if you want to change the screen's background color, you put a number corresponding to the color you want into memory address 53281, which is actually input to the C64's VIC-II chip, which does the graphics.

Commodore BASIC programs that want to do anything beyond the limited capacities of the language itself involve a lot of POKEing (putting values into memory) and PEEKing (taking values out of memory) to do so. Sometimes this is interacting with the I/O area or some of the BASIC interpreter's internal variables (the loader program, for instance, tells BASIC to start storing programs higher in memory to make room for the custom font). Other times it's loading a machine language program into memory. (Thankfully, Hex War doesn't do that, so we don't have to get into that kettle of worms today.)

If you want to really dig into the nitty gritty of the C64 memory map, this is a reference I've been using a lot.

Hex War Line 0

The first statement in line 0 is POKE 53269, 0. 53269 controls which of the Commodore 64's 8 sprites are visible. (I'll go into more detail on sprites later. Short version: they're graphics objects that can be moved around without affecting the rest of the screen.) Each bit controls one of the eight sprites; setting this value to 0 makes all of the sprites invisible. (This is true by default, so this statement is just a precaution in case you've been doing other stuff before running the program.)

Next, we have PRINT "{clear}". As has been discussed before, we don't actually type {clear} here. Instead, we type Shift-Home, which, when inside a string, prints a 'clear screen' character. When the program is run, that character is printed, which clears the screen and resets the cursor to the upper left corner. (That's why Commodore BASIC, unlike some other BASICs, doesn't have an explicit 'clear the screen' command.)

Quote Mode

So what happens if you type Shift-Home outside of a string? It clears the screen and resets the cursor immediately, that's what. But how does Commodore BASIC know when to clear the screen and when to create the symbol?

That's what Quote Mode is for. Every time you type a double quote, Quote Mode is toggled. When it's on, key sequences that would usually clear the screen or change the cursor color or move the cursor instead create the symbol representing that action. You turn Quote Mode off by typing another double quote or by pressing Enter, which clears Quote Mode.

Note that Commodore BASIC doesn't understand when you're typing in a string or not; it relies on the Quote Mode flag to know what to do. That means that, if you're in the middle of typing a string and want to move the cursor, you have to type another quote, go do what you want to do, go back to where you were, type another quote to turn Quote Mode back on, then delete the quotes you typed and continue. This is the sort of thing that's easy to screw up either way.

Oh, Quote Mode is engaged in one more way. If you press Insert, a space is inserted in the line you're typing. You can press it multiple times to insert more spaces. Then, for the number of times you pressed Insert, your keystrokes will be in Quote Mode. So if you missed inserting a clear symbol, you can move to the affected spot, insert one space, then type the clear symbol. If you press Shift-Home again, though, the screen will be cleared.

Hex War Line 0, continued

The next three statements in line 0 are XC = 14: YC = 12: GOSUB 1. The first two of those set variables. The default variable type is a floating-point number, but you can get strings by appending a $ to the variable name, integers by appending a % to the variable name, and arrays by appending one or more numbers bracketed by parentheses after the variable name. (You can also have string arrays and integer arrays; for instance, A$(3, 4) is a two-dimensional string array.) Note that A, A$, A%, A(3), A$(3), and A%(3) are all different variables. Note also that you can't have more than one array with the same name and type, even if they would have different dimensions.

So why does the program set those variables? Well, that's where GOSUB 1 comes in. The GOSUB command tells Commodore BASIC to go to a line number and continue running the program until it hits a RETURN command, at which point it will jump back to the GOSUB command and continue running immediately after it. So what's on line 1?
code:
1 POKE 781, YC: POKE 782, XC: POKE 783, 0: SYS 65520: RETURN
I mentioned that Hex War doesn't use any machine language. That's not quite true. What I meant is that it does't create any machine language. It does, however, call existing machine language code, and fairly frequently, as this subroutine gets used a lot. How does it work?

Addresses 780-783 are reserved by Commodore BASIC for use with the SYS statement. The CPU has a few registers (internal variables) that aren't mapped to memory addresses, and when we use the SYS command, the numbers in these addresses are copied to those registers. When control returns to BASIC, the values of those registers are copied back to the addresses. Those three POKE commands copy YC and XC into the (ironically) X and Y registers and 0 into the status register.

SYS 65520 tells the computer that we want to run the machine-language code starting at 65520. That area is overlaid by the KERNAL ROM, which is where a bunch of low-level programs are stored. (The memory map I linked to earlier doesn't go into detail on this area; for that, you need to look at this page on the C64 wiki.)

65520 in particular is the location of the PLOT kernal routine; it either gets or sets the location of the cursor. With the status register set to 0, that tells the routine to set the cursor location, using the X register for the row (what we think of as the Y coordinate) and the Y register for the column (what we think of as the X coordinate). If we set the status register to 1 instead, the cursor's current location would be loaded into the X and Y registers.

Once control is returned back to BASIC, the RETURN command puts us back on line 0, where the next command is PRINT "PLEASE WAIT". This prints that text to the screen starting at, you guessed it, row 12, column 14. (Rows and columns are numbered starting at 0.) Finally, the GOTO 9 command jumps over the lines between 0 and 9. Among other reasons, this is important because, if we didn't, we'd go straight to line 1, set the cursor position again, then attempt to RETURN - but since it hasn't been called by GOSUB, the program would crash with a ?RETURN WITHOUT GOSUB error.

So what's the tl;dr on all this? Surprisingly simple. Line 1 is a subroutine we can call to set the cursor position. Line 0 turns off any sprites, clears the screen, prints the message PLEASE WAIT in the center of the screen, then jumps to line 9.

Which we will cover in the next update, because holy crap that was a lot of talking for just two lines of code.