Writing a PET 2001 Ten-Line Canyon Run Video Game

String exploits and a reasonably paced video game in just 10 lines of Commodore BASIC.

Writing a 10-line canõn run game on the PET 2001
Yee-haw! — A canyon to ride by just 10 lines of BASIC!

In continuation of and as a finale to our mini series on the internal representations of Commodore BASIC (were we previously explored BASIC text and memory partitions, as well as variables in memory), we’ll now put some of our findings to use, especially some possible exploits of string variables.

What we’re going to do, is a classic canyon run game, where a single player rides/flies/drives/navigates a procedurally generated canyon down from the top, trying to progress as far and deeply into the winding depths as possible while avoiding the walls of the canyon. And we’re going to implement it in just 10 lines of BASIC, which puts some serious constraints on our solution. As do some of the intricacies of the PET 2001. Especially, we’ll have to avoid any direct access to the video memory (as in PEEKs and POKEs), meaning, we’ll do it all by print statements. So we’ll have to consult our bag of tricks — and come up with some exploits of the string mechanism.

Spoiler alert:
It will be about fast partial strings and fast FIFO queues, while avoiding garbage collection.

But first, let’s have a look at what we’re going to achieve:

PET 2001 Ten-Line Canyon Run: title screen
PET 2001 Ten-Line Canyon Run: (minimal) title screen.
PET 2001 Ten-Line Canyon Run: start sequence
PET 2001 Ten-Line Canyon Run: start sequence.
PET 2001 Ten-Line Canyon Run: on the run
PET 2001 Ten-Line Canyon Run: on the run.
PET 2001 Ten-Line Canyon Run: Game Over
PET 2001 Ten-Line Canyon Run: crash & Game Over.

Not that terrific, but not that bad, either, as we even managed to squeeze a tiny splash screen into our 10-liner. Also, it performs at an at least half-decent pace, nothing to be ashamed of for an almost full-screen game written entirely in BASIC. At the end of a game, there’s even a score. — Pure luxury, to say the least!

(Yes, an advert.)

A Tale of PETs on Fire and Digital Snow

When the PET 2001 was introduced, it came with slow SRAM for 1K of video memory. At 1MHz this SRAM was that slow that it was impossible to hide the access of the video circuitry and the CPU from one another by separating them by the Phi 1 and Phi 2 phases of the common clock signal. Meaning, most appropriately for a tale of fire and ice, the video RAM was some of a contested territory and the video logic and the CPU were fighting over conflicting signal ramps. If the CPU was accessing the video RAM, this also affected the display, summoning “digital snow” onto the screen, caused by random pixels. (This became somewhat better with a new board revision for the PET 2001 8KN, featuring the full-travel keyboard and 2MHz video RAM, and much better with the 9″ 30xx series, which still shared the same general logic.)

In order to work around this, the original Commodore BASIC printed to the screen during VBLANK (the vertical screen blank interval) only, where the electron beam of the CRT was deactivated anyway. This also meant that any BASIC program had to be temporarily halted until the next VBLANK occured, once the interpreter encountered a PRINT statement. I wasn’t long, until some developers came up with an ingenious work-around for the work-around, which was slowing down their video games so annoyingly: as it was found, it was possible to force the PET into a VBLANK at any time by POKEs to a certain register of one the PET’s interface chips (VIA Port B, bit 5). As soon as the PET was tricked into VBLANK state, it dropped the current frame and internally generated a VBLANK singal, thus temporarily shutting down the video output. And, as a side effect, it also started to evaluate any PRINT statement immediately. The VBLANK signal, however, wasn’t fed to the analog video electronics at all, since a special “master timing” circuitry took care of that, and the CRT was unaffected. And all was good.

Then Commodore introduced a new series of considerably revamped PETs (board revision #3), which featured the 6545 CRTC chip (Cathode Ray Tube Controller), dedicated to generating the video output. Since there was now this versatile video chip, these “Fat40” PETs (featuring 12″ 40 column screens) and any consecutive models lacked the “master timing” electronics, instead interfacing directly with the analog video circuitry. If those encountered the ingenious POKE, the untimely generated VBLANK signal, which now was fed directly to the video circuits, forced the CRT out of sync. Which wasn’t especially appreciated by the video circuitry, which signalled its anger in form of smoke and fire to the unsuspecting human user. As in HALT-AND-CATCH-FIRE, maybe without the HALT part, but possibly involving fire and a damaged machine. For which the ingenious trick now became known as the infamous Killer Poke (“POKE 59458,62”).

The Killer Poke
Halt & Catch Fire on a 12″ CRTC PET — The Killer Poke.
(Dramatized artist’s rendition.)

The moral to this story is, we do not know which board revision our program will run on. It may be an early board, generating a snowy display, whenever we try to access the video RAM directly, or it may be a CRTC revision, where the Killer Poke may cause serious damage. Therefore, we me not use either of these tricks. No premature VBLANK and no PEEKs and POKEs to the video RAM. No shortcuts allowed. — And we’ll have to do it all in memory, in plain BASIC.

Doing It

Now that we know what we want to do, and how not to do it, and why we may not ressort to certain tricks, we’ll have to actually address this canyon run thing. And, given the constraints, we may have to come up with certain tricks of our own, but without messing with the hardware. Which is, were some of the insights into the internal management of BASIC runtime resources, we gathered in the last two episodes, may come to hand. Because, we won’t do it without any tricks at all.

And this is, what we’ll eventually come up with:
(here for a PET 2001 with ROM 2, also known as “new ROM”)

LIST

 0 A$="▒▒▒▒▒▒▒▒":A$=A$+A$+A$+A$:B$=A$:S$
="QQQQQQQQQQQQQQQQQQQQQQQQ"
 1 Z$="ππππππππππππππππππππππππ":FL=48:F
H=49:SL=PEEK(FL):SH=PEEK(FH):B=255
 2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+
7:P=15:R=5:C=3:M=29:M1=M-1:X=19:XM=38
 3 KI=158:KL=151:S=30:ZL=23:W=8:PRINT"T
EN-LINE CANYON RUNQ":PRINT" K: LEFT, L: RI
GHT"
 4 PRINT"QRANY KEY TO CONTINUE":POKEKI,.
:WAITKI,1:POKEKI,.:PRINT"";S$:TI$="0000
00"
 5 P=P+INT(RND(.)*R)-C:P=(P>.)*-P-M1:P=(
P<.)*-P+M:GETK$:POKEKI,.:POKEKL,B
 6 X=X+(K$="K")-(K$="L"):X=(X>.)*-X-XM:X
=(X<.)*-X+XM:POKEAL,P:POKEBL,S-P
 7 PRINTA$;"         ";B$;"S";TAB(X);"♦"
;S$:POKEFL,SL:POKEFH,SH:Z$=RIGHT$(Z$,ZL)
+CHR$(P)
 8 Z=ASC(LEFT$(Z$,1)):D=X-Z:IFZ=BORD>=.A
NDD<=WGOTO5
 9 PRINT"S";TAB(X);"*":PRINT"QQQ";TAB(14
);"R GAME OVER ":PRINT"QSCORE: ";TI:POKE
KI,.:END
READY.
█

As may be observed, the entire program fits into just ten lines (numbered 0..9) and lists successfully. Since the program may be somewhat inaccessible in this densly formatted form, we’ll investigate its part in a more intelligible, pretty printed fashion, where we’ll also use escapes for any special PETSCII screen characters (“{ddd}”, e.g., “{147}” for “”, the control character [CLR HOME]).

The Canyon

Arguably, drawing the canyon is an important part of any canyon game, and we may start with this, as well. Let’s have a look at the very first line:

0 A$="{166}{166}{166}{166}{166}{166}{166}{166}":
  A$=A$+A$+A$+A$:
  B$=A$:
  S$="{17}{17}{17}...{17}" :REM 24×{17}

All, we accomplish here, is defining a few strings. After the second statement, A$ refers to a string literal of 32 (4 × 8) checkerboard characters (“”), which will make the solid parts of our landscape, to the left and to the right of the canyon. And, since there’s a left and a right part, we define a second string variable B$, which points to the same string literal in memory. We’ll see in a moment, how this works.

S$ is another string variable, consisting of 24 [CRS DOWN] control characters, enough to travers the screen from its home position at the top left corner down to the very bottom line. (We’ll have to print the player at the very top, while we’ll have to add any new lines for the canyon at the very bottom, in order to have the screen scrolling up. Meaning, we’ll have to have some means of switching between those print positions.)

Time, to come up with the principal layout:

 0     wall         canyon            wall         38
|<——————————————><—————————><—————————————–—————————> |
|XXXXXXXXXXXXXXXX           XXXXXXXXXXXXXXXXXXXXXXXXX |
| A$, length = P      9      B$, length = 39 - 9 - P  |

P gives the horizontal position of the canyon, which is 9 spaces wide. A$ and B$ provide the padding on either side. The total width of a line is 39 characters in order to avoid the automatic extension of the logical line into a new physical one by printing at the 40th column, which would cause a scroll by two physical screen lines at once on consecutive print statements.

As an attentive reader may have assumed already, we’ll print this by partial strings of A$ and B$, separated by a sequence of spaces.

However, we won’t use any string operations like “LEFT$(A$,P)” for this, since, as we’ve seen in the previous episodes, the intermediate products of these operations would pile up pretty soon, as every of those commands generates a new entry in the string storage, to be exact, 30 characters for every frame. Chances are, our user may be running sooner into the much feared Garbage Collection than into a canyon wall, especially on a machine with just 8K of RAM.

Last time, we learned that there’s a simple and straight forward way to manipulate the length of an existing string: The length is stored as a binary value in the third byte of a string variable. And it’s easy to find out, where these string variables are stored: as A$ and B$ are the first two variables defined, they must be the first ones allocated, as well. Variable allocation starts just after the end of the stored BASIC text, which is the value in system pointer VARTAB. (For ROM 2, this is address 42 and 43, in LO, HI order.) The rest is as easy as a single POKE to adjust the length, no function call and no string copy involved. — You probably can’t do faster than this!

2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+7

Here, we read the address stored in pointer VARTAB (at memory location 43 and 42). AL is the address of the byte holding the string length of the very first variable (A$), and, since each variable occupies exactly 7 bytes in memory, the length of B$ must be stored at an offset of 7 bytes from this, which is the address now in BL.

Let’s define a few constants: Say, our canyon starts at position 15 (P), the max extend of a line is 39, and the width (W) of the canyon is 9. Since we’ll have an expression like “39-9” in every drawing command, we’ll may define this slice as a constant (S=30), as well.

0 A$="{166}{166}{166}{166}{166}{166}{166}{166}":
  A$=A$+A$+A$+A$:
  B$=A$:
  S$="{17}{17}{17}...{17}" :REM 24×{17}

2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+7:P=15

3 S=30

6 POKE AL,P:POKE BL,S-P

7 PRINT A$;"         ";B$:GOTO 7

This will print a vertical canyon as fast as we can: line 6 adjusts the width of the two border walls and line 7 finally prints the line over and over again in an endless loop.

▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
…

This is certainly nice, while it could use a bit of variation. Maybe, using “RND()”… But, we’ll have to keep this in bounds. We’ll want the position P to be at least 1, in order to have at least some wall for the player to run into. Just the same, we’re going the limit the right most position to 38-9 = 29.

But, here, we encounter a typical 10-liner issue: Checking and adjusting boundaries is usally done by conditional statements. However, since there’s no “ELSE” in Commodore BASIC, this would also mean to finish the line, and we’d need two lines to check a single state. This way, we’d run out of lines very soon.

Math to the rescue!

Considering that we can use the result of a comparison in an arithmetic expression, where false is 0 and true is -1, we may come up with some idioms:

REM CHECKING THE LOWER BOUNDARY, P >= 0
P=(P>0)*-P

REM CHECKING THE LOWER BOUNDARY, P >= N
P=P-N:P=(P>0)*-P+N

REM CHECKING THE UPPER BOUNDARY, P <= M
P=P-M:P=(P<0)*-P+M

REM CHECKING BOTH BOUNDARIES, N >= P <= M
P=P-N:P=(P>0)*-P+N-M:P=(P<0)*-P+M

ADDING RANDOM (-2..+2) TO P, KEEPING IT IN BOUNDS 1..M
P=P+INT(RND(0)*5)-2-1:P=(P>0)*-P+1-29:P=(P<0)*-P+29

OPTIMIZING FOR SPEED (0 = .) AND USING CONSTANTS:
R=5:C=3:M=29:M1=M-1
P=P+INT(RND(.)*R)-C:P=(P>.)*-P-M1:P=(P<.)*-P+M

Constants are always faster than parsing numeric literals to numbers!
As is using “.” for zero.

Thanks to this, we may come up with this version, which adds some variety to the canyon (mind that there are no numeric literals to be evaluated in the high speed part, lines 5 to 7):

0 A$="{166}{166}{166}{166}{166}{166}{166}{166}":
  A$=A$+A$+A$+A$:B$=A$:
  S$="{17}{17}{17}...{17}" :REM 24×{17}
2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+7:P=15
3 S=30:R=5:C=3:M=29:M1=M-1
5 P=P+INT(RND(.)*R)-C:P=(P>.)*-P-M1:P=(P<.)*-P+M
6 POKE AL,P:POKE BL,S-P
7 PRINT A$;"         ";B$:GOTO 5
PET 2001 Ten-Line Canyon Run: the canyon
Drawing the canyon.

Adding the Player

Adding the player is much like what we’ve already achieved. It involves keeping the position (X) in bounds, as in 0 ≤ X ≥ 38, with up to 38 columns to the left of the single player character. However, the position will be derived from user input, here the keyboard, using keys [K] (left) and [L] (right).

Again, we’ll have to check something, here the keypresses, and, again, we’ll use BASIC’s boolean representations of true and false (-1 and 0, respectively) in arithmetic context.

X=15:XM=38

GETK$
X=X+(K$="K")-(K$="L")
X=(X>0)*-X-XM:X=(X<0)*-X+XM

(…)

PRINT "{19}";TAB(X);"{218}";S$

Code 218 () is used to represent the player. “{19}” is the control character for [HOME], which sets the cursor to the top-left origin of the screen, function “TAB(X)” skips X screen columns without overprinting. Finally, we put the string variable S$, we've seen defined already, to use and travers down to the bottom of the screen in order to print the next line of the scrolling canyon in the next frame.

But there’s more to this. The PET 2001 lacks keyboard repeat, it even inhibits it on design! Therefore, a player would have to press the keys repeatedly to move — and you may imagine, what fun this might be on the original PET’s chiclet keyboard! Do not dispair, as help is near. On closer inspection, the PET prevents repeated presses of the same key in consecutive scan cycles by storing the last key code pressed. Value 255 is used for no key pressed at all. The location for this is 151 for ROM 2 and we’ll store this address in variable “KL”. Another trick is resetting the keyboard buffer in order to get the latest input, there is, instead of a stale, previously queued one. The index of the keyboard buffer is at location 158 (again, ROM 2) and we’ll store this address in variable “KI”.

Thus, our keyboard read sequence becomes:

KI=158:KL=151
GET K$:POKE KI,0:POKE KL,255

Putting the parts together, we arrive already at what pretty much resembles a game:

0 A$="{166}{166}{166}{166}{166}{166}{166}{166}":
  A$=A$+A$+A$+A$:B$=A$:
  S$="{17}{17}{17}...{17}" :REM 24×{17}
2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+7:P=15:X=15
3 B=255:KI=158:KL=151:S=30:R=5:C=2:M=29:M1=M-1:XM=38
5 P=P+INT(RND(.)*R)-C:P=(P>.)*-P-M1:P=(P<.)*-P+M:GET K$:POKE KI,.:POKE KL,B
6 X=(X>.)*-X-XM:X=(X<.)*-X+XM:POKE AL,P:POKE BL,S-P
7 PRINT A$;"         ";B$;"{19}";TAB(X);"{218}";S$:GOTO 5
PET 2001 Ten-Line Canyon Run: canyon and player
Canyon and player — almost a game.

Collision Detection and Fast Queues

By now, we are missing, apart from some nice to have but not essential things (like a nice start sequence were we drop into the canyon) collision detection to make this a true game. This should be rather easy, just compare X and P. However, X represents a position on the very top of the screen and P a position at the very bottom. And a newly added P will take some 20-odd frames in order to propagate to the top. We need some kind of a queue, a FIFO buffer (First In – First Out) or pipe.

However, if you have ever done some programming in 8-bit BASIC, you know what shifting the values of a queue by a single position takes: eons! Kids are born, go to school and grow up to eventually enter their first job, until our loop finishes.

10 DIM A(24)
…
50 FOR I=0 TO 23:A(I)=A(I+1):NEXT
60 A(24)=P
…

A loop like this at the bottom of every frame puts much of an end to the idea of a half-decent video game. It’s simply not feasible. — What could we do about it? If we just could trick BASIC into doing the job for us! Copying bytes is a rather trivial and quick affair in machine language and could be done in what is just an instance in comparison to a BASIC loop. Are there any built-in functions, which do this kind of job, which we could exploit?

As any seasoned C programmer knows, bytes and characters are quite the same (at leats, they used to be, at least, in an 8-bit locale). Our values for P range from 1 to 29 and fit well into a byte. So, what about characters, a sequence of characters, a function, which skips the very left part of a sequence, of a string?

Could we use “RIGHT$()” for this?

Hold on, didn’t we say, we wanted to avoid these string functions at all costs, since they would run us inevitably into a garbage collection, putting the game on hold for what may be seconds? Is there a way around this?

Let’s recap what we know about string allocation:

So, will resetting FRETOP leave the variables unaffected, as long as we do not overwrite the string literal, to which the variable refers?

Compare the sequence “BCDEFFBCDE”:
There are two blocks of 5 bytes each. Five bytes at the end, which will be overwritten by new partial strings in the course of any further iterations of the process. And there are another five bytes at the front, which will both provide the source for those operations and will eventually receive the final, newly assembled string. At no point in the process, we’re in danger of overwriting any characters which currently serve as a source for string copying!

Let’s put this to a test:

10 SL=PEEK(48):SH=PEEK(49) :REM STORE FRETOP (Lo,Hi)
20 A$="ABCDE"
30 FOR C=70 TO 80          :REM ASCII "F".."P"
40 POKE 48,SL:POKE 49,SH   :REM RESET FRETOP
50 A$=RIGHT$(A$,4)+CHR$(C)
65 PRINT A$
70 NEXT

RUN
BCDEF
CDEFG
DEFGH
EFGHI
FGHIJ
GHIJK
HIJKL
IJKLM
JKLMN
KLMNO
LMNOP

READY.
█

1FF0: AA AA AA AA AA AA 4C 4D  ......LM
1FF8: 4E 4F 50 50 4C 4D 4E 4F  NOPPLMNO

Looks good, string allocation and string references are actually independent mechanisms and there’s no interference at all!

Let’s have closer look at the strings in motion:

#0  (A$ embedded in BASIC text)

A$="ABCDE"

1FF6:  . . . . . . . . . .
                          ↑
                        FRETOP



#1               RIGHT$(A$,4)
                      |
                      |
                      V
1FF6:  . . . . . . B C D EFRETOP <––



#2            CHR$(70)
                 |
                 |
                 V
1FF6:  . . . . . F B C D EFRETOP <––



#3           A$= +
           +–––––+––––+
    A$     |     |    |
      \    V     |    |
1FF6:  B C D E F F B C D EFRETOP <––



#4  Reset FRETOP (next iteration)

    A$
      \
1FF6:  B C D E F F B C D E
                          ↑
                    ––> FRETOP



#5         RIGHT$(A$,4)
           +––––––––––+
    A$     |          |
      \    |          V
1FF6:  B C D E F F C D E FFRETOP <––



#6            CHR$(71)
                 |
    A$           |
      \          V
1FF6:  B C D E F G C D E FFRETOP <––



#7           A$= +
           +–––––+––––+
    A$     |     |    |
      \    V     |    |
1FF6:  C D E F G G C D E FFRETOP <––



#8  Reset FRETOP (next iteration)…

    A$
      \
1FF6:  C D E F G G C D E F
                          ↑
                    ––> FRETOP

As we may see, at no point in this sequence the source characters for any of the copy operations involved are touched or overwritten, even when A$ is temporarily located in the freely allocatable memory space. By this, we meet the preconditions for a stable queue.

Seems, as soon as we’re done with any vital string definitions and assignments, we may indeed take note of the address, FRETOP is currently pointing to, and effectively freeze string allocation temparily for sake of a fast FIFO queue. That is, as long we adhere to the rules of never overwriting any source characters at any time. Otherwise, we’d end up with scrambled garbage. (Hoping for the collection, which will never come.)

By this, we come up with the following mechanism:

REM PREFILL QUEUE Z$ WITH PI (PETSCII CODE 255)
Z$="ππππππππππππππππππππππππ"
REM TAKE NOTE OF CONTENTS OF FRETOP
FL=48:FH=49:SL=PEEK(FL):SH=PEEK(FH)
…
REM RESET FRETOP
POKE FL,SL:POKE FH,SH
REM SHIFT FIRST BYTE/CHARACTER OUT, ADD NEW ONE
Z$=RIGHT$(Z$,23)+CHR$(P)
REM RETRIEVE FIRST BYTE
Z=ASC(LEFT$(Z$,1))
REM COMPARE PLAYER POS X AND RETRIEVED BYTE, 0 <= D <= W (W=8)
REM Z=B=255 → NO POSITION YET (START SEQUENCE), NO COLLISION
D=X-Z:IF Z=B OR D>=0 AND D<=W GOTO 5
REM COLLISION! ADD END OF GAME CODE HERE.

Why should this be of any importance?

Well, glad you asked. Without this trick, we’d run out of memory in less than 150 frames, since there’s a maximum of 7,167 user addressable bytes available on a 8K PET and we’d allocate 48 bytes to manage a queue of 24 bytes length each frame. (7167/48=149.3125) Meaning, provided we also use some of the memory for minor stuff, like the program and keeping state, without this, we’d run into garbage collection in less than a minute, even at a slow frame rate in the single digit numbers.

Having thus solved our timing problem by an essential abuse of the string mechanism, we now go on to squeeze in a bit of extras. As there are, a tiny start screen with minimal instructions, and a “GAME OVER” sequence, which also gives us a score. The score is simply the number of ticks in the jiffy clock, which advances once per screen update (60Hz). For this, we set BASIC’s system variable TI$ to “000000”, when we leave the start screen, and read out the number of ticks from variable TI (as in “TIME”).

Maybe, there’s a last trick to report, but nothing out of the ordinary. As we wait for the user to press any key, we do this by resetting the index of the keyboard buffer to zero (by this essentially clearing the buffer from any previously queued key strokes) and wait for it to advance to 1, as in “WAIT KI,1”, and reset it again, thus ignoring the key stroke otherwise.

KI=158
…
POKE KI,0:WAIT KI,1:POKE KI,0

And here’s our program in its entirety (and with escaped PETSCII codes):

0 A$="{166}{166}{166}{166}{166}{166}{166}{166}":A$=A$+A$+A$+A$:B$=A$:S$="{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}{17}"
1 Z$="ππππππππππππππππππππππππ":FL=48:FH=49:SL=PEEK(FL):SH=PEEK(FH):B=255
2 V=PEEK(43)*256+PEEK(42):AL=V+2:BL=AL+7:P=15:R=5:C=3:M=29:M1=M-1:X=19:XM=38
3 KI=158:KL=151:S=30:ZL=23:W=8:PRINT"{147}TEN-LINE CANYON RUN{17}":PRINT" K: LEFT, L: RIGHT"
4 PRINT"{17}{18}ANY KEY TO CONTINUE":POKEKI,.:WAITKI,1:POKEKI,.:PRINT"{147}";S$:TI$="000000"
5 P=P+INT(RND(.)*R)-C:P=(P>.)*-P-M1:P=(P<.)*-P+M:GETK$:POKEKI,.:POKEKL,B
6 X=X+(K$="K")-(K$="L"):X=(X>.)*-X-XM:X=(X<.)*-X+XM:POKEAL,P:POKEBL,S-P
7 PRINTA$;"         ";B$;"{19}";TAB(X);"{218}";S$:POKEFL,SL:POKEFH,SH:Z$=RIGHT$(Z$,ZL)+CHR$(P)
8 Z=ASC(LEFT$(Z$,1)):D=X-Z:IFZ=BORD>=.ANDD<=WGOTO5
9 PRINT"{19}";TAB(X);"*":PRINT"{17}{17}{17}";TAB(14);"{18} GAME OVER ":PRINT"{17}SCORE: ";TI:POKEKI,.:END
PET 2001 Ten-Line Canyon Run: screen listing
Screen listing of the final code.

Did I mention that the emulator now supports PETSCII escapes as in {ddd} or {$hh} in BASIC source files? Meaning, you can copy and paste the above listing into a text file, save it with a .txt or .bas extension, and simply drag&drop it onto the emulator.

Compatibility

As may have become apparent by now, there are at least 2 major versions of ROMs for the PET 2001, ROM 1 (“old ROM”) and ROM 2 (“new ROM”). So far, we’ve coded for ROM 2 only, which is also the by far more common configuration. Now that we have jumped through all those hoops in order to support all versions of the PET 2001, we’ve run out of space. Bummer. After adding the tiny bits of chrome, we’ve made use of all of the available line numbers already. Our 10-liner is complete.

There are well proven ways to auto detect the ROM version, e.g., by peeking into the ROM for the first character which will be printed onto the screen for the startup message. If it’s an asterisk (*), it’s ROM 1, if it’s a pound mark (#), it’s ROM 2. Another way to do this, may be checking well known addresses, like the IRQ vector. A proven way is the following idiom:
IF PEEK(50003) THEN ROM = 2 : REM NEW ROM

Here are the system addresses, which we’d have to change in oder to make this work with the older ROM or on the C64:

var   ROM 2   ROM 1    C64     comment

FL      48     130      51     FRETOP (Lo-Byte), bottom end of string storage
FH      49     131      52     FRETOP (Hi-Byte)
        42     124      45     VARTAB (Lo-Byte), start of variable storage
        43     125      46     VARTAB (Ho-Byte)  (see PEEKs at line 2)
KI     158     525     198     index of keyboard buffer
KL     151     547     197     last keypress (PET: 255 = none, C64: 0 = none)

All of these values are found in about the same region of the code, in lines 1–3.

The VIC-20 uses the same addresses and values as the C64. But here, for the VIC-20, we’d have to adjust the program for the screen dimensions as well, which isn’t as easy as just changing a few addresses.

The program is available at the program library of the PET 2001 online emulator, both for download and to run it online.

Here’s a direct link (PET 2001, ROM 2):

— That’s all, folks! —