That uses 66 byte of BASIC program space (though will use more RAM for strings as it runs).
It appears there are still optimizations to make! In the comments. Stewart Orchard pointed out this one:
Not a big gain but two bytes can be saved by removing the third argument from MID$().
The two argument version of MID$() returns the remainder of the string starting from the specified position, and ASC() returns its result based on the first character of its argument.
– Stewart Orchard
In Color BASIC, MID$ can accept three parameters:
Color Computer 3 BASIC Quick Reference Guide, page 11
If you had as string of 10 characters, and wanted to print the third character, you could do it like this:
A$="ABCDEFGHIJ"
OK
PRINT MID$(A$,3,1)
C
Stewart pointed out that if you left off the final parameter, it returns the rest of the string:
A$="ABCDEFGHIJ"
OK
PRINT MID$(A$,3,1)
CDEFGHIJ
Now that I look at it, it appears MID$ with two parameters is sort of like an inverted RIGHT$. MID$ would give you all the characters starting at the one you specify, and RIGHT$ gives you the number of ending characters you specify.
PRINT MID$(A$,3)
CDEFGHIJ
PRINT RIGHT$(A$,3)
HIJ
I don’t recall using MID$ like this, but I have simulated the same behavior using RIGHT$ like:
I am a bit embarrassed to admit I think I even did something like this:
PRINT MID$(A$,3,LEN(A$)-3)
CDEFGHIJ
…but let’s not speak of that.
And if MID$ can work like RIGHT$, it can also work like LEFT$:
PRINT LEFT$(A$,3)
ABC
PRINT MID$(A$,1,3)
ABC
But I digress…
If MID$(A$,3,1) gives you one character starting as location 3, and MID$(A$,3) gives you all the characters starting at position 3, how does that work? Stewart explained that ASC will still work if you pass it a string since it only works on the first character of the string. It is even documented that way:
Thus, ASC(“HELLO”) produces the same result as ASC(“H”) — the ASCII value of letter H.
With that in mind, we can remove “,1” from the program and reduce it by two bytes:
The challenge is simple: Write a program that prints out a centered text Christmas tree using the same layout of their example program. I thought it would be fun to work through several methods to do this.
PRINT
We start with their simple example, which displays the centered tree using PRINT statements. Here is their example with spaces altered so it centers on the CoCo’s 32 column screen:
You will see one memory optimization they did was leaving off the ending quote. BASIC will stop parsing a quoted string if it reaches the end of a line, so the ending quote is only necessary if more statements follow the PRINT.
I do not know if the space between PRINT and the quote is required for the system they wrote this example, but on the CoCo we could remove it and save 23 bytes of program space right there.
PRINT TAB
Speaking of spaces, each space takes up a character. BASIC has a command that will jump forward a certain amount of spaces — TAB. It works like this:
PRINT TAB(5);"FIVE SPACES OVER"
From my test, it appears TAB takes up one byte(for the “TAB(” portion, then a byte for each digit, then a byte for the closing parenthesis. If tabbing a single digit of spaces, it takes three bytes of program space. Thus, any time we are spacing over more than 3 spaces, it would save memory to use TAB. Also, the semicolon between TAB and the item being printed is optional, so we can leave that out as well.
For the tree example centered on a 32 column screen, every line starts with at least 4 spaces, so using TAB should save memory. Let’s try this:
It is drawing three sections, with the first expanding on each side by 1 (adding two asterisks to each row), then the next section expanded each side by 2 (adding four asterisks to each row), then the final section expands each side by 3 (adding six asterisks to each row).
There is probably a simple way to math it. But first…
Centering
Since one of the requirements is centering the tree on the screen, let’s look at how we center a string. To do this, we simply subtract the length of the string we want to center from the width of the string then divide the result by two:
A$="CENTER THIS"
PRINT TAB((32-LEN(A$))/2);A$
Or, to eliminate some parenthesis, we can start with half the width of the screen (the center of the screen) and subtract half the length of the string:
A$="CENTER THIS"
PRINT TAB(16-LEN(A$)/2);A$
With that out of the way, now we need to figure out how to draw the three triangle shapes of the tree, each time getting wider.
Numbers and patterns.
I decided to lookat the number sequences of each line length for each section of the tree (skipping the trunk lines).
I could easily create the first pattern by doing a FOR/NEXT loop with a step of 2:
FOR I=1 TO 7 STEP 2:PRINT I:NEXT
And then I could do 3 to 15 with a STEP 4, and 5 to 23 with a STEP 6.
FOR I=1 TO 7 STEP 2:PRINT I:NEXT
FOR I=3 TO 15 STEP 4:PRINT I:NEXT
FOR I=5 TO 23 STEP 6:PRINT I:NEXT
This would generate the number sequence I need, and then I could simply print a centered string of that many asterisks, and manually print the tree trunk at the end.
10 FOR I=1 TO 7 STEP 2:GOSUB 50:NEXT
20 FOR I=3 TO 15 STEP 4:GOSUB 50:NEXT
30 FOR I=5 TO 23 STEP 6:GOSUB 50:NEXT
40 I=3:GOSUB 50:GOSUB 50:END
50 PRINT TAB(16-I/2)STRING$(I,42):RETURN
127 bytes.
That’s much better! Though, I saved a bit more by combining lines, and I could save even more by removing spaces and combining lines further:
Now we’re getting somewhere! But ignoring the compressing, can we do better than 127 using better math?
Math. Why did there have to be math?
I am slow and bad at math, but I am convinced there is a math solution to this, as well.
Let’s look at how the numbers for START value and STEP value work. I know there are going to be three sections of the tree, so it seems logical I’d start with something like this:
10 FOR I=1 TO 3
20 PRINT I*2-1
30 NEXT
That would print out 1, 3 and 5, which is the start width of each section of the tree. I could have also done the FOR loop using 0 to 2 and added 1 to get the same result:
10 FOR I=0 TO 2
20 PRINT I*2+1
30 NEXT
Maybe one will be more useful than the other depending on what comes next…
Once we know how wide a section starts, we need to know how much the length increases for each line.
Section 1 increases by 2 each row, section 2 increases by 4 each row, and section 3 increases by 6 each row. That is a pattern of 2, 4, 6. It looks like I could get those values by taking the section (1 to 3) and multiplying it by 2. This will print 2, 4, 6:
10 FOR J=1 TO 3
20 PRINT J*2
30 NEXT
I am bad with math, as I mentioned, but I was able to work out that taking a value of 1 to 4 (for each layer of a section) and multiply that by the section (1 to 3), and subtracting one, I could get this:
10 FOR I=1 TO 3
20 FOR J=1 TO 4
30 PRINT J*I*2-1
60 NEXT
70 NEXT
To print the row of asterisks, I use the STRING$ function. It takes the number of characters to print, then the ASCII value of the character. The asterisk is ASCII character 42 so it I wanted to print ten of them I would do:
PRINT STRING$(10,42)
I can now print the layers of the tree like this:
10 FOR I=1 TO 3
20 FOR J=1 TO 4
30 W=J*I*2-1
50 PRINT TAB(16-W/2);STRING$(W,42)
60 NEXT
70 NEXT
But that still doesn’t handle the “trunk” at the bottom of the tree.
Since the trunk does not follow any pattern, I will simply treat it like an extra section, and increase the section loop (FOR I) by one, and add an IF that says if we are on that layer, the width will be 3. Then, since I don’t want this section to be four layers thick, I can use a second IF after the one that checks for the fourth section to know when to exit (checking for J to be larger than 2):
10 FOR I=1 TO 4
20 FOR J=1 TO 4
30 W=J*I*2-1
40 IF I>3 THEN W=3:IF J>2 THEN END
50 PRINT TAB(16-W/2);STRING$(W,42)
60 NEXT
70 NEXT
162 bytes.
Ah, well, being clever seems to have increase beyond the 127 bytes of just using three FOR/NEXT loops. I suspect it’s the overhead of the way I am trying to print the trunk section. Indeed, without it (like 40, above, and adjusting the I loop to 3) makes it 125.
Sometimes being clever doesn’t help. Even without trying to be clever about the trunk section, the “W=J*I*2-1” stuff takes up as much space as just doing another FOR/NEXT and a GOSUB to print the line.
DATA
We could just have a table to line lengths, and do it like this:
10 READ W
20 IF W=0 THEN END
30 PRINT TAB(16-W/2);STRING$(W,42):GOTO 10
40 DATA 1,3,5,7,3,7,11,16,5,9,13,17,3,3,
98 bytes.
Not bad at all! And that is before compressing it to fewer lines. Here’s a 79 byte attempt, removing spaces, removing the semicolon, altering the logic just a bit to be on LINE 0 (so you can just say GOTO) and changing the IF so there is no END token:
Another “DATA” technique which I have seen Jim Gerrie use is to simply encode the line lengths as characters in a string, and use that as data. For example, ASCII “A” (character 65) will represent 1, “B” will represent 2, and so on. Make a string of the appropriate characters and retrieve them using MID$() and get the ASCII value using ASC() then subtract 64 from that value and you have something like this.
10 FOR C=1 TO 14
20 W=ASC(MID$("ACEGCGKOEKQWCC",C,1))-64
30 PRINT TAB(16-W/2);STRING$(W,42)
40 NEXT
97 bytes.
If we compress the lines together, we can shrink it even further:
Is this as small as it gets? (Technically, this uses more memory, since string space has to be used to create temporary strings for MID$(), but we are only looking at program size and not memory usage.)
CLEAR 0 can be used to reserve 0 bytes for string storage. With no string space, we obviously expect something like this to NOT work:
A$="THIS WON'T WORK WITH CLEAR 0"
Indeed, that would give us “?OS ERROR” — Out of string space.
The exception to this rule are “constant strings” that are embedded inside the BASIC program itself. In a YouTube video I recently posted, I demonstrated how constant strings in a program do not use string space:
You can see twenty nine other short Color BASIC videos I posted to YouTube during the month of #SepTandy 2021.
But I digress…
Here are some other things that won’t work without string space, even if, at first glance, it seems like they would:
It seems you can’t SAVE, LOAD or even PRINT a “constant” string if there is no string space.
But why?
Consider this… The same thing happens with string functions such as LEFT$, RIGHT$, MID$ and even INSTR:
Looking at those, and recalling my String Theory article, we know LEFT$, RIGHT$ and MID$ are trying to create a new string. With no string space, there is no way to create it. That must be why they fail. This makes sense.
But INSTR does not create any strings. It merely returns the position where one string appears inside of another, or 0 if the string is not found. (Or 1 if you give it an empty string, which truly does seem like a bug but other flavors Microsoft BASIC behave the same way with their implementations of INSTR. But I digress…)
And SAVE, LOAD and PRINT are similar. They just print something, not create a string.
That does not make sense.
Let’s speculate a bit.
What’s all this, then?
Without consulting the Color BASIC Unravelled disassembly, my guess is that the ROM code for SAVE, LOAD, PRINT, etc. probably expects some register to be pointing to where the string exists in memory (a location pointer in the variable data). This might be code space, in the case of a constant string embedded in a BASIC line, or string space, in the case of a dynamic string.
But when you are entering a command directly in to BASIC, there is no program memory for that command (even though it seems like it could just point to the keyboard input buffer and then make the ROM call). BASIC needs to put that constant string data somewhere before jumping to the ROM call.
That’s weird, and quite possibly unnecessary, but would, at least, make sense.
I am going to add this to my “look this up in the disassembly to see what’s going on” list for future investigation.
In the meantime … I wonder what else won’t work without string space?
The other night I was experimenting with Extended Color BASIC and the “GET” and “PUT” commands. I knew that the documentation was incorrect about them, and was trying to figure out how they worked. I also wanted to do this without cheating (i.e., doing a quick web search and finding the results of someone who already did this).
I will share the results of this experimentation later, but I wanted to pass along something I was unaware of until last night. I ended up looking at the Extended Color BASIC disassembly to try to find clarification on why something I was seeing was happening. Reading through it revealed a syntax I was unaware of for the following commands:
CIRCLE
GET
LINE
PAINT
PUT
Much like the CoCo BASIC “PRINT@” command, the ‘@’ sign is allowed on these commands as well. It does nothing, and is merely skipped:
Extended Color BASIC’s unused @ syntax.
A note in the Unraveled book says:
It is interesting to note that the “@” symbol does not do anything! It is there to make the command syntax consistent with the “PRINT @” concept and to make it compatible with other versions of Microsoft BASIC.
– Extended Color BASIC Unraveled II
Since this was not documented in the manual (as far as I know), I was not aware of this syntax.
Did you know about this?
There are certainly many undocumented secrets in the Color BASIC ROMs, from the unimplemented Easter egg to things like how the PLAY command plays nonexistent notes. Certainly the author of the Unraveled series discovered these items, and anyone who read those books would have known about it, but I wonder how widespread these oddities were back in the 1980s.
Beyond removing some spaces and a REM statement, here is the smallest I have been able to get my “attract” program:
10 ' ATTRACT4.BAS
20 FOR I=0 TO 3:READ L(I),LD(I),CL(I),CD(I):NEXT:Z=143:CLS 0:PRINT @268,"ATTRACT!";
30 Z=Z+16:IF Z>255 THEN Z=143
40 FOR I=0 TO 3:POKE L(I),Z:L(I)=L(I)+LD(I):FOR C=0 TO 3:IF L(I)=CL(C) THEN LD(I)=CD(C)
50 NEXT:NEXT:GOTO 30
60 DATA 1024,1,1024,1,1047,1,1055,32,1535,-1,1535,-1,1512,-1,1504,-32
(We could reduce it by one line by sticking the DATA statement on the end of line 50, now that I look at it.)
Let’s rewind and look at the original, which used individual variables for each of the moving color blocks:
10 ' ATTRACT.BAS
20 A=1024:B=A+23:C=1535:D=C-23:Z=143
30 AD=1:BD=1:CD=-1:DD=-1
40 CLS 0:PRINT @268,"ATTRACT!";
50 POKE A,Z:POKE B,Z:POKE C,Z:POKE D,Z
60 Z=Z+16:IF Z>255 THEN Z=143
70 A=A+AD
80 IF A=1055 THEN AD=32
90 IF A=1535 THEN AD=-1
100 IF A=1504 THEN AD=-32
110 IF A=1024 THEN AD=1
120 '
130 B=B+BD
140 IF B=1055 THEN BD=32
150 IF B=1535 THEN BD=-1
160 IF B=1504 THEN BD=-32
170 IF B=1024 THEN BD=1
180 '
190 C=C+CD
200 IF C=1055 THEN CD=32
210 IF C=1535 THEN CD=-1
220 IF C=1504 THEN CD=-32
230 IF C=1024 THEN CD=1
240 '
250 D=D+DD
260 IF D=1055 THEN DD=32
270 IF D=1535 THEN DD=-1
280 IF D=1504 THEN DD=-32
290 IF D=1024 THEN DD=1
300 GOTO 50
This was then converted to us an array:
10 ' ATTRACT2.BAS
20 L(0)=1024:L(1)=1024+23:L(2)=1535:L(3)=1535-23
30 Z=143
40 CL(0)=1024:CD(0)=1
50 CL(1)=1055:CD(1)=32
60 CL(2)=1535:CD(2)=-1
70 CL(3)=1504:CD(3)=-32
80 CLS 0:PRINT @268,"ATTRACT!";
90 LD(0)=1:LD(1)=1:LD(2)=-1:LD(3)=-1
100 FOR I=0 TO 3:POKE L(I),Z:NEXT
110 Z=Z+16:IF Z>255 THEN Z=143
120 FOR I=0 TO 3:L(I)=L(I)+LD(I):NEXT
130 FOR L=0 TO 3
140 FOR C=0 TO 3
150 IF L(L)=CL(C) THEN LD(L)=CD(C)
160 NEXT
170 NEXT
180 GOTO 100
And then it was converted to use READ/DATA instead of hard-coding values:
10 ' ATTRACT3.BAS
20 FOR I=0 TO 3
30 READ L(I),LD(I),CL(I),CD(I)
40 NEXT
50 Z=143
60 CLS 0:PRINT @268,"ATTRACT!";
70 Z=Z+16:IF Z>255 THEN Z=143
80 FOR I=0 TO 3
90 POKE L(I),Z
100 L(I)=L(I)+LD(I)
110 FOR C=0 TO 3
120 IF L(I)=CL(C) THEN LD(I)=CD(C)
130 NEXT
140 NEXT
150 GOTO 70
160 ' L,LD,CL,CD
170 DATA 1024,1,1024,1
180 DATA 1047,1,1055,32
190 DATA 1535,-1,1535,-1
200 DATA 1512,-1,1504,-32
Shuffling code around is fun.
But it’s still really slow.
10 PRINT “FASTER”
There are other ways to do similar effects, such as with strings. We could make a string that contained a repeating series of the color block characters, like this:
FOR I=0 TO 7:B$=B$+CHR$(143+16*I):NEXT
Then we could duplicate that 8-character string a few times until we had a string that was twice the length of the 32 column screen:
B$=B$+B$+B$+B$+B$+B$+B$+B$
Then we could make the entire thing move by printing the MID$ of it, like this:
FOR I=1 TO 32
PRINT@0,MID$(B$,33-I,32);
PRINT@480,MID$(B$,I,31);
NEXT
We print one section @0 for the top line, and the other @480 for the bottom line. Unfortunately, using PRINT instead of POKE means if we ever print on the bottom right location, the screen would scroll, so the bottom right block has to be left un-printed (thus, printing 31 characters for the bottom line instead of the full 32). This bothers me so apparently I do have O.C.D. Maybe we can fix that later.
But, it gives the advantage of scrolling ALL the blocks, and is super fast. Check it out:
10 ' ATTRACT5.BAS
20 CLS 0:PRINT @268,"ATTRACT!";
30 FOR I=0 TO 7:B$=B$+CHR$(143+16*I):NEXT
40 B$=B$+B$+B$+B$+B$+B$+B$+B$
50 FOR I=1 TO 32
60 PRINT@0,MID$(B$,33-I,32);
70 PRINT@480,MID$(B$,I,31);
80 NEXT:GOTO 50
That’s not bad, but only gives the top and bottom rows (minus that bottom right location). But, it’s fast!
ATTRACT5.BAS
Since the orders of the colors is the same on the top and bottom, we’d really need to reverse the bottom characters to make it look like it’s rotating versus just reversing. Let’s tweak that:
10 ' ATTRACT6.BAS
20 CLS 0:PRINT @268,"ATTRACT!";
30 FOR I=0 TO 7:B$=B$+CHR$(143+16*I)
35 R$=R$+CHR$(255-16*I):NEXT
40 B$=B$+B$+B$+B$+B$+B$+B$+B$
45 R$=R$+R$+R$+R$+R$+R$+R$+R$
50 FOR I=1 TO 32
60 PRINT@0,MID$(B$,33-I,32);
70 PRINT@480,MID$(R$,I,31);
80 NEXT:GOTO 50
That’s a bit better. But getting the sides to work is a bit more work and it will slow things down quite a bit. But let’s try anyway.
Initially, I tried scanning down the sides of the string using MID$, like this:
FOR J=1 TO 14
PRINT@480-32*J,MID$(R$,39-J+I,1);
PRINT@31+32*J,MID$(R$,33-J+I,1);
NEXT
But that was very, very slow. You could see it “paint” the sides. Each time you use MID$, a new string is created (with data copied from the first string). That’s a bunch of memory shuffling just for one character.
Then I thought, since I can’t get the speed up from a horizontal string being PRINTed, it was probably faster to just use CHR$().
I tried that, and it was still too slow.
Benchmark Digression: POKE vs PRINT
This led me back to an earlier benchmark discussion… Since I cannot get any benefit of using PRINT for a vertical column of characters, I could switch to the faster POKE method. This would also allow me to fill that bottom right character block. My O.C.D. approves.
To prove this to myself, again, I did two quick benchmarks — one using PRINT@ and the other using POKE.
0 ' LRBENCH1.BAS
1 ' 4745
10 C=143+16
20 TIMER=0:FOR A=1 TO 1000
30 FOR P=1024 TO 1535 STEP 32
40 POKEP,C
50 NEXT
60 NEXT:PRINT TIMER
0 ' LRBENCH2.BAS
1 ' 6013
10 C=143+16
20 TIMER=0:FOR A=1 TO 1000
30 FOR P=0 TO 511 STEP 32
40 PRINT@P,CHR$(C);
50 NEXT
60 NEXT:PRINT TIMER
Line 1 has the time that it printed for me in the Xroar emulator.
POKE will be the way.
However, there is still a problem: Math.
It just doesn’t add up…
The CoCo screen is 32×16. There are 8 colors. That means those 8 colors can repeat four times along the top of the screen, and four times along the bottom, leaving only 14 on each side going vertical. 32+32+14+14 is 92, which is not evenly divisible by our 8 colors. If we represent them as numbers, they would look like this:
If you start at the top left corner and go across, repeating 12345678 over and over, you end up back at the top left on 4. We have three colors that won’t fit. This means even if I had a nice fast routine for rotating the colors, they would not be evenly balanced using this format.
However…
…if I leave out the four corners, we get 88, and that divides just fine by our 8 colors!
Thus, the actual O.C.D.-compliant border I want to go for would look like this:
The only problem is … how can this be done fast in BASIC?
To be continued…
Bonus: Show Your Work
Here are the stupid BASIC programs I wrote to make the previous four screens:
0 ' border1.bas
10 CLS:C=113:L=1024
20 ' RIGHT
30 L=1024:D=1:T=31:GOSUB 110
40 ' DOWN
50 L=1087:D=32:T=13:GOSUB 110
60 ' LEFT
70 L=1535:D=-1:T=31:GOSUB 110
80 ' UP
90 L=1472:D=-32:T=13:GOSUB 110
100 GOTO 100
110 ' L=LOC, D=DELTA, T=TIMES
120 POKE L,C
130 C=C+1:IF C>120 THEN C=113
140 IF T=0 THEN RETURN
150 L=L+D:IF L>1023 THEN IF L<1536 THEN 170
160 L=L-D:SOUND 200,1
170 T=T-1:GOTO 120
0 ' border2.bas
10 CLS:C=113:L=1024
20 ' RIGHT
30 L=1025:D=1:T=29:GOSUB 110
40 ' DOWN
50 L=1087:D=32:T=13:GOSUB 110
60 ' LEFT
70 L=1534:D=-1:T=29:GOSUB 110
80 ' UP
90 L=1472:D=-32:T=13:GOSUB 110
100 GOTO 100
110 ' L=LOC, D=DELTA, T=TIMES
120 POKE L,C
130 C=C+1:IF C>120 THEN C=113
140 IF T=0 THEN RETURN
150 L=L+D:IF L>1023 THEN IF L<1536 THEN 170
160 L=L-D:SOUND 200,1
170 T=T-1:GOTO 120
0 ' border3.bas
10 CLS 0:C=143:L=1024
20 ' RIGHT
30 L=1025:D=1:T=29:GOSUB 110
40 ' DOWN
50 L=1087:D=32:T=13:GOSUB 110
60 ' LEFT
70 L=1534:D=-1:T=29:GOSUB 110
80 ' UP
90 L=1472:D=-32:T=13:GOSUB 110
100 GOTO 100
110 ' L=LOC, D=DELTA, T=TIMES
120 POKE L,C
130 C=C+16:IF C>255 THEN C=143
140 IF T=0 THEN RETURN
150 L=L+D:IF L>1023 THEN IF L<1536 THEN 170
160 L=L-D:SOUND 200,1
170 T=T-1:GOTO 120
Previously, I took a target from the attract screen code to talk more on using arrays in BASIC than individual variables. I gave an example of moving ghosts around a screen, and then did modifications to use an array. This let the user select how many ghost they wanted to see randomly display on the screen.
I mentioned that arrays were slower, but allowed flexibility. With that in mind, here is that “wandering ghosts” example turned in to a simple game demo. The player will appear as a yellow block in the top left corner of the screen. The ghosts will randomly appear around the screen as white blocks. The goal is to navigate to the bottom right corner of the screen without hitting a ghost, or being hit by one.
To hopefully make this compatible with the MC-10 computer, it uses the keyboard letters “WASD” — A for left, D for right, W for up and S for down.
You can choose one ghost, and get a single fast moving ghost to avoid. Or you can choose 100 ghosts, and get 100 slow moving ghosts to avoid, making it more like navigating a random maze that slowly moves walls.
Ghost Run in Color BASIC
10 ' ghostrun.bas
15 INPUT "NUMBER OF GHOSTS";G:G=G-1:IF G<0 THEN 15
20 CLS0:DIM G(G):DIM C(G):C=207:B=128:FOR I=0 TO G
30 G(I)=1023+RND(512)
41 NEXT
45 D(0)=1:D(1)=-1:D(2)=-32:D(3)=32
46 PL=1056:PC=159:POKE PL,PC
50 ' DISPLAY GHOSTS
60 FOR I=0 TO G:POKE G(I),C
65 D=INSTR(" DAWS",INKEY$):IF D>1 THEN GOSUB 190
70 ' RANDOM MOVE G(I)
80 NL=G(I)+D(RND(4)-1)
130 IF NL<1024 THEN 170
140 IF NL>1535 THEN 170
150 ' ERASE G(I) AND UPDATE LOCATION
160 POKE G(I),B:POKE NL,C:G(I)=NL
165 IF NL=PL THEN 480
170 NEXT:GOTO 60
180 ' PLAYER MOVED D
190 NL=PL+D(D-2)
200 IF NL<1024 THEN RETURN
210 IF NL>1535 THEN RETURN
220 IF NL=1535 THEN 510
230 IF PEEK(NL)=C THEN 530
240 POKE PL,B:POKE NL,PC:PL=NL
250 RETURN
470 GOTO 60
480 ' GHOST GOT PLAYER
490 PRINT "WE GOT YA!"
500 END
510 PRINT "YOU MADE IT!"
520 END
530 PRINT "YOU HIT A GHOST!"
540 END
To reduce instant death, I made the ghosts spawn no higher than one line below the player. But, with a small amount of faster ghosts, something could spawn then randomly move towards the player quickly. It’s surprisingly challenging (or frustrating).
To make the game responsive to the player, as the code it updating the position of the ghost, the player can move. For example, if drawing 100 ghosts, the player can move as each ghost being drawn. I found this much more fun than doing a turn-by-turn game like the old 1976 CHASE game (also known as Robots for Unix, Daleks for Mac, and a zillion spinoffs in the 70s and 80s). Check out the first published listing Creative Computing 1976 or the wikipedia entry for more details.
But I, as I say, digress.
Maybe we can revisit this in future installments of this series.
Until then, here is another size optimization of the attract screen code, this time removing all the hard-coded array initializations and turning them in to DATA statements loaded by the READ command:
10 ' ATTRACT3.BAS
20 FOR I=0 TO 3
30 READ L(I),LD(I),CL(I),CD(I)
40 NEXT
50 Z=143
60 CLS 0:PRINT @268,"ATTRACT!";
70 Z=Z+16:IF Z>255 THEN Z=143
80 FOR I=0 TO 3
90 POKE L(I),Z
100 L(I)=L(I)+LD(I)
110 FOR C=0 TO 3
120 IF L(I)=CL(C) THEN LD(I)=CD(C)
130 NEXT
140 NEXT
150 GOTO 70
160 ' L,LD,CL,CD
170 DATA 1024,1,1024,1
180 DATA 1047,1,1055,32
190 DATA 1535,-1,1535,-1
200 DATA 1512,-1,1504,-32
We have no changed the original 30 line version in to a 20 line version… but that is actually two lines longer than the previous one due to adding some extra lines for DATA and READ. But, if we were dealing with 50 objects instead of just 4, we’d likely be quite ahead at this point.
Next time, we’ll try to reduce this even further by packing lines together.
In part 1, I showed a simple but slow way to recreate a classic CoCo game startup screen with color blocks moving around the screen. I recall many early CoCo games had startup screens similar to this, though fancier. The assembly language games would rotate all the blocks around the screen, rather than just moving four blocks like my BASIC demo does. For instance, Steve Bjork‘s port of Clowns and Balloons:
You can check it out in videos on YouTube, or play it in a web browser via the wonderful JS Mocha CoCo emulator.
My BASIC attract screen does not attempt to recreate that one, but is more of an homage to the style of attract screens we had in those early years.
My initial version clocked in at about 30 lines, and I suggested ways to make it smaller, such as using arrays to store screen locations rather than individual variables. Arrays make things smaller, but are slower. i.e., if you wanted to track four ghosts on the screen for a Pac-Man game, you could have variables like G1, G2, G3 and G4 and then have a block of code that handled each one individually. Or, you could have an array such as DIM G(3) and access the four ghost locations using G(0), G(1), G(2) and G(3). This allows handling the ghosts in a FOR/NEXT loop instead of four individual blocks of code using separate variables.
Here is a verbose example of randomly moving around four “ghosts”:
10 ' WANDERING.BAS
20 CLS0
30 G1=1024:G2=1055:G3=1504:G4=1535
40 C1=191:C2=239:C3=223:C4=255
50 ' DISPLAY GHOSTS
60 POKE G1,C1:POKE G2,C2:POKE G3,C3:POKE G4,C4
70 ' RANDOM MOVE G1
80 ON RND(4) GOTO 90,100,110,120
90 NL=G1+1:GOTO 130
100 NL=G1-1:GOTO 130
110 NL=G1-32:GOTO 130
120 NL=G1+32
130 IF NL<1024 THEN 180
140 IF NL>1535 THEN 180
150 ' ERASE G1 AND UPDATE LOCATION
160 POKE G1,128:POKE NL,C1:G1=NL
170 ' RANDOM MOVE G2
180 ON RND(4) GOTO 190,200,210,220
190 NL=G2+1:GOTO 230
200 NL=G2-1:GOTO 230
210 NL=G2-32:GOTO 230
220 NL=G2+32
230 IF NL<1024 THEN 280
240 IF NL>1535 THEN 280
250 ' ERASE G2 AND UPDATE LOCATION
260 POKE G2,128:POKE NL,C2:G2=NL
270 ' RANDOM MOVE G3
280 ON RND(4) GOTO 290,300,310,320
290 NL=G3+1:GOTO 330
300 NL=G3-1:GOTO 330
310 NL=G3-32:GOTO 330
320 NL=G3+32
330 IF NL<1024 THEN 380
340 IF NL>1535 THEN 380
350 ' ERASE G4 AND UPDATE LOCATION
360 POKE G3,128:POKE NL,C3:G3=NL
370 ' RANDOM MOVE G4
380 ON RND(4) GOTO 390,400,410,420
390 NL=G4+1:GOTO 430
400 NL=G4-1:GOTO 430
410 NL=G4-32:GOTO 430
420 NL=G4+32
430 IF NL<1024 THEN 470
440 IF NL>1535 THEN 470
450 ' ERASE G4 AND UPDATE LOCATIOn
460 POKE G4,128:POKE NL,C4:G4=NL
470 GOTO 60
And here is that same program, converted to use arrays for the four ghost locations, four ghost colors, and four directions:
10 ' WANDERING2.BAS
20 CLS0
30 G(0)=1024:G(1)=1055:G(2)=1504:G(3)=1535
40 C(0)=191:C(1)=239:C(2)=223:C(3)=255
45 D(0)=1:D(1)=-1:D(2)=-32:D(3)=32
50 ' DISPLAY GHOSTS
60 FOR I=0 TO 3:POKE G(I),C(I)
70 ' RANDOM MOVE G(I)
80 NL=G(I)+D(RND(4)-1)
130 IF NL<1024 THEN 170
140 IF NL>1535 THEN 170
150 ' ERASE G(I) AND UPDATE LOCATION
160 POKE G(I),128:POKE NL,C(I):G(I)=NL
170 NEXT
470 GOTO 60
I tried to keep common line numbers where I could. 47 lines of code down to 15.
And, now that it is an array, it’s easy to make it handle as many ghosts as you want. By adding one more element to the array, and changing the FOR/NEXT loop to count 0-4 instead of 0-3, we get an extra ghost:
10 ' WANDERING3.BAS
20 CLS0
30 G(0)=1024:G(1)=1055:G(2)=1504:G(3)=1535:G(4)=1263
40 C(0)=191:C(1)=239:C(2)=223:C(3)=255:C(4)=143
45 D(0)=1:D(1)=-1:D(2)=-32:D(3)=32
50 ' DISPLAY GHOSTS
60 FOR I=0 TO 4:POKE G(I),C(I)
70 ' RANDOM MOVE G(I)
80 NL=G(I)+D(RND(4)-1)
130 IF NL<1024 THEN 170
140 IF NL>1535 THEN 170
150 ' ERASE G(I) AND UPDATE LOCATION
160 POKE G(I),128:POKE NL,C(I):G(I)=NL
170 NEXT
470 GOTO 60
Arrays are great for reducing code size, and making it so one routine can handle multiple instances of something (locations, colors, etc.).
But, it is slower. Looking up X(3) is slower than looking up X3 since looking up an array has to first look up the variable, and then index in to it to find the entry.
Here is the attract code, converted to use arrays for the block positions and movement directions. As you can see, I’m basically handling the blocks like I did the ghosts above — as objects that can be moved around the screen. Instead of making their movement random, they follow a pattern around the outline of the screen. Instead of having a set color, they just cycle through the seven available non-black VDG colors:
10 ' ATTRACT2.BAS
20 L(0)=1024:L(1)=1024+23:L(2)=1535:L(3)=1535-23
30 Z=143
40 CL(0)=1024:CD(0)=1
50 CL(1)=1055:CD(1)=32
60 CL(2)=1535:CD(2)=-1
70 CL(3)=1504:CD(3)=-32
80 CLS 0:PRINT @268,"ATTRACT!";
90 LD(0)=1:LD(1)=1:LD(2)=-1:LD(3)=-1
100 FOR I=0 TO 3:POKE L(I),Z:NEXT
110 Z=Z+16:IF Z>255 THEN Z=143
120 FOR I=0 TO 3:L(I)=L(I)+LD(I):NEXT
130 FOR L=0 TO 3
140 FOR C=0 TO 3
150 IF L(L)=CL(C) THEN LD(L)=CD(C)
160 NEXT
170 NEXT
180 GOTO 100
30 lines of the originally down to 18 by using arrays.
When time isn’t as important as code size (or convenience), arrays are a great thing.
I have two more iterations of this attract screen to share, so I’ll end with…
Inspired by some of the title screens on early TRS-80 Color Computer games, tonight I wrote this:
10 ' ATTRACT.BAS
20 A=1024:B=A+23:C=1535:D=C-23:Z=143
30 AD=1:BD=1:CD=-1:DD=-1
40 CLS 0:PRINT @268,"ATTRACT!";
50 POKE A,Z:POKE B,Z:POKE C,Z:POKE D,Z
60 Z=Z+16:IF Z>255 THEN Z=143
70 A=A+AD
80 IF A=1055 THEN AD=32
90 IF A=1535 THEN AD=-1
100 IF A=1504 THEN AD=-32
110 IF A=1024 THEN AD=1
120 '
130 B=B+BD
140 IF B=1055 THEN BD=32
150 IF B=1535 THEN BD=-1
160 IF B=1504 THEN BD=-32
170 IF B=1024 THEN BD=1
180 '
190 C=C+CD
200 IF C=1055 THEN CD=32
210 IF C=1535 THEN CD=-1
220 IF C=1504 THEN CD=-32
230 IF C=1024 THEN CD=1
240 '
250 D=D+DD
260 IF D=1055 THEN DD=32
270 IF D=1535 THEN DD=-1
280 IF D=1504 THEN DD=-32
290 IF D=1024 THEN DD=1
300 GOTO 50
It produces color changing blocks that chase around the screen:
It works by assigning screen POKE memory locations to the four moving blocks. A (1024) is the top left corner, and C (1535) is the bottom right. I then made B halfway between A and C, and D halfway between C and A. See? (I initially started with them in each of the four corners, but since the screen is 32 wide and 16 tall, the pattern did not look very good.)
I assign Z as the graphical block I will be POKEing on to the screen, using 143 (the first solid color block).
20 A=1024:B=A+23:C=1535:D=C-23:Z=143
I then assign four directional (delta) variables for each position — AD is the direction A should move (1 to add one and move it right, -1 to subtract one and move it left, 32 to move it down, or -32 to move it up). A and B are not he top row so they start with deltas of 1 (right). C and D are on the bottom row, so they start with deltas of -1 (left).
30 AD=1:BD=1:CD=-1:DD=-1
I clear the screen to black and print a message in the center of the screen.
40 CLS 0:PRINT @268,"ATTRACT!";
Next, I use POKE to place the graphic block (Z) on to locations A, B, C and D.
50 POKE A,Z:POKE B,Z:POKE C,Z:POKE D,Z
To change the color to the next color block, I add 16 to Z. If it’s larger than 255, I reset it back to the 143 value of the first block. (Solid blocks are 16 apart in the character set).
60 Z=Z+16:IF Z>255 THEN Z=143
Then I just do A=A+AD to get the new position (and the same for B, C and D), then check to see if the new location is one of the four corners, and adjust the delta (movement) variable accordingly.
70 A=A+AD
80 IF A=1055 THEN AD=32
90 IF A=1535 THEN AD=-1
100 IF A=1504 THEN AD=-32
110 IF A=1024 THEN AD=1
This is duplicated for B, C and D, then back to the top to do it all over…
120 '
130 B=B+BD
140 IF B=1055 THEN BD=32
150 IF B=1535 THEN BD=-1
160 IF B=1504 THEN BD=-32
170 IF B=1024 THEN BD=1
180 '
190 C=C+CD
200 IF C=1055 THEN CD=32
210 IF C=1535 THEN CD=-1
220 IF C=1504 THEN CD=-32
230 IF C=1024 THEN CD=1
240 '
250 D=D+DD
260 IF D=1055 THEN DD=32
270 IF D=1535 THEN DD=-1
280 IF D=1504 THEN DD=-32
290 IF D=1024 THEN DD=1
300 GOTO 50
It works!
But it seems to go very, very slowly.
And it’s quite large.
I wonder if you might want to try to make the smallest version that does the same thing (even if it’s slower), and the fastest version (even if it’s larger).
Some Size-Reducing Ideas
For example, to make is small, perhaps using a multidimensional array of POKE locations, like DIM L(3) (it is base zero, so 0-3 would be the elements). That would let all the positions be POKEd in a loop like:
L(0)=1024:L(1)=1024+23:L(2)=1535:L(3)=1535-23
FOR I=0 TO 3:POKE L(I),Z:NEXT
For movement, there could be a location delta array like DIM LD(3) that could have the direction each location is meant to be heading.
LD(0)=1:LD(1)=1:LD(2)=-1:LD(3)=-1
To move each location, code like this could be used:
FOR I=0 TO 3:L(I)=L(I)+LD(I):NEXT
Then, perhaps the screen corners could be in an array, such as DIM C(3), and another corresponding array of delta directions could be used like DIM D(3) set up like this:
Then, you could check for each block being in a corner and adjust it’s delta using the array as a lookup table:
FOR L=0 TO 3 ' Check each Location
FOR C=0 TO 3 ' Check each Corner
IF L(L)=CL(C) THEN LD(L)=CD(L)
NEXT
NEXT
And instead of hard-coding all these values, maybe they could be read in via READ/DATA statements.
That might make it smaller, but probably slower.
I’ll share three iterations of this program, each one making it smaller than the previous. Then, if we aren’t bored by then, we can look at ways to make it faster.
As always, leave your thoughts in the comments. I bet you know a better way to do this.
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:
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).
My “big maze” program printed 2×2 character blocks along the bottom of the screen until it got to the bottom right of the screen, then the screen will scroll (and an extra PRINT is added to add a second line) and the process resets and repeats.
After William Astle provided some optimizations, it dawns on me that there was another thing we could try. Here is the code in question (removing unneeded lines and adjusting the GOTO as appropriate):
That was a very subtle change that could double (or more, or less) the speed just by not needing to parse over “PRINT:GOTO 70” every time P was NOT greater than 479 (which is most of the time in that loop).
This made me think that perhaps instead of checking for greater than 479 we could adjust the logic and check for less than 480. Something like this, perhaps:
There’s really no reason for this to be any different speed, is there? GOTO (“THEN”) 100 still has to start at the top and move forward, the same as GOTO (“THEN”) 70 would.
But, in the first case, it quickly skips “THEN 70” to hit the “GOTO 100” below, every time the value is not greater than 479. That
In the second, every time the value is LESS than 480 it returns to 100 (go to top of program and search forward).