The Lost Ways of Programming: Commodore 64 BASIC
by Tomas Petricek, 5 November 2020
@tomaspetricek |
tomas@tomasp.net
In this interactive article, we will build a breakout game using Commodore 64 BASIC in the browser. This is a fun programming hack, but it has quite profound theoretical background. Let me explain.
- I believe that how we interact with a programming environment when programming is more important than the specific programming language that we are using.
- This has never been widely studied and we have interesting things to learn from past systems, including Commodore 64 BASIC.
- We should look at the history and recreate past programming experiences in order to learn from them, following a method that a historian of science, Hasok Chang, calls complementary science.
- Reading about interactions is not enough. To get a sense of how the interaction worked, you need to experience it yourself, at least in a limited form. This is best done with an interactive article.
This is an interactive article that documents some of the interesting aspects of programming Commodore 64 BASIC. I'm not trying to create an accurate Commodore 64 simulator though. The point is to show a few things that we can learn from for future programming systems.
We will start with a Hello World example to see how things work and then we'll build a small Breakout game. This illustrates how easy it is to get started, how the environment supports learning and how the Commmodore 64 BASIC mode of interaction lets us gradually build a program in a way that is quite different from modern programming environments.
Reading this on a phone? Keyboard and a large screen is better for reading this essay, but it works on phone too. The simulator opens whenever you click a button. Commodore 64 screen width is fixed, so it may read better in a landscape mode.
100 Hello World
When Commodore 64 starts, a welcome screen from BASIC awaits you. Even if you want to use it to just play games, you start with a programming environment. This tells you that you too can become a programmer and you certainly do not need to download gigabytes of tools and wait hours for your XCode or Visual Studio to install.
Let's follow the tradition and tell BASIC to say hello world for us. To do this, type the following command, or if you are lazy, just click the button.
PRINT "HELLO WORLD"
To a modern programmer, it is amazing how little it takes to get from booting the machine to printing hello world. Not only you start in the programming environment but you also do not need to write any classes, static methods and compile the program.
The obvious next thing is to print hello world in an infinite loop. Previously, we entered a single command and BASIC executed it, but now we need to create a program. To do this, we prefix code with line numbers:
10 PRINT "HELLO WORLD" 20 GOTO 10
This is again ingenious. BASIC keeps a list of lines of your program and when you type a line starting with number, it inserts it into the right location. You can use the same prompt for running commands and editing your program. To run the program now, just type RUN:
RUN
If you run a program with an infinite loop like this one, you can stop it using Ctrl+C or Command+C. If you want to see the program that you entered, you can type the LIST command.
110 Drawing a maze
There is a lot of clever hacks that you can do in BASIC with a few lines of code. This ease of getting started contributes to what makes it a fun programming environment. If you found an interesting hack in a computer magazine, you could type it into the console and run it straight away.
The fact that you had to copy code from a paper magazine sounds like a hassle, but it has an educational quality. It keeps the samples that can be distributed in this way reasonably small and it makes you think about the code as you are typing it.
To experience this yourself, you should try typing the following three-line program to the console! It generates a famous maze. This relies on special Commodore character codes: 147 clears the screen, 205 and 206 are backslash and slash crossing the full character size.
10 PRINT CHR$(147); 20 PRINT CHR$(205.5 + RND(1)); 30 GOTO 20 RUN
200 Creating a moving ball
To build our Breakout game, we can proceed gradually. This is yet another nice feature of the programming environment. We want to create a ball that bounces off the wall, but let's start with a ball that just moves to the right.
We will do only a tiny bit of planning. Code that initializes variables with the game state starts at line 1000 and code that handles ball movement will start at 2000. We will also first clear the screen and use DELETE to remove all the previous maze and Hello World code.
PRINT CHR$(147); DELETE 1000 REM STATE INITIALIZATION 1010 X=0 2000 REM BALL MOVEMENT 2010 POKE X CHR$(32) 2020 X=X+1 2030 POKE X CHR$(209) 2040 GOTO 2000 RUN
To draw a ball at a specific location, we use POKE which writes a value to a memory location. Here, the part of memory representing a screen starts at offset 0. We first erase the previous ball using a space (character code 32) and then draw a ball (character code 209).
210 Making the ball bounce
To make the ball bounce off the walls, we need to check when it gets to the side of the screen and reverse the direction in which it moves. To do this, we need to keep more state. We were using the variable X to keep the X coordinate. Now we add Y for the Y coordinate and also DX, DY for the direction (+1 or -1). To see our code clearly, let's first clear the screen and LIST our current code.
PRINT CHR$(147); LIST 1020 Y=0 1030 DX=1 1040 DY=0
We immediately follow the edits in initialization code with edits in the ball movement code, starting at line 2000, to check for collisions with the left side and the right side of the screen:
2010 POKE ((Y*40)+X) CHR$(32) 2020 X=X+DX 2030 Y=Y+DY 2040 IF X=40 THEN DX=-1 2050 IF X=40 THEN X=38 2060 IF X<0 THEN DX=1 2070 IF X<0 THEN X=1 2200 POKE ((Y*40)+X) CHR$(209) 2210 GOTO 2000 RUN
We knew that we will need to add checks for the top and the bottom side, so we left some empty lines. The checking code ends at 2070 and ball drawing is on line 2200. We just need to inser the remaining checks:
2080 IF Y=25 THEN DY=-1 2090 IF Y=25 THEN Y=23 2100 IF Y<0 THEN DY=1 2110 IF Y<0 THEN Y=2 RUN
Why is the ball still moving just from left to right and back? We added the checking code, but forgot to change DY. This is easy to fix:
1040 DY=1 RUN
This is what we wanted, a ball that bounces off the walls! But let me reflect on how we created the program. We started with a moving ball and then added bouncing in two steps. The programming model of Commodore 64 BASIC makes this very easy.
This relies on two things. First, as the code is a simple imperative list of commands, the addressing (using line numbers) makes editing the program much easier than if it consisted of complex composed expressions. We had to be clever about line numbers to have space for inserting code, but this is a small price to pay. Second, the dual use of the console, as both an editor and a REPL, means that the interaction is kept quite simple.
There is even more to the console. You can use it to run one off tests,
like PRINT (Y*40)+X
to test your calculation. You can also
modify the state directly and then jump into the middle of a program.
For example, we can move the ball to the right bottom corner and let the
program run from there by jumping over the initialization code:
X=39 Y=24 GOTO 2000
300 Handling keyboard input
Before we can turn the program into an actual game, we need to figure out
how to handle input from keyboard. If you have a string variable K$
,
you can use GET$ K$
to read a key from a buffer. This will return
an empty string if there is no key in the buffer, so we need to write a
loop waiting for a key. We can do this on lines 10-50, before the start of
our actual program:
PRINT CHR$(147); 10 K$="" 20 GET$ K$ 30 IF K$="" THEN GOTO 20 40 PRINT ASC(K$) 50 STOP
After printing the PETSCII code of the character, we explicitly stop the
program, so that it does not continue into our ball bouncing code. Try running
the program now using the RUN
command repeatedly to get key codes
for up and down arrows! The interpreter does not support all keys, but it handles
arrows, space and alphanumeric characters.
We will need the key code for the up key (145) and the down key (17). Now that we know this, we can delete our test code:
DELETE 10-50
310 Moving a paddle
Now that we figured out how to handle input, we can add a paddle to our game. It will be on the left side of the screen and will move using up and down keys. We can write the code independently of all that we have written so far, adding a line 1050 to the initialization code and starting the paddle moving code at 2500.
The logic is quite simple. We read a key and get its code. If the key is up or down,
we increment or decrement the position of the paddle. We then draw the paddle using
POKE
, also drawing a space before and after to erase a previous state.
The emulator does not support FOR
loops, so we just draw 5 vertical
bar characters one after the other.
1050 P=10 2500 REM MOVING A PADDLE 2510 K$="" 2520 K=0 2530 GET$ K$ 2540 IF K$<>"" THEN K=ASC(K$) 2550 IF K=145 THEN P=P-1 2560 IF K=17 THEN P=P+1 2570 POKE ((P-1)*40) CHR$(32) 2571 POKE ((P+0)*40) CHR$(182) 2572 POKE ((P+1)*40) CHR$(182) 2573 POKE ((P+2)*40) CHR$(182) 2574 POKE ((P+3)*40) CHR$(182) 2575 POKE ((P+4)*40) CHR$(182) 2576 POKE ((P+5)*40) CHR$(32) 2580 GOTO 2500
You can run this regardless of whether you followed the previous sections.
If you did, typing RUN
would start the bouncing ball, so
instead, we initialize P manually and jump directly to the
paddle handling code.
P=10 GOTO 2500
To get something running as soon as possible, we did not add any checks to make sure that the paddle does not run outside of the screen. If this happens, the program right now stops and you need to kill it using Ctrl+C. To fix this, we need to draw spaces around the paddle only when it is not at the start/end and ensure that P is between 0 and 20. We were wise enough to use multiples of 10 as our line numbers, so we have space to insert this code between 2560 and 2570.
2570 IF P>0 THEN POKE ((P-1)*40) CHR$(32) 2576 IF P<20 THEN POKE ((P+5)*40) CHR$(32) 2561 IF P<0 THEN P=0 2562 IF P>20 THEN P=20 GOTO 2500
400 Putting everything together
We built two parts of the game largely independently.
The code for the bouncing ball is between lines 2000 and 2500 and we can start it by
typing RUN
or GOTO 2000
. The code for the paddle starts after
that and we can run it using GOTO 2500
. The last step is to connect the
two parts! If you skipped a part of the tutorial, the following lets you reload all the
code.
The whole code is now too long to fit on a screen when you run LIST
.
If you want to see it, you need to run LIST -2500
to see everything before line 2500 or LIST 2500-
to see everything after.
To link the two parts, we need to make a couple of edits. First, we'll clear the screen
at the end of the initialization code and update the bouncing so that the ball keeps
on the right of the paddle. Second, we'll connect the two parts. To do this, we merge
the two loops by removing GOTO 2000
from the end of the ball movement code.
We'll jump to line 2000 only after also updating the paddle on line 2580.
1100 PRINT CHR$(147) 2060 IF X<1 THEN DX=1 2070 IF X<1 THEN X=2 2210 2580 GOTO 2000 RUN
This looks like a game, but we are missing one last crucial bit. We need to add a check to detect when the ball does not hit the paddle while bouncing off the left side of the screen. We insert this between lines 2030 and 2040, i.e just after we update the ball location.
2031 IF (X=0) AND (Y<P) THEN GOTO 3000 2032 IF (X=0) AND (Y>(P+4)) THEN GOTO 3000 3000 STOP RUN
When the ball is below or above the paddle, the code jumps to line 3000 which
uses the STOP
command to terminate the program. To make the game
a bit nicer, we replace this with a nice GAME OVER effect. If you are typing this,
you can also run GOTO 3000
at the end to debug just the game over
code, without having to play the game.
3000 REM GAME OVER 3010 PRINT CHR$(147); 3020 S=0 3030 S=S+1 3040 PRINT " GAME OVER" 3050 IF S<25 THEN GOTO 3030 3060 PRINT CHR$(147) RUN
500 Interacting with BASIC
That's it! If you followed the individual steps of the article,
you now created a simple Breakout game in a bit less than 50 lines
of code. The point of the article was to let you experience how the
process of programming feels, but if you skipped over some parts,
you can cheat and load the whole game below. After doing this, use
RUN
to run it or LIST
to see the full code.
I started this article by suggesting that how we interact with a programming environment is more important than the language that is used. Computer science is very good for talking about programming languages, but it has traditionally said little about interacting.
The point of this article is to point out some of the interesting aspects of interacting with Commodore 64 BASIC. This is a very simple environment, but there are some valuable lessons:
- It is easy to start programming. The system boots into a programming environment, making that the primary way of interacting with the machine. Loading a game from a tape and running it involves the same kind of interactions as programming.
- It is easy to learn. The fact that many programs were available as code listings in paper magazines may be an accidental virtue, but it means that they are short and that you learn as you copy them.
- There is just one kind of interaction. You type commands into a console, but this works both as a way of running commands immediately (REPL) and as a way of constructing a program (editor).
- You get a simple yet flexible workspace. We created two parts of the game independently and then connected them by editing two GOTOs. They are also easy to test independently, because we can set variables in the REPL mode and jump direclty into the middle of our code.
- Hacker access to extra features. I only partly demonstrated this, but
POKE
(andSYS
) give you access to many extra features of the system. The common things are simple, but curious hackers get a reward.
I'm not suggesting we should throw away our TypeScript with Visual Studio Code or Java with Eclipse and go back to Commodore 64 and BASIC. However, modern programming environments, even with REPL or live reloading, can certainly learn a few tricks.
Perhaps the most important point is that the language should be co-designed with the programming interactions. For example, line numbers in BASIC are not just poor man's loops. They enable many of the interesting interactions with the system.
510 Complementary science
This article also tries to make a methodological point. A naive view of science is that it is a progressive enterprise that gradually improves our knowledge bit by bit. As historians and philosophers of science pointed out, actual science is much more complicated.
Pluralism in science means that there can sometimes be multiple accepted, but mutually inconsistent theories, which can still productively exchange ideas. Scientific revolutions mean that sometimes, revolutionary shift in the basic assumptions invalidates past knowledge.
Although we are sometimes reluctant to call computer science a science, we nevertheless believe in the naive idea of its straightforward progressive nature. This view implies that our present knowledge encompasses all the good ideas from the past and forgotten ideas were lost because they were simply not good. This is not true for sciences like physics and much less so for computer science.
What does this mean? If we accept that there might be something to past scientific ideas that were lost, it becomes worthwhile to reconstruct those ideas, bring them into a modern context and see how they can contribute to current developments. Historian of science, Hasok Chang, calls this approach complementary science.
Complementary computer science is exactly what I tried to do in this article. I believe that interesting ideas on how to interact with a programming environment have been lost, largely because programming research became so focused on languages (possibly under the influence of Algol). This article is an attempt to recreate some of the interesting past ideas. My choice of starting with Commodore 64 BASIC is a fairly pragmatic one. There are more interesting past programming systems, but this one was relatively easy to recreate.
Talking about programming interactions is also harder than talking about programming languages, because a language can easily be described formally in a printed paper. To make sense of an interaction, you need to experience it. This is why this article is interactive and why I encourage the reader to play with the environment, at least by copying some of the code.
In other words, we need to change both what we talk about and how we talk about it, because the medium is the message.