Category Archives: Commodore

Revisiting 10 PRINT RACER

Awhile back I ported 8-Bit Show and Tell‘s “10 PRINT RACER” from Commodore PET to CoCo. I tried to make it a literal port, keeping the code as close as I could to the original. I did, however, mention a few things that could make it faster, taking advantage of things like Extended Color BASIC’s hex values (&H2 is faster to parse than 2, for instance).

The other day, MiaM left a comment on the original article:

It might be faster to use A=ASC(INKEY$) and IF A=4 instead of IF A$=CHR$(4)

– MiaM

Intriguing. The original Commodore version, the direction was read by using GET A$, and I simply converted that over to A$=INKEY$ for Color BASIC. Here is a look at Robin’s Commodore PET original:

1 REM 10 PRINT RACER: 8-BIT SHOW & TELL
5 R$="":PRINT"{CLR}INIT:";:FORX=1TO75:M$=CHR$(205.5+RND(.)):R$=R$+M$:PRINTM$;:NEXT
10 PRINT"{CLR}":C=20:R=13:W=15:D=0:S=32768
20 L=0:FORZ=0TO1STEP0:X=RND(.)*10
30 IFX<4THENR=R-1:IFR<1THENR=1
40 IFX>6THENR=R+1:IFR+W>37THENR=37-W
50 RN=RND(.)*35+1:PRINTMID$(R$,RN,R);SPC(W);MID$(R$,RN,39-R-W)
60 D=D+1:L=L+1:IFL>49THENL=0:W=W-1:IFW<3THENW=3
70 IFD<25THENNEXT
75 GETA$:IFA$="4"THENC=C-1
80 IFA$="6"THENC=C+1
90 P=PEEK(S+C):IFP<>32THEN200
100 POKES+C,42:NEXT
200 PRINTSPC(17)"CRASH!":IFD>HTHENH=D
205 PRINT,"SCORE:"D"  HIGH:"H
210 FORX=1TO2000:NEXT:POKE158,0
220 GETA$:IFA$=""THEN220
230 GOTO10

And here is my Color BASIC conversion:

0 ' 10 PRINT RACER
1 ' BY WWW.8BITSHOWANDTELL.COM
2 '
3 ' PORTED FROM PET TO COCO
4 ' BY SUBETHASOFTWARE.COM
5 R$="":CLS:PRINT"INIT:";:FORX=1TO75:M$=CHR$(47+45*(RND(2)-1)):R$=R$+M$:PRINTM$;:NEXT
6 S$=STRING$(32," ")
10 CLS:C=16:R=10:W=12:D=0:S=1024
20 L=0:FORZ=0TO1STEP0:X=RND(.)*10
30 IFX<4THENR=R-1:IFR<1THENR=1
40 IFX>5THENR=R+1:IFR+W>29THENR=29-W
50 RN=RND(.)*28+1:PRINTMID$(R$,RN,R);MID$(S$,1,W);MID$(R$,RN,31-R-W)
60 D=D+1:L=L+1:IFL>49THENL=0:W=W-1:IFW<3THENW=3
70 IFD<16THENNEXT
75 A$=INKEY$:IFA$=CHR$(8)THENC=C-1
80 IFA$=CHR$(9)THENC=C+1
90 P=PEEK(S+C):IFP<>96THEN200
100 POKES+C,106:NEXT
200 PRINTTAB(13)"CRASH!":IFD>H THENH=D
205 PRINTTAB(6)"SCORE:"D"  HIGH:"H
210 FORX=1TO2000:NEXT:A$=INKEY$
220 A$=INKEY$:IFA$=""THEN220
230 GOTO10

The block of code MiaM refers to is this:

75 GETA$:IFA$="4"THENC=C-1
80 IFA$="6"THENC=C+1

75 A$=INKEY$:IFA$=CHR$(8)THENC=C-1
80 IFA$=CHR$(9)THENC=C+1

On the Commodore PET, without arrow keys, it used “4” and “6” on the numeric keypad for Left and Right. On the CoCo, I changed that to the Left Arrow key and the Right Arrow key.

The Commodore PET has much less work to do looking for A$=”4″ versus A$=CHR$(8) not he CoCo (due to all the parsing). I could have made the CoCo use letter keys like “A” for left and “S” for right to get similar performance.

But what MiaM suggests may be faster. Instead of comparing strings like A$=CHR$(8), the suggestion is to use BASIC’s ASC() keyword to return the numeric value of the character, then compare a numeric value rather than a string compare.

Which is faster? A one character string compare, or ASC() and a number compare?

Let’s find out.

Comparing a String to a String

For this, I dug out my old BENCH.BAS benchmarking code and inserted the first method I wanted to test — the way the Commodore PET did it:

5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 A$=INKEY$:IF A$="4" THEN REM

70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

Comparing A$ to a quoted value in this loop produces 515.

Comparing a String to a CHR$

My conversion changed this to comparing to a CHR$(8) value, like this:

0 REM ascvsstringcompare.BAS
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 A$=INKEY$:IF A$="4" THEN REM
30 A$=INKEY$:IF A$=CHR$(8) THEN REM

70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

This produces a slower 628. No surprise, due to having to parse CHR$() and the number. I could easily speed up the CoCo port by using quoted characters like “A” for Left and “S” for Right.

But I really wanted to use the arrow keys.

ASC and you shall receive…

The new suggestion is to use ASC. ASC will convert a character to its ASCII value (or PETASCII on a Commodore, I would suppose). For example:

PRINT ASC("A")
65

The cool suggestion was to try using INKEY$ as the parameter inside of ASC(), and skipping the use of a variable entirely. Unfortunately, when I tried it, I received:

?FC ERROR

Function Call error. Because, if no key is pressed, INKEY$ returns nothing, which I suppose would be like trying to do:

PRINT ASC("")

We have been able to use INKEY$ directly in other functions, such as INSTR (looking up a character inside a string), and that works even when passing in “”:

PRINT INSTR("","ABCDE")
0

But ASC() won’t work without a character, at least not in Color BASIC. And, even if we used A$=INKEY$, we can’t pass A$ in to ASC() if it is empty (no key pressed) which means we’d need an extra check like:

30 A$=INKEY$:IF A$<>"" THEN IF ASC(A$)=4 THEN ..

The more parsing, the slower. This produced 539, which isn’t as slow as I expected. It’s slower than doing IF A$=”4″ but faster than IF A$=CHR$(8). Thus, it would be faster in my CoCo port than my original.

This did give me another thing to try. ASC() allows you to pass in a string that contains more than one character, but it only acts upon the first letter. You can do this:

PRINT ASC("ALLEN TRIED THIS")
65

This means I could always pad the return of INKEY$ with another character so it would either be whatever keys he user pressed, or my other character if nothing was pressed. Like this:

30 IF ASC(INKEY$+".")=8 THEN REM

If no key has been pressed, this would try to parse “”+”.”, and give me the ASCII of “.”.

If a key had been pressed, this would parse that character (like “4.” if I pressed a 4).

As I learned when I first stated my benchmarking BASIC series, string manipulation is slow. Very slow. So I expect this to be very slow.

To my surprise, it returns 520! Just a smidge slower than the original IF A$=”4″ string compare! I’m actually quite surprised.

Now, in the actual 10 PRINT RACER game, which is doing lots of string manipulations to generate the game maze, this could end up being much slower if it had to move around other larger strings. But, still worth a shot.

Thank you, MiaM! Neat idea, even if Color BASIC wouldn’t let me do it the cool way you suggested.

Until next time…

Bonus

Numbers verses string compares:

30 IF Z=4 THEN REM

That gives me 350. Even though decimal values are much slower to parse than HEX values, they are still faster than strings.

But, in pure Color BASIC, there is no way to get input from a keypress to a number other than ASC. BUT, you could PEEK some BASIC RAM value that is the key being held down, and do it that way (which is something I have discussed earlier).

Any more ideas?

VIC-20 knowledge lived on, without me knowing it.

I have posted a number of CoCo BASIC articles that used the example of bouncing a ball around the screen. I have a simple routine I use for this, which involves an X and Y location variable, and movement MX and MY variables that will be positive or negative for which direction the ball is moving.

To my surprise, I found nearly this exact code in the old VIC-20 manual as a bouncing ball example!

This code called it DX and DY (delta), but it’s basically the same code I’ve been using all these years. I had no idea I learned this from the VIC-20 manual back in 1982!

Classic.

TheVIC20 USA Users Group

The replica Commodore VIC-20, known as TheVIC20, is not for sale in the USA and isn’t planned to be sold here. This is a limited edition version the C64 replica, TheC64, but in a VIC-20 enclosure with some different VIC software included.

It can be ordered from Amazon UK and shipped here, however, and then you use your own 1 amp USB power supply instead of the funky UK one that comes the box.

Although I don’t have one currently, one is being sent to me soon. So I started a users group…

https://www.facebook.com/groups/1228896444155278hnn

And a website, coming soon:

https://www.thevic20usausersgroup.com

Random Easter Egg

Updates:

  • 2022-12-09 – Fixed program listing.

There is an interesting hidden message embedded in the Color BASIC ROM, and here is the code that reveals it:

0 REM COLOR BASIC EASTER EGG
10 A=26493:B=66:C=13:GOSUB40
20 A=291227:B=B+2:C=C+3:GOSUB40
30 END
40 I=RND(-A):FOR I=1 TO 4:PRINT CHR$(B+RND(C));:NEXT:RETURN

If you run this code, it will display this:

“COCOFEST” easter egg in the Color BASIC ROM!

Amazing, eh? How did they possibly know back in 1980 that COCOFEST would become a thing?

But actually, it’s just a random message, and not a hidden message at all. I learned of this trick from this video by 8-Bit Show and Tell that claims to share a hidden anti-Microsoft Easter egg in Commodore 64 BASIC… and then reveals how the prank works.

A hidden anti-Microsoft Easter egg in Commodore 64 BASIC! Or not…

If you tried to run that program on other flavors of BASIC, it probably would not work. It certainly does not produce the expected results on a CoCo.

10 A=125708:GOSUB 20:A=33435700:GOSUB 20:A=17059266:GOSUB 20
20 A=RND(-A)
30 A=INT(RND(A)*22):IF A THEN PRINT CHR$(A+64);:GOTO 30
40 PRINT:RETURN

This was the first video from 8-Bit Show and Tell I ever saw, and it’s lead me down quick a rabbit hole trying things he demonstrates on the Commodore computers on our beloved CoCo. And it all started with this random video that YouTube randomly showed me.

Monkeys and Shakespeare

The infinite monkey theerem states that…

“…a monkey hitting keys at random on a typewriter keyboard for an infinite amount of time will almost surely type any given text, such as the complete works of William Shakespeare.”

Wikipedia

We are just using BASIC’s RND() random number generator to simulate a monkey at a typewriter, and using short words instead of the complete works of Shakespeare.

It’s much quicker this way.

As previously discussed, the RND function generates a series of numbers that are not random. Each time you power up a CoCo, for instance, this code will produce the same “random” numbers the first time you run it:

FOR A=1 TO 8:PRINT RND(50);:NEXT
You get these same random numbers every time you power up the CoCo.

Try it for yourself using the web-based JS Mocha CoCo emulator.

In order to change the series of numbers, you pass a negative value into the RND() function, and that series will be used. If you do X=RND(-1), you will then get the same series of random values every time. If you do X=RND(-42), you get a different set of random numbers every time.

Magic!

Or math. But math is hard, and magic is just frustrating.

The monkey simulator

But how do you find which random seed value will give you the random numbers you want in the order you want them? The original prankster used brute-force trial and error.

A program can be designed that first seeds the RND function with -1, then generates a series of random numbers and tests to see if they are what it is looking for. In the case of the C64 version, it needed to see the numbers that represented the characters of the word followed by a ZERO to terminate the string.

If it did not work, it tries a seed of -2, and so on. This could take hours or days, and there is no guarantee the exact series of numbers will be found.

I decided to write a CoCo version of this monkey typewriter simulator, but I made some changes.

  1. First, I figured looking for “W”+”O”+”R”+”D” was more work than just looking for “W”+”O”+”R”+”D” without a 0 byte at the end. That should speed up the search, but require an extra bit of data in the display program since it now needs to know how many random values to use (the length of the word).
  2. The C64 version looked from A to the highest letter used (“BILL GATES SUCKS” scans A to U, though it doesn’t really need to try to find A since the earliest letter is B.) I figured that looking for A to Z (worst case, 26 choices) would be more work than just looking at the range of letters actually used in the word. For instance, finding “ABC” in a repeating random series of 26 numbers seems less likely than finding “ABC” if you were only using 3 random numbers. I made my generator look for a range covering only the letters being used. “CAT” would need numbers from “C” to “T”. “DOG” would need “D” to “O”. “ALACAZAM” would need “A” to “Z”. This meant my display program also needed to know the starting letter value and range value, in addition to the word length.

My version is not as clean and tidy as the C64 original

Here is the program I came up with. You can type in a word and it will present the range of letters it will look for, and then start searching until it finds it (or, weeks later, it has not and you give up):

10 REM rndwords.bas
20 POKE 65495,0
30 INPUT "TARGET STRING";T$
40 TL=LEN(T$)
50 IF TL=0 THEN 30
60 REM FIND LOWEST AND HIGHEST LETTER
70 LL=255:HL=0
80 FOR P=1 TO LEN(T$)
90 V=ASC(MID$(T$,P,1))
100 IF VHL THEN HL=V
120 ?V,LL;HL
130 NEXT
140 REM CALCULATE LETTER RANGE
150 R=HL-LL+1
160 PRINT "SEARCHING FOR: ";T$
170 PRINT "LETTER RANGE :";R;"(";CHR$(LL);" - ";CHR$(HL);")"
180 REM SEARCH...
190 FL=LL-1
200 FOR SD=1 TO 9999999
210 V=RND(-SD):A$=""
220 A$=A$+CHR$(FL+RND(R))
230 IF LEN(A$)<TL THEN 220
240 IF A$=T$ THEN PRINT -SD,T$
250 NEXT
1000 V=RND(-26493):FOR I=1 TO 4:PRINT CHR$(66+RND(13));:NEXT

Some words are found almost instantly. “HI” shows up immediately:

Finding random words in the Color BASIC RND function.

The output shows the random seed to start with (-5), the word it was looking for (“HI”), the ASCII character to add to the random numbers it finds, and the range to use in the RND functions.

To display the string back, you would modify my original COCOFEST program with the proper values, or do it manually:

V=RND(-5):FOR I=1 TO 2:PRINT CHR$(71+RND(2));:NEXT:PRINT

Here are some words I have found:

REM "COCO"
V=RND(-26493):FOR I=1 TO 4:PRINT CHR$(66+RND(13));:NEXT

REM "FEST"
1001 V=RND(-291227):FOR I=1 TO 4:PRINT CHR$(68+RND(16));:NEXT

REM "SUB"
1002 V=RND(-56403):FOR I=1 TO 3:PRINT CHR$(65+RND(20));:NEXT

REM "ETHA"
1003 V=RND(-1049135):FOR I=1 TO 4:PRINT CHR$(64+RND(20));:NEXT

I tried to find “COCOFEST” together, but after days and days of running, it still hadn’t. Perhaps it would have found it if I was searching the entire A-Z range versus just C-T. It’s random-ish, after all.

Perhaps one of you will take this concept and recreate the C64 version, looking for A-Z and a zero. Maybe that works better. I did not try.

Perhaps one of you will start compiling a dictionary of random words and we can use this as a secret decoder ring for passing cryptic messages to each other on Facebook.

Perhaps this will just be a passing random thought and we will never speak of it again.

But knowing me and this site, I expect we will speak of it again. Especially if I get any good comments to this post.

Until next time…

BASIC and ELSE and GOTO and Work – part 1

My recent return to exploring my old Commodore VIC-20 code has reminded me about the main reason I jumped ship to a Radio Shack TRS-80 Color Computer: Extended Color BASIC. The older CBM BASIC V2 used by the VIC was missing keywords like ELSE, and had no functions for graphics or sounds. While I am having a blast re-learning how to program VIC-20 games, I sure do miss things like ELSE.

But should I?

IF/THEN/ELSE versus IF/THEN versus ON/GOTO

Pop quiz time! Suppose you were trying to determine if you needed to move a game character up, down, left or right. Which is the faster way to handle four choices?

30 IF Z=1 THEN 100 ELSE IF Z=2 THEN 200 ELSE IF Z=3 THEN 400 ELSE IF Z=4 THEN 500

…or…

30 IF Z=1 THEN 100
31 IF Z=2 THEN 200
32 IF Z=3 THEN 400
33 IF Z=4 THEN 500

Of course, if the values were only 1, 2, 3 and 4, you wouldn’t do either. Instead, you might just do:

ON Z GOTO 100,200,300,400

…but for the sake of this question, assume the values are not in any kind of order that allows you to do that.

IF/THEN/Work/ELSE versus IF/THEN/WORK

Suppose you were a junior high kid learning to program and you wanted to update some player X/Y positions based on keyboard input. Which one of these would be faster?

30 IF Z=1 THEN X=X+1 ELSE IF Z=2 THEN X=X-1 IF Z=3 THEN Y=Y+1 IF Z=4 THEN Y=Y-1

…versus…

30 IF Z=1 THEN X=X+1
31 IF Z=2 THEN X=X-1
32 IF Z=3 THEN Y=Y+1
33 IF Z=4 THEN Y=Y-1

All is not fair

I should point out that the speed it takes to run these snippets depends on the value of Z. For the sake of this article, let’s assume no key is pressed, so Z is set to something that is not 1, 2, 3 or 4.

Obviously, when there are four IFs in a row (either in a single line with ELSE, or on separate lines), the order of the checks determines how fast you get to each one. If Z is 1, and that is the first IF check, that happens faster than if Z is 4 and the code has to check against 1, 2 and 3 before finally checking against 4.

The same thing applies in languages that use switch/case type logic, so the things that need to be the fastest or happen most often should be at the top of the list and checked before things that happen less often.

Because of this, to be fair, we should also check best case (Z=1) and worst case (Z=4) and see what that does.

Benchmarking going to a line

In my benchmark program, this one counted to 954:

0 REM elsetst1.bas '954
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF Z=1 THEN 100 ELSE IF Z=2 THEN 200 ELSE IF Z=3 THEN 400 ELSE IF Z=4 THEN 500
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

And this one was a tiny bit faster. It counted to 942:

0 REM elsetst1.bas '942
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF Z=1 THEN 100
31 IF Z=2 THEN 200
32 IF Z=3 THEN 300
33 IF Z=4 THEN 400
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

Thus, using ELSE was a bit worse if none of the conditions were true.

IF we could have used ON/GOTO, that would be blazing at 253!

0 REM elsetst3.bas '253
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON Z GOTO 100,200,300,400
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

But I said we couldn’t, because I changed the rules to “do work” rather than “go to a line.”

Benchmarking doing work

Doing work with ELSE clocked in at 601:

0 REM elsetst4.bas '601
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF Z=1 THEN X=X+1 ELSE IF Z=2 THEN X=X-1 IF Z=3 THEN Y=Y+1 IF Z=4 THEN Y=Y-1
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

Since ELSE was slower to go to a line, I thought maybe it would be here, too, but instead, splitting the statements was slower. This one reports 963:

0 REM elsetst5.bas '963
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF Z=1 THEN X=X+1
31 IF Z=2 THEN X=X-1
32 IF Z=3 THEN Y=Y+1
33 IF Z=4 THEN Y=Y-1
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

It seems like ELSE has its place, but not for just going to a line.

Best versus Worst: FIGHT!

Let’s try some best and worst cases now. For this test, I’ll resolve the jumps to lines 100, 200, 300 and 400 by adding this:

100 GOTO 70
200 GOTO 70
300 GOTO 70
400 GOTO 70

That will greatly slow things down since we have to search forward to the new line, then it has to start back at the top of the program and search forward to find line 70. BUT, it will be consistent from test to test. I’ll add a “6 Z=1” or “6 Z=4” line.

  • elsetst1.bas (else): Z=1 produces 507. Z=4 produces 1058.
  • elsetst2.bas (separate): Z=1 produces 390. Z=4 produces 1053.
  • elsetst3.bas (on/goto): Z=1 produces 317. Z=4 produces 357.

Wow. ON/GOTO is really good at going places, best or worst case.

And what about the “doing work” stuff?

  • elsetst4.bas (else): Z=1 produces 632. Z=4 produces 633.
  • elsetst5.bas (separate): Z=1 produces 1171. Z=4 produces 1172.

In conclusion…

If you are using IF to go to some code, ON/GOTO is the fastest, following by separate lines. Even in the worst case, separate lines are still a tiny bit faster, which surprised me. I suspect it’s the time it takes to parse the ELSE versus a new line number. Retesting with all the spaces removed might change the results and make them closer.

But it does look like we need to stop doing “IF X=Y THEN ZZZ ELSE IF X=Y THEN ZZZ ELSE” unless we really need the extra bytes ELSE saves over a new line number.

And if you are trying to do work, ELSE seems substantially faster than separate line numbers. But, in both cases, best and worst case are very close. I believe this is a benchmark issue, since the time to scan a few lines is tiny compared to the time it takes to do something like “X=X+1”, and both best and worst case do the same amount of work. A better test would need to be performed.

Bonus

There is a way to speed up the separate line statements when doing work, especially for better case. Consider this:

0 REM elsetst6.bas '1034
5 DIM TE,TM,B,A,TT
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF Z=1 THEN X=X+1:GOTO 70
31 IF Z=2 THEN X=X-1:GOTO 70
32 IF Z=3 THEN Y=Y+1:GOTO 70
33 IF Z=4 THEN Y=Y-1
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END

By adding the GOTO, if line 30 is satisfied (Z=1), the parser can start searching for line 70 without having to do the check against Z three more times. But, when a case is not satisfied, it now has to parse through the GOTO token and a line number to find the end of the line, meaning that for worst case (Z=4) it should be a bit slower.

Let’s see if this works.

  • elsetst6.bas (separate/goto): Z=1 produces 544. Z=4 produces 1241.

Compare that to the previous version without the end line GOTOs:

  • elsetst5.bas (separate): Z=1 produces 1171. Z=4 produces 1172.

It looks like there’s a significant improvement for best case, and a slight decrease in performance for worst case (the overhead of skipping more characters to find the end of the line for the false conditions).

The more you know…

I guess I am learning quite a bit by revisiting the VIC-20 and having to do things without ELSE.

What do you think? Did I miss anything?

Until next time…

VIC-20 “smooth move”.

Updates:

  • 2024-01-05 – Added YouTube video of the program.

I got stuck on my multi-part Sky-Ape-Er dissection tangent, so I thought I’d do something different for today’s VIC-20 Tuesday.

The VIC-20 uses programmable character based graphics, You can change the pixels that make up a letter “A” to be a small 8×8 icon of a spaceship, for instance. But, when you move that letter A to different spots on the screen, it jumps 8 pixels at a time making movement quite jerky (as demonstrated by all my VIC-20 programs):

Since I don’t have time to write a full article at the moment, I’ll share this small VIC-20 program and come back to discuss what it is doing, and why it is doing it, later.

30 print"{clear}{reverse on}frames:"
60 for c=0 to 7:print chr$(65+c);chr$(73+c):print:next
70 gosub 950
100 poke 36869,255
105 rem
108 rem go thru each row of the character
109 rem
110 for ln=0 to 7
115 rem
118 rem read value, multiply by 256 to make 16-bits
119 rem
120 read v:v=v*256
125 rem
128 rem go thru each frame character
129 rem
130 for ch=0 to 7
135 rem
138 rem split 16-bit value into 8-bit values
139 rem
140 b1=int(v/256)
150 b2=v-(b1*256)
155 rem
158 rem poke shifted value in each charater
159 rem
160 poke 7176+ch*8+ln,b1
170 poke 7176+ch*8+ln+64,b2
175 rem
178 rem shift 16-bit value to the right one bit
179 rem
180 v=v/2
190 next
200 next
210 gosub 950
900 poke 36869,240:poke 198,0
910 end
950 get a$:if a$="" then 950
960 return
1000 DATA 60,126,255,255,255,255,126,60

Until then…

More Crazy ON/GOTO/GOSUB and IF/THENs

More comments from the first ELSE article… First, MiaM chimes in:

MiaM:

You could also split that to two separate statements. One handling K=17 case, and then do ON K-38 GOTO 50,x,30 where x is just the line following the ON GOTO line.

don’t know about speed but you could also try ON K-16 GOTO 40,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,50,x,x,30 (also where x is the following line)

MiaM

In my example, I was getting keypress values back that represented left (17), right (39) and jump (41). By filling the ON/GOTO/GOSUB with bogus line values where the gaps are, you can now use ON/GOTO for non-sequential values. But, if the first number expected was a 17, that would be 17 dummy values at the start. Mia’s approach is to subtract that starting value, eliminating the need for 16 dummy values. Clever!

Clever, sure. But can it be benchmarked?

So how bad is this with speed? Let’s find out.

First, for the dummy lines we will just put nothing between the commas. That will be parsed as a zero, which is bad if any of those values are hit since going to 0 would restart the program, but since we are just testing and can control the value, it will give us the fastest way to parse a long ON/GOTO/GOSUB. Using real lines numbers will only be slower.

0 REM ONMIAM.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON K-16 GOTO 100,,,,,,,,,,,,,,,,,,,,,,200,,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70

Best case for the first target gives me 590. Not bad!

Trying again with “K=41” for worst case gives us 664. Still not terrible.

How does this rank against manual IF/THENs like this?

0 REM ONMIAM2.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=17 THEN 100:GOTO 70
40 IF K=39 THEN 200:GOTO 70
50 IF K=41 THEN 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70

Best case (17) reports 504 and worst case (41) reports 1128. Can there really be that much more overhead to skip two extra IF/THENs? It seems so. In this example, the long ON/GOTO is faster in worst case. Interesting. If worst case is a button not used that often (“smart bomb”), IF/THEN may be the best option, but if all buttons are used equally, there’s probably a point where a long ON/GOTO makes more sense.

But wait … there’s more!

Rob provided a suggestion about using an array:

Yep, could also do something like
Dim C(256)
C(17)=1:C(39)=2:C(41)=3

ON C(K) GOSUB 20,30,40
etc.
But that’s probably a bit memory hungry.

Rob

Rob’s idea of using an array to translate the non-sequential values into sequential numbers is a fun one. It uses more memory, and trades the time it takes to do an array lookup for the time it takes to parse a long ON/GOTO/GOSUB line.

Let’s try:

0 REM ONMIAM2.BAS
5 DIM TE,TM,B,A,TT
6 K=17 'BEST CASE
7 DIMK(41):K(17)=1:K(39)=2:K(41)=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON K(K) GOTO 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 REM LEFT
110 GOTO70
200 REM RIGHT
210 GOTO70
300 REM JUMP
310 GOTO70

Since the largest value we need to check for is 41, I did a DIM K(41). That will allow for values from 0 to 41.

Best case (17) gives us 432! Faster than the manual IF/THEN check!

Worse case (41) gives us 432 … Really? ON/GOTO is really fast with just a few choices. It would be slower if there were dozens and you wanted the one at the end.

The downside of this approach is the memory it took for an array of 42 (0-41) variables. Doing something like this:

NEW:CLEAR
PRINT MEM
DIM K(41)
PRINT MEM

…shows me 22823 and 22606. That’s 217 bytes being taken by the 42 K array entries. (There is an entry reserved for the array itself, then each array element takes 5 bytes, I believe. It’s been awhile since I wrote my String Theory articles which I think looked into how variables are stored.)

This may be the fastest approach if you have a few hundred bytes available to use for this. On a VIC-20 with 3583 bytes free on startup, if I had memory left when I was done with my normal IF/THEN version, I could retrofit it with this approach and use that extra available RAM to speed up my program a tad.

Very cool.

Thanks to MiaM and Rob for these interesting ideas.

Until next time…

VIC-20: Sky-Ape-Er code dissection – part 5

See also: part 1, part 2, part 3, part 4 or part 5 (with more coming).

Is it really VIC-20 Tuesday again? Okay, then. Let’s get started…

The theory so far…

When we last left off, I had just described my theory about how my prototype Sky-Ape-Er game loaded as just one file which contained a custom character set — without being contained in DATA statements or anywhere in the BASIC code.

My theory was that I modified BASIC’s “start of variables” pointer (which normally points to just past the end of the BASIC code) so it was after the memory where the custom characters were stored. When saved, the file would contain the entire range of memory including those custom characters. When the program was LOADed and ran, the first thing it had to do was set the “start of variables” pointer back to where it needed to be, just after the BASIC code.

Today I want to test that theory by trying to create a standalone BASIC program that contains custom character set data. I am going to use the excellent CBM prg Studio development environment to make a BASIC project that will have three things:

  1. A custom character set. I will use the editor to export the characters out as DATA statements into a BASIC file.
  2. That new file will be turned in to a program that will READ the DATA statements and POKE the values into RAM memory.
  3. Finally, I will have a simple test program that will do the necessary POKEs to enable RAM characters and animate them.

Since I haven’t owned a VIC-20 since 1983, I am going to do all of this in the VICE VIC-20 emulator. To do it like I did it back in 1982, I am going to use a virtual cassette tape for program storage. I could probably do this easier using an emulated disk drive, but never had a disk drive on my VIC-20 and I want to keep this as virtually real as possible.

Except for the whole part of using a Mac and virtual PC for development, of course.

Step 1: Custom characters and loader program.

Using CBM prg Studio’s character set editor, I created a few custom characters:

VIC-20 custom character set test in CBM prg Studio.

I then used the “Character Set -> Export -> To Listing” option to output the DATA statements containing those characters.

I then added the following code to load the DATA statements into memory, and display them to verify they work.

0 rem custom charset
1 rem protect chars
2 rem from basic.
3 REM set string end
4 POKE51,0:POKE52,28
5 REM set memory end
6 POKE55,0:POKE56,28
7 REM clear vars
8 CLR
10 for l=7168 to 7168+12*8-1
15 read v:poke l,v
20 next
25 rem clear 'space'
30 for l=7424 to 7432
35 poke l,0:next
40 print"{clear}{reverse on}charset:{reverse off}"
45 print" a b c d"
50 print" e g i"
55 print"@f@h@j@k@"
60 poke 36869,255
65 get a$
70 if a$="" then 65
75 poke 36869,240
80 print l and 255;int(l/256)
85 end
1000 DATA 255,2,4,8,16,32,255,255
1010 DATA 0,28,46,71,142,92,56,0
1020 DATA 16,40,68,98,118,62,28,8
1030 DATA 0,28,58,113,226,116,56,0
1040 DATA 16,56,124,110,70,34,20,8
1050 DATA 96,240,96,62,185,89,28,30
1060 DATA 189,68,132,66,33,0,255,255
1070 DATA 24,60,24,126,189,189,189,60
1080 DATA 189,36,36,36,102,0,255,255
1090 DATA 6,15,6,124,157,154,56,120
1100 DATA 189,34,33,66,132,0,255,255
1110 DATA 255,102,129,66,138,150,223,255

Here is what it is doing:

  • Lines 4 and 6 – These POKEs are used to protect the characters in memory so BASIC will not override them. They set the highest memory location that BASIC and strings can use. I set them to 7168, the address where the custom characters load.
  • Line 10 to 20 – FOR/NEXT loop of READ and POKE the first 8 bytes where character RAM will be. This is where the “@” symbol is (character 0).
  • Line 30 to 35 – These POKEs clear out the “space” character in the custom character set. I do this so my DATA statements don’t have to contain all the characters up to space.
  • Line 40 to 55 – Clear screen then print reverse text (which will still show up even after we switch to RAM character mode) and the custom characters.
  • Line 60 – Set VIC chip to use RAM starting at 7168 for custom characters. At this point, the screen will show my custom characters, and the reverse video should appear as normal text.
  • Line 65 and 70 – Wait for key to be pressed.
  • Line 75 – Set VIC chip to use normal ROM area for characters.
  • Line 80 – Print the two bytes that represent the last memory location used by the character set. These will be POKEd into 45 and 46 before SAVING the demo program later.
  • Line 85 – End.
  • Line 1000 to 1110 – Each line has eight bytes that make up a custom character.

Here is what it looks like when it runs:

VIC-20 custom character set demo.

Then when you press enter, it disables the custom characters and you will see it says “CHAR:” in reverse view with letters a-i and @ where the custom characters were. It then prints two numbers, which I need to write down. Those numbers represent the address of the end of the custom characters my test program uses.

I will build this into a “.prg” file, and then load that into VICE. Next, I will “Create and attach an empty tape image” (I called mine “Custom Char Demo.tap“) and then save this loader program to that virtual tape:

SAVE "CHAR SET LOAD"

Step 2: Program to use the custom characters.

The next part will be a standalone program that will make use of these characters. I am creating a simple demo where spinning bricks fall from the sky and a player character on a sidewalk below has to dodge them. Except nothing happens if a brick hits the player because this is just a demo.

Here is my demo program:

0 rem charset demo
1 REM set vars start
2 POKE45,104:POKE46,19
3 REM set string end
4 POKE51,0:POKE52,28
5 REM set memory end
6 POKE55,0:POKE56,28
7 REM clear vars
8 CLR
9 for l=7424 to 7432
10 poke l,0:next
11 REM charset in RAM
12 POKE 36869,255
13 rem
100 print "{clear}{down*20}";
105 print "@@@@@@@@@@@@@@@@@@@@@@";
110 for l=38400 to 38911:poke l,0:next
115 rem init bricks
120 for b=0 to 3:bl(b)=7680+rnd(1)*22+88*b:bc(b)=1+b:next
125 rem init player
130 p1=8109:p2=8131:pt=7:pb=8
135 rem main loop
140 pokep1,pt:pokep2,pb
145 for b=0 to 3:poke bl(b),32
150 bl(b)=bl(b)+22:if bl(b)>8120 then bl(b)=7680+rnd(1)*22
155 bc(b)=bc(b)+1:if bc(b)>4 then bc(b)=1
160 poke bl(b),bc(b)
165 next
170 get a$
175 if a$="a" then if p2>8119 then pokep1,32:pokep2,0:p1=p1-1:p2=p2-1:pt=5:pb=6:goto 140
180 if a$="s" then if p2<8141 then pokep1,32:pokep2,0:p1=p1+1:p2=p2+1:pt=9:pb=10:goto 140
185 if a$="q" then 510
190 pt=7:pb=8
195 goto 140
500 REM charset in ROM
510 POKE 36869,240
520 END
1000 PRINT PEEK(45);PEEK(46)

Here is what it is doing… Actually, I’ll skip the demo logic and just mention a few important things:

  • Line 1000 – This prints the programs’ current end (start of variables). Since I need the program to restore this when it loads (after being saved with the custom characters), I can load this program and “RUN 1000” to get those values. I then change the POKEs in line 2 to match those values. Thus, when the real program is loaded, it will fix those pointers which will get messed up by the SAVE process.

Thus, I would load this program into memory (but NOT run it) and do “RUN 1000” and note those numbers. I changed the POKEs on line 2 to match those values. Then I saved this after the “CHAR SET TEST” program as:

SAVE "CHAR SET TEST"

Step 3: Save the all-in-one test and charset file.

Now I reset the virtual VIC and rewind the virtual tape. Here are the steps:

  1. LOAD and RUN the “CHAR SET LOAD” program to get the character set in memory. I make a note of the two numbers printed out at the end.
  2. LOAD (but DO NOT run) the “CHAR SET TEST” program.
  3. With the TEST program in memory, I do the following POKEs to change the end of BASIC pointer:
    POKE 45,X:POKE 46,Y
    …where X is the first number the loader program printed and Y is the second number the loader program printed.
  4. I now can SAVE the test program and it should save all of the BASIC and continue saving until it gets to the end of RAM.
SAVE "CHAR SET DEMO"

Step 4: Test!

After a reboot, and rewind of the virtual tape, I try loading the “CHAR SET DEMO” program and running it…

VIC-20 error when loading my character set demo program.

Oh no! My theory is not correct. Something is still wrong. Running this program produces parts of the custom character, but not all. It’s clear I am off somewhere.

What am I doing wrong? I guess I’m gonna need a part 6. . .

Until next time…

ON/GOTO/GOSUB with arbitrary values!?!

Someone named Rob posted a comment to my recent ELSE article that simply contained one line of weird BASIC code:

Arbitrary ON/GOTO/GOSUB!?!

It looked like Rob was presenting a way to do an ON/GOTO with arbitrary values. Will that actually work?

ON GOTO/GOSUB

ON/GOTO (and ON/GOSUB) normally expects values from 1 to X, and a corresponding line number for each consecutive value:

10 INPUT "ENTER 1-5";A
20 ON A GOTO 100,200,300,400,500
30 GOTO 10
100 PRINT "YOU CHOSE 1":GOTO 10
200 PRINT "YOU CHOSE 2":GOTO 10
300 PRINT "YOU CHOSE 3":GOTO 10
400 PRINT "YOU CHOSE 4":GOTO 10
500 PRINT "YOU CHOSE 5":GOTO 10

If A is 1, it will go to 100. If A is 2, it will go to 200. And so on.

Mind the gaps

If you had wanted gaps in the choices, like 1, 3 and 5, you’d have to fill out the ON/GOTO with numbers for the missing choices:

10 INPUT "ENTER 1, 3 OR 5";A
20 ON A GOTO 100,10,300,10,500
30 GOTO 10
100 PRINT "YOU CHOSE 1":GOTO 10
300 PRINT "YOU CHOSE 3":GOTO 10
500 PRINT "YOU CHOSE 5":GOTO 10

That might be a nice approach if the numbers were relatively close to each other, but at some point, adding a bunch of dummy numbers to the ON/GOTO line would take more time to parse than just using separate IF/THEN statements.

Arbitrary GOTO

My example was based on some VIC-20 code I wrote back in 1983. I was reading which key was currently being held down, and would get back three different values for the keys I was reading:

  • 17 – ‘A’ key is pressed (LEFT)
  • 42 – ‘S’ key is pressed (RIGHT)
  • 39 – ‘F1’ key is pressed (JUMP)

I couldn’t use ON/GOTO for values 17, 42 and 39.

But Rob’s code does just that!

20 ON -(K=41)-2*(K=17)-3*(K=39) GOTO 30,40,50

In BASIC, any comparison returns a -1 if it is TRUE, or a 0 if it is FALSE:

PRINTing the result of a comparison in Color BASIC.

…so in Rob’s example, the checks in parenthesis will be turned in to either a -1 or a 0 based on the value of K.

  • If K is 41, then (K=42) will be (-1) and (K=17) and (K=39) will both be (0).
  • If K is 17, then (K=17) will be (-1) and (K=41) and (K=3) will both be (0).
  • If K is 39, then (K-39) will be (-1) and (K=42) and (K=17) will both be (0).

Let’s see what that does:

20 ON -(K=41)-2*(K=17)-3*(K=39) GOTO 30,40,50

K = 41 produces:
ON -(-1) - 2*(0) - 3*(0) GOTO 30,40,50
ON    1  -    0  -    0  GOTO 30,40,50
ON    1                  GOTO 30,40,50

K-17 produces:
ON -(0) - 2*(-1) - 3*(0) GOTO 30,40,50
ON   0  -    -2  -    0  GOTO 30,40,50
ON            2          GOTO 30,40,50

K-39 produces:
ON -(0) - 2*(0) - 3*(-1) GOTO 30,40,50
ON   0  -    0  -    -3  GOTO 30,40,50
ON                    3  GOTO 30,40,50

Fantastic! Subtracting a negative makes it a positive, and multiplying by zero makes zero.

Math rules! And it actually works:

0 REM robgoto.bas
10 INPUT "41, 17 OR 39";K
20 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 30,40,50
25 GOTO 10
30 PRINT "30":RETURN
40 PRINT "40":RETURN
50 PRINT "50":RETURN
Arbitrary ON/GOTO (tip by Rob).

Fantastic! What a great tip. Thanks, Rob!

Arbitrary benchmark

So of course, I now have to see how this compares to separate IF/THEN’s speed-wise. Let’s pull out the trusty benchmark test code and do a version for best case (first choice) and worst case (last choice) for each approach (Rob’s, and IF/THENs).

Arbitrary ON/GOSUB, best case:

0 REM robgoto1.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

This produces 1368.

Arbitrary ON/GOSUB, worse case:

0 REM robgoto2.bas
5 DIM TE,TM,B,A,TT
6 K=39
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=41)-2*(K=17)-3*(K=39) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

This produces 1434

Separate IF/THEN/GOSUB, best case:

0 REM ongoto1.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=41 THEN GOSUB 100:GOTO 70
40 IF K=17 THEN GOSUB 200:GOTO 70
50 IF K=39 THEN GOSUB 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

This produces 518 – almost three times faster!

Separate IF/THEN/GOSUB, worse case:

0 REM ongoto2.bas
5 DIM TE,TM,B,A,TT
6 K=39
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 IF K=41 THEN GOSUB 100:GOTO 70
40 IF K=17 THEN GOSUB 200:GOTO 70
50 IF K=39 THEN GOSUB 300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

This produces 1098, meaning even worst case is still faster.

BUT, we are doing a bunch of number parsing and math here. We can’t do anything about the math, but on Color BASIC, we can change those decimal values to HEX and speed up that part. Let’s try that:

0 REM robgoto3.bas
5 DIM TE,TM,B,A,TT
6 K=41
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=&H29)-&H2*(K=&H11)-&H3*(K=&H27) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

By switching the five integer values in line 30 one to HEX, the speed of best case goes from 1368 to 1150! That’s faster, but it still doesn’t beat 518 using separate IF/THEN/GOSUB.

We might be able to make this a bit faster by using variables, so lets try that:

0 REM robgoto4.bas
5 DIM TE,TM,B,A,TT
6 K=41
7 L=&H29:M=&H2:N=&H11:O=&H3:P=&H27
10 FORA=0TO3:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 ON -(K=L)-M*(K=N)-O*(K=P) GOSUB 100,200,300
70 NEXT
80 TE=TIMER-TM:PRINTA,TE
90 TT=TT+TE:NEXT:PRINTTT/A:END
100 RETURN
200 RETURN
300 RETURN

This brings the time down slightly to 1092. Still not enough to beat the separate IF/THEN/GOSUB (and that could also be sped up slightly using HEX or variables).

Conclusion

This trick is very cool. From my calculations, it looks like it save code space, which could be very important on a low-memory system like a 4K CoCo or the 5K VIC-20. That alone might make this trick worth doing.

But for speed, such as a BASIC game, it looks like brute force IF/THEN may be a better approach.

It’s really nice to have options. I can’t wait for an opportunity to use this technique in something.

Thanks, Rob, for leaving such a cool comment!

Until next time…