I have to admit, this year’s Logiker challenge stumped me. I had a few “clever” ideas on how to reproduce the pattern, but the code was larger than just printing out the pattern from arrays of strings.
Meanwhile, Jason Pittman kept posting revisions of his concept in the comments. In a response to part 2, he shared this:
This is the last attempt I came up with. It uses the same “-3 to 2 … ABS()” from the other example I sent on part 1. I think one potential trick here is to treat it as if you are holding a rubber stamp that stamps out three asterisks that are six spaces apart. You want to make this pattern by stamping it twice on each line. You’re keeping track of a starting position on each row (1027 + 32 for each row) and an offset to add and subtract to the starting position for each row. On the first line, the offset is zero, so you stamp it twice on top of itself at the starting position. On the next line, the offset is 1, so you stamp it twice, but one time you add -1 and the other time you add +1 to the starting position. Does this make any sense?
This was an optimization of his original approach, but it had one limitation that might prevent it from solving the challenge: The CoCo’s 32×16 screen is too small to display the entire image, and POKEing characters on the screen would be limited to just 16 lines of text. He was aware of this, and his program does POKE the full image, but it is POKEing past the end of the visible screen. Would this count? RUNning the program displays the pattern over and over again (which was done to avoid having a new line with an endless loop GOTO):
POKEing past the visible screen works here because I am emulating a Disk Extended BASIC CoCo, and the memory after the text screen is reserved for four pages of PMODE high resolution graphics. But, I suspect, if I ran this on a cassette based CoCo, it might be POKEing in to memory used for the BASIC program itself.
Perhaps the CoCo 3 could help, since it has 40×25 and 80×25 text modes? Jason tried that:
I may play around with LPOKE. This should get it on the 40 column screen. I bet there is a crafty way to (a) not do the last line manually outside of the loops (b) remove one of the FOR loops (c) Shoot, there’s probably some crafty wizard way to do it in one FOR loop with logical operators, but I wouldn’t ever find it.
In this version, Jason uses LOCATE(x,y) to position the cursor. That is what the CoCo 3 used instead of PRINT@ for text positioning. And it works!
It also feels better to use built-in BASIC text commands versus POKEing in to memory.
But he wasn’t done! He added this version:
10 WIDTH 40 20 FOR S = -15 TO 33 STEP 6 30 FOR X = 0 TO 18 40 IF S+X < 19 AND S+X >= 0 THEN LOCATE S+X,X:PRINT "*"; 41 IF S-X >= 0 AND S-X < 19 THEN LOCATE S-X,X:PRINT "*"; 50 NEXT X 60 NEXT S 70 GOTO 70
This one draws the same pattern, but in a very different way. It draws diagonal lines going down from the points at the top. Try it! It’s cool!
And, then this odd one, which creates the pattern by drawing the asterisks RANDOMLY, eventually getting them all on the screen.
…seems like it should be simple. Three asterisks, then six, then six, then four, then six, then six, then three… Spaces that go five, three, one, zero and then back up. Unhelpful.
But, it’s also just one pattern repeated across the screen three times…
* * * * * * *
And then it’s reversed, so I think if we can do the above, we can do the whole pattern. We see spaces of three, two, one, zero on the left, and zero, one, three, and five in the inside.
Color BASIC does not have a SPC() option (my VIC-20 did, I think) for outputting spaces, but TAB() will go to a specific column. Maybe we can figure out which column the asterisks should be in:
This gives us 4, 10 and 16. Then 3 and 5, 9 and 11, and 15 and 17. Then 2 and 6, 8 and 12, and 14 and 18. Finally, 1 and 7 and 13 and 19. I don’t know why, but I kind of like thinking about it as tab positions.
10 FOR SP=3 TO 0 STEP-1 20 PRINT TAB(SP);"*";TAB(6-SP); 30 IF SP<3 THEN PRINT "*"; 40 PRINT 50 NEXT
That would give us one of the pyramid shapes. To complete the bottom, we’d do another FOR/NEXT loop. At least, that’s what I would do. BUT, in a comment to part 1, Jason Pittman had a smarter idea:
Awesome! I’ve got an idea on this one but I’m not going to jump ahead this year and I’m just going to follow along.One thought here is that you could combine the two print loops on 100 and 110 by coming up with a series that goes “0 1 2 3 2 1”. I did it by replacing 100 and 110 with this: “100 FOR A=-3 TO 2:PRINT A$(ABS(ABS(A)-3)):NEXT”
Or, you could shorten that a little if you reverse the direction of the array (so that it looks like “VVV”) and use “100 FOR A=-3 TO 2:PRINT A$(ABS(A)):NEXT”
– Jason Pittman
I could print one diamond like this:
10 FOR A=-3 TO 3:SP=ABS(A) 20 PRINT TAB(SP);"*";TAB(6-SP); 30 IF SP<3 THEN PRINT "*"; 40 PRINT 50 NEXT
That prints almost the entire diamond, except for the final asterisk. Because, if I wanted to print three of them, I’d do this in a loop, then print the final asterisk row at the end.
Unfortunately, as I start going down this rabbit hole, I find the code of loops and such ends up looking larger than some much simpler approaches, like one shown to my by Rick Adams. His code was written for a PDP-8 BASIC, which lacks things like ELSE and MID$. His technique was to have strings representing parts of the pyramid:
" * " " * * " " * *" "* "
…then to print each string three times across the screen. This produced:
" * * * " " * * * * * * " " * * * * * *" "* * * "
…and then do it backwards. There is a missing “*” on the right, that gets printed with an “IF”. In a chat, we bounced around some ideas to shrink the code, but looking at his approach, it seems everything I try to do gets larger:
Try “run length encode” where DATA statements represent the spaces. Print that many spaces, then an asterisk, and repeat.
Try DATA statements showing the positions of the asterisks. DATA is large than a string.
Try simulating a “SET(x,y)” to draw it, but using PRINT@ on the CoCo. Alas, the CoCo 32×16 screen is too small to fit the whole pattern, so even if this was smaller, it would still require extra code at the end to scroll the screen and print the final few lines (as the top portion scrolls off). BUT, using a CoCo 3 40/80 column screen would work using LOCATE x,y instead. But still, larger.
Is there an elegant solution to this challenge that doesn’t involve just PRINTing strings?
This image is 19×19, so while it will fit on a Radio Shack Color Computer 1/2 screen width-wise, it’s a bit too tall to fit height-wise. The challenge allows for it to scroll off the screen, which is something we had to do for past challenges.
Logiker 2023 Challenge pattern.
I can think of a number of ways to approach this.
The pattern is made up of only four unique lines, so you could print them A B C D B C A B C D and so on. There’s probably a simple way to do that with a FOR/NEXT loop and an array of those four lines.
10 CLS 50 A$(0)=" * * * 60 A$(1)=" * * * * * * 70 A$(2)=" * * * * * * 80 A$(3)="* * * * 90 FOR I=1 TO 3 100 FOR A=0 TO 3:PRINT A$(A):NEXT 110 FOR A=2 TO 1 STEP-1:PRINT A$(A):NEXT 120 NEXT 130 PRINT A$(0) 333 GOTO 333
If we had a larger screen (like the 40 or 80 column screens on the Color Computer 3), we could use LOCATE x,y to plot the pattern using some line drawing type math.
We could try the RLE (run length encoding) compression from past years to see if we could compress it down to spaces and characters.
We could try using math to figure out a pattern.
These all seem fun.
I hope to find some time to experiment. I don’t plan to “enter,” since one of the asks for the challenge is to not share your work until after the challenge ends.
VAL() takes a string and converts it in to a floating point numerical variable. The value of “1E39” is a number in scientific notation, and this appears to cause a problem.
In Microsoft BASIC, the notation “1E39” represents the number 1 multiplied by 10 raised to the power of 39. This is also known as scientific notation, where the “E” indicates the exponent to which the base (10 in this case) is raised. So, “1E39” is equal to 1 * 10^39, which is an extremely large number:
There is a classic Commodore one-liner program that prints a maze. I have discussed it on this site several times in the past:
10 PRINT CHR$(205.5+RND(1));GOTO 10
I converted it to use CoCo ASCII “/” and “\” characters:
10 PRINT CHR$(47+(RND(2)-1)*45); : GOTO 10
I have since noticed that the 10 PRINT book even has a version for the CoCo in it:
https://10print.org/10_PRINT_121114.pdf page 55
Recently, Jim Gerrie posted a video of a new one-liner MC-10 version of the maze that truly fit on one 32-column line of the MC-10:
https://www.youtube.com/watch?v=7FQ_ht5u2y4
His approach of using MID$() looked much simpler than the CHR$ version of the Commodore original. I am pretty sure my VIC-20 had MID$ so surely the C64 had it as well. Why wasn’t it used? Perhaps this was an example of code obfuscation. “Type in this mysterious code and see what you get!”
Indeed, in a quick test, using MID$() is smaller and faster:
Now, doing the MC-10 version with CHR$() would be longer on the CoCo since we can’t type those characters like the MC-10 allows. We’d have to use CHR$(140)+CHR$(138) embedded in the MID$ to make that work (and be much slower since it would parse those numbers for every byte of the maze it prints). But…
0 PRINTMID$(CHR$(140)+CHR$(138),RND(2));:GOTO
…does work.
To make it faster, we’d need two lines and a string:
Even with that, the GOTO is slower than the MC-10 version since it has to parse a number, and then has to skip a line each time. You could get around that by doing it like this, and starting it with “RUN 1”:
Awhile back, I posted a note about some weird behavior with DATA statements in Color BASIC. The issue was that you really couldn’t have anything else on a line after the DATA keyword other than data.
I recently mentioned this to Alex Evans so he could make sure his BASIC utilities were not combining DATA statements together. He mentioned something to me that demonstrated this issue even more:
100 DATA 1,2,3:PRINT "FOO"
110 DATA -1
If you RUN that program, it should print “FOO” and do nothing else, proving that the interpreter does scan the entire line looking for things after a DATA statement.
BUT, if you try to USE that DATA, it will continue reading after the 3 and error out, since the data it finds is not an ASCII number:
Above, the “?SN ERROR” is caused by trying to read a numeric variable from what is NOT a string (the token value for PRINT followed by FOO in quotes). Altering the program to use A$ instead of A (and print one item read per line) shows this a bit better:
Alex explained it like this:
Basically, for the purposes of READ everything after the DATA keyword is part of the data statements, but the interpreter executes the line properly.
– Alex Evans
You can see it appears to treat the colon as a DATA separator, since it does not appear as part of the read DATA.
This is a quirk I do not expect many of us ever encountered, since most of us probable grouped all our DATA together, without mixing commands in with them:
100 DATA 1,2,3,-1
I bet all of this was discovered and covered back in the 1980s in various CoCo magazines and newsletters, but the behavior surprised me when I stumbled upon it so I guess I never saw it.
The later model CoCo 2s used an upgrade MC6847 video display generator chip. This chip provided an updated font with true lowercase, as well as the ability to change the border color and inverse the video.
The original 6847 had a hardware pin that could inverse video, and I recall taking the VDG out of my CoCo 1 to pull up a pin then re-insert it. That gave me the nice inverted display I often use in the XRoar emulator:
In the nearly-forty years since I did that modification I had forgotten that it could only be done in hardware. I thought there was probably a POKE or something, since many programs I had used the inverted video mode.
I couldn’t figure it out, and the bits that made the updated 6847T1 show inverse video did not work on the original 6847.
I wrote a program to go through the 6847 T1 VDG text modes, which includes lowercase, changing the border, and inverting the screen.
I’ve seen programs on the CoCo (like GRABBER) that invert/reverse the screen in software, but the bits to do that on the T1 don’t seem to do anything on the 6847.
Does anyone know of a reference that would show me how to do this? Thanks, much!
There is an inverse video pin on the chip. But it is not hooked up. I think.
– tim lindner, 2/15/2023
With that in mind, I decided to do some quick tests. This test PRINTs every possible visible character on the screen:
10 CLS:PRINT
20 FOR C=32 TO 255:PRINT CHR$(C);:NEXT
30 GOTO 30
I skipped the first line and I started printing with CHR$(32) (a space) because the values below that are non-visible characters.
BUT, if you POKE to the screen, you get more characters. Here is an update that will POKE the values 0-255 to the bottom half of the screen:
10 CLS:PRINT
20 FOR C=32 TO 255:PRINT CHR$(C);:NEXT
30 FOR C=0 TO 255:POKE 1280+C,C:NEXT
40 GOTO 40
Comparing the PRINT versus POKE characters, you can see PRINT has 64 ASCII-printable characters starting at 32-96. After that are 32 inverted characters. There are 32 characters that you cannot print in inverse.
When you POKE, values 0-63 represent the inverted full ASCII character set, and 64-127 are the uppercase.
PRINT POKE
------- -------
32-96 64-127 Space to Left Arrow
96-127 0-31 Inverse "@" to Inverse Left Arrow
32-63 Inverse Space to Inverse "Left Arrow"?"
From that table, you can see it’s impossible to PRINT thirty two of the available inverse characters in the 6847, but you can POKE them.
I thought it might be fun to write a routine that would PEEK through the screen memory and invert all the characters. Since there are 32 non-inverted characters from 32-96, and 32 inverted characters from 96-127, it seems all I needed to do is PEEK each location and if it was from 96-127, subtract 64 and POKE it back.
We start with this slow program, complete with some benchmarking timing code:
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
30 TIMER=0
40 FOR L=1024 TO 1536
50 V=PEEK(L):IF V>63 AND V<128 THEN POKE L,V-64
60 NEXT
70 PRINT TIMER
This prints 631 for me. One of the first speedups we can do is change the decimal numbers in line 50 to hex.
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
30 TIMER=0
40 FOR L=1024 TO 1536
50 V=PEEK(L):IF V>&H3F AND V<&H80 THEN POKE L,V-&H40
60 NEXT
70 PRINT TIMER
That simple change takes the time down to 490. And, since variable lookups (when there aren’t a ton of variables) is faster than parsing a hex number in source code, we can change those values to variables and make it even faster:
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
25 X=&H3F:Y=&H80:Z=&H40
30 TIMER=0
40 FOR L=1024 TO 1536
50 V=PEEK(L):IF V>X AND V<Y THEN POKE L,V-Z
60 NEXT
70 PRINT TIMER
This drops to 475.
And, any time you use “IF this AND that”, it can be sped up by doing “IF this THEN IF that”. Let’s try that…
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
25 X=&H3F:Y=&H80:Z=&H40
30 TIMER=0
40 FOR L=1024 TO 1536
50 V=PEEK(L):IF V>X THEN IF V<Y THEN POKE L,V-Z
60 NEXT
70 PRINT TIMER
In this case, it really did not change anything — I see 474. Okay, that was a fail. I guess it doesn’t always help.
Since the math we are doing is a subtract 64, we should see if we can do that by using AND to remove the bit that represents 64. We don’t want to do that if it is a graphics character (128-255) so we’ll need to retain on IF:
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
25 X=&H3F:Y=&H80:Z=&H40
30 TIMER=0
40 FOR L=1024 TO 1536
50 V=PEEK(L):IF V<Y THEN POKE L,V AND &H3F
60 NEXT
70 PRINT TIMER
That shows 367 for me. Better!
I did test to see if checking for the 8th bit (V AND 128) was faster than just comparing a value (A > 127) and the “>” check was faster, so we’ll stick with that.
At this point, the routine to inverse the screen while leaving graphics characters alone is getting close to twice as fast as the original version.
Can we do better? At least a bit, by removing spaces and combining lines (and even though it only parses the FOR values once, I’ll switch them to hex):
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 255:PRINT CHR$(C);:NEXT
25 X=&H3F:Y=&H80
30 TIMER=0
40 FORL=&H400 TO&H5FF:V=PEEK(L):IFV<Y THENPOKEL,V AND&H3F
50 NEXT
70 PRINT TIMER
That prints 356. (There were a few spaces that could not be removed due to needing them so the BASIC parser knows where a variable ends and a keyword begins.)
Now we could make a subroutine that would invert the screen, slowly.
While it might be a neat “effect” for a title screen to watch it painting the screen, it’s not fast enough to use in a program that you want the output to always be inverted.
Some thoughts…
If no semigraphics characters were being used, the check for them could be removed:
10 ' FILL SCREEN WITH STUFF
20 CLS:FOR C=32 TO 127:PRINT CHR$(C);:NEXT
25 X=&H3F:Y=&H80
30 TIMER=0
40 FORL=&H400 TO&H5FF:V=PEEK(L):POKEL,V AND&H3F
50 NEXT
70 PRINT TIMER
That prints 304 — twice as fast as the original, though it will corrupt semigraphics blocks by changing them.
Another approach might be to only do the portion of the screen that has been PRINTed to by changing the range that the L loop scans. For example, you could call a routine to invert just the last line of the screen every time something is PRINTed there.
Or, perhaps a custom “print reverse” routine might make more sense. We’d also want a special CLS routine that cleared the screen to inverse spaces.
10 CLS
20 GOSUB 1100
30 P=0:A$="This is in REVERSE video!":GOSUB 1000
40 P=32:A$="And this one is, too.":GOSUB 1000
50 P=480:A$="Hello, bottom line!":GOSUB 1000
999 GOTO 999
1000 ' PRINT@ REVERSE
1010 L=&H3FF+P:FOR I=1 TO LEN(A$)
1020 V=ASC(MID$(A$,I,1)):IF V>95 THEN V=V-96 ELSE IF V>64 THEN V=V-64
1030 POKE L+I,V:NEXT:RETURN
1100 ' CLS REVERSE
1110 P=0:FOR L=&H400 TO &H5FF:POKE L,&H20:NEXT:RETURN
To use the function set P to the PRINT@ location, assign the string to A$, then GOSUB 1000. Not much to it.
Handling scrolling the screen would be more work (and slower), but could also be done. Perhaps the routine could track the PRINT@ position internally, so every time you print it increases P by 32 (next line) and if it gets to the end of the screen, it could scroll everything up… slowly…
10 CLS
20 GOSUB 1200
30 A$="This is in REVERSE video!":GOSUB 1000
40 A$="And this one is, too.":GOSUB 1000
50 A$="PRINT;":GOSUB 1100
60 A$="NEXT TEXT":GOSUB 1100
70 P=480:A$="BOTTOM LINE":GOSUB 1000
80 A$="AND THIS":GOSUB 1000
999 GOTO 999
1000 ' PRINT@ REVERSE
1010 GOSUB 1100:P=P+32-I+1:IF P<512 THEN 1050
1020 ' SCROLL
1030 FOR L=&H400 TO &H5E0:POKE L,PEEK(L+&H20):NEXT
1040 FOR L=&H5E0 TO &H5FF:POKE L,&H20:NEXT:P=P-32
1050 RETURN
1100 ' PRINT@; REVERSE
1110 L=&H3FF+P:FOR I=1 TO LEN(A$)
1120 V=ASC(MID$(A$,I,1)):IF V>95 THEN V=V-96 ELSE IF V>64 THEN V=V-64
1130 POKE L+I,V:NEXT:P=P+I-1
1140 RETURN
1200 ' CLS REVERSE
1210 P=0:FOR L=&H400 TO &H5FF:POKE L,&H20:NEXT:RETURN
To clear the screen, GOSUB 1200.
To do a normal PRINT, set the string in A$ and GOSUB 1000.
To do a PRINT with a semicolon at the end, set the string in A$ and GOSUB 1100.
To simulate a PRINT@, set P to the screen position then GOSUB 1000 or 1100.
This program does NOT handle printing past position 512 at the bottom right of the screen.
IT CAN POKE PAST SCREEN MEMORY AND CAUSE A CRASH!
A simple check could be done before line 1130 to see if L+I is going to be past the end of screen memory (1536) and adjust accordingly.
If there is any interest (leave a comment), we can look at how we might do that in a future article.
Is this useful?
If we really wanted to use inverse video in BASIC, I guess it would be useful. Can you make it faster in BASIC? What would you suggest? Leave a comment.
But, doing inverse text would be a simple thing for some assembly language routines that could be called easily from BASIC.
OR, perhaps there would be a simple way to just hook in to the output vector used by Color BASIC and do it there.
This is a poor example because PRINTing a variable is generally faster than printing a number due to the BASIC interpreter needing to parse all the TEXT digits of the number and convert it to a string to PRINT. To my surprise, when I ran this test, I found they were basically the same speed. The overhead of processing a line and converting a string of digits seemed to offset the time saved of the loop that was printing a variable that doesn’t need parsing. (FOR/NEXT loop fast, PRINTing variable fast versus a bunch of lines that might be faster but have slower parsing for the text digits.)
However, if the series of individual PRINT lines were printing a variable (“PRINT X”) it would be more than twice as fast.
10 PRINT X
20 PRINT X
...
1000 PRINT X
Apples to apples (difficult to do in this example), blasting out a ton of lines is faster than doing them in a loop. I’ve heard assembly language programmers refer to this as “loop unrolling” and it can work in BASIC as well.
A better example might be how you draw a background screen. Consider a 32 column text screen that is cleared to black, and a border is printed around it:
Here is some code that would do this:
10 CLS 0
20 PRINT CHR$(128);STRING$(30,207);CHR$(128);
30 FOR I=1 TO 14
40 PRINT CHR$(207);STRING$(30," ");CHR$(207);
50 NEXT
60 PRINT CHR$(128);STRING$(30,207);
70 GOTO 70
Or, you could do it using loop unrolling and change that loop of 14 steps to be 14 PRINTS:
When I time those two, there is a teeny speed improvement in the second version (around 1/60th of a second). Is that enough to justify the overhead of the extra memory needed? Probably not in this example, but it might be if it was in code that was redrawing a screen for a game or something.
Side Note: Of course, we could speed this up by pre-generating the strings and the PRINTing them. That would avoid all the parsing of CHR$() and the numbers and building temporary strings over and over and over.
10 CLS 0
20 PRINT CHR$(128);STRING$(30,207);CHR$(128);
25 L$=CHR$(207)+STRING$(30," ")+CHR$(207);
30 FOR I=1 TO 14
40 PRINT L$;
50 NEXT
60 PRINT CHR$(128);STRING$(30,207);
70 GOTO 70
That is about 40% faster than the original, and it could be made even faster, but that’s not the point currently.
Inlining
Another way to speed things up is eliminate as many GOTOs and GOSUBs as you can. Every GO has to search either forward, line by line, looking for the target line (if going to a line after the current one), or start at the very first line and search forward (if starting at an earlier line).
A 10,000 line program will find it quite slow to be on line 10000 and type GOTO 9999. Likewise, line 1 saying “GOTO 10000” will be quite slow.
If there is enough memory, inlining subroutine code will speed things up every time the code is used. Consider this subroutine from my old *ALL RAM* BBS:
15 BR$="*==============*==============*"
...
25 A$="Welcome To *ALL RAM* BBS!":GOSUB1055
...
50 'New User
55 A$="Password Application Form":GOSUB1055
...
100 'Main Menu
105 A$="*ALL RAM* BBS Master Menu":GOSUB1055
...
150 'Call Sysop
155 A$="Calling the Sysop":GOSUB1055
...
200 'Goodbye
205 A$="Thank you for calling":GOSUB1055
...
250 'Userlog
255 A$="List of Users":GOSUB1055
...
1050 'Function Border
1055 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$:RETURN
The routine at line 1050 takes a string in A$. It will clear the screen, print a border string, print the A$ centered, then print the border string again and return.
The time it takes to run this code should be the same no matter where it appears in the program, but the time it takes to get to this code will vary depending on where it is called from. If you were calling it from line 1040, it would be quite fast. If you were calling it from line 20, it has to scan forward through every line from 20-1050 to find it, which would be slower. At least, because it is a GOSUB routine, it will return quickly. (That’s a big advantage of using GOSUB over GOTO.)
This routine could be inlined in to the code, and each use would be slightly faster.
15 BR$="*==============*==============*"
...
25 A$="Welcome To *ALL RAM* BBS!"
26 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
50 'New User
55 A$="Password Application Form"
56 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
100 'Main Menu
105 A$="*ALL RAM* BBS Master Menu"
106 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
150 'Call Sysop
155 A$="Calling the Sysop"
156 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
200 'Goodbye
205 A$="Thank you for calling"
206 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
250 'Userlog
255 A$="List of Users"
256 CLS:PRINTBR$:PRINTTAB((32-LEN(A$))/2)A$:PRINTBR$
...
Again, this is a pretty poor example, but if this routine was something that required speed (a game, animation, etc.) every little bit would help.
Other time-saving techniques
I have covered many other optimizations to code, such as using “&H” hex numbers rather than decimal and using “.” instead of 0, and all of these things can combine to make a dramatically faster program — at the expense of code size. (A=9 takes three bytes, while A=&H9 is faster but takes four bytes.)
But if we were back in 1980 and trying to program on a 4K CoCo, perhaps we would have more things to worry about than speed.
Size matters
On that 1980 4K CoCo, a “PRINT MEM” on startup shows 2343 bytes free for a program. If you didn’t plan to use any strings or string functions, a CLEAR 0 would give back 200 bytes for a total of 2534 bytes free. You probably won’t be doing any loop unrolling or inlining in this environment.
Instead, focusing on the smallest way to do something makes more sense.
Subroutine everything!
If any bit of code is used more than once, it may make sense to make it a subroutine as long as the overhead of having to add “GOSUB xxx” and “RETURN” do not take more than the duplicate code would.
10 X=0:Y=0:P=(Y*32)+X:PRINT@P,"HELLO"
Above, some X and Y coorindates (representing the CoCo’s 32×16 text screen) are converted to a PRINT@ screen position (0-511). This is the type of code that might appear many places where something is printed at the screen. This makes it a good candidate for being a subroutine:
Each time “P=(Y*32)+X” is used, that takes 10 bytes. The overhead for “GOSUB 1000” is 8 bytes, then the subroutine itself adds 5 bytes for the line overhead and 2 bytes for “:RETURN”. That’s 15 bytes extra we just added, but if it were used more than a few times, it starts saving memory. (See note below about line numbers and how you can make this save even more.)
And, if the routine was using X/Y to a P position to print something, you might as well put that in the subroutine as well and eliminate the P variable completely:
With just a bit of reworking code, many bytes can be saved.
But that’s not the best example of saving bytes, because “GOSUB 1000” takes up a lot of memory. Let’s look at how to make it take up less.
Small line numbers save bytes
When a program is tokenized (keywords like PRINT replaced with one and two byte tokens representing the command), the line number is changed in to a two-byte representation of the line. It doesn’t matter if you are at line 5 or line 55000, the line number will take up two bytes of the tokenized line.
But, when you have a line number in the line itself, such as as the target of GOTO, GOSUB if an IF/THEN, those digits will be stored in full just as you typed them:
10 GOTO 10
20 GOTO 100
30 GOTO 1000
40 GOTO 10000
Above, line 20 will take up one more byte than line 10, because “100” takes one more byte than “10”. Because of this, using shorter line numbers will save space. Instead of this:
In this example, you have immediately added extra overhead by having line 0, but as long as you save enough to offset that, the program will be smaller. In this example, the three “GOSUB 10000” were turned in to “GOSUB 1” saving four bytes every time they were used.
As a bonus, having the subroutines at the start of the program will make them faster, since every GOSUB will be to a higher number, and BASIC will just start at the first line and scan forward, finding the subroutine much quicker. Smaller and faster!
And, of course, don’t forget to number your program by 1 — or, if using Extended Color BASIC with the RENUM command, you can do this:
RENUM newline, startline, increment Renumbers a program. newlineis the first new renumbered line. If you omit newline, BASIC uses 10. startlineis where the renumbering starts. If you omit start/ine, BASIC renumbers the entire program. incrementis the increment between each renumberedline. If you omit increment, BASIC uses 10. Note: RENUM does not rearrange the order of lines.
– Getting Started With Extended Color BASIC (1984 edition), page 58
RENUM 0,0,1
That will renumber the program to start at line 0, beginning at line line 0, and incrementing by 1s. If you started with:
100 PRINT
200 PRINT
300 PRINT
400 PRINT
…a RENUM 0,0,1 will give you:
0 PRINT
1 PRINT
2 PRINT
3 PRINT
You may save a few bytes every time a GOTO/GOSUB or IF/THEN is used.
Side Note: I believe it was Alex “Basic Utils” Evans who recently pointed out to me that GOTO and GOSUB each used two bytes. They are actually three token words — GO, TO and SUB. You can even write them separated with a space like “GO TO 100” or “GO SUB 100“. I hadn’t seen that since I was first learning BASIC back in 1982 or so, in some books a schoolmate was loaning me.
Avoid gratuitous use of lines
Every line in Color BASIC has five bytes of overhead. If you typed in a line that was just:
10 A
…you would see memory goes down by six bytes. Five bytes for the line overhead, and one byte for the “A”.
Side Note: Those five bytes are a 2-byte address of the next line, a 2-byte line number, and a trailing 0 terminator at the end of the line. In the above example, the bytes would look like “[xx][xx][yy][yy][A][0]” for that line.
Because of this, writing things out on separate lines consumes more memory, and is also slower since BASIC has more line number data to parse (included when doing a GOTO/GOSUB).
[xx][xx][yy][yy][print_token][:][print_token][:][print_token][:][print_token][:][print_token][:][0] <- 15 bytes total
In Color BASIC, the input buffer will allow you to type up to 249 characters for one line, so you can really pack a lot of commands together and save space (and speed).
Side Note: In Extended BASIC, the EDIT command can be used to get a few extra characters on a line. If you type a long line that features BASIC keywords up to the max of 249 characters, then press ENTER, the line gets tokenized and words that took up five characters like “PRINT” get turned in to one or two byte tokens. This can free up enough space to allow doing an EDIT on the line, then an Xtend to go to the end of the line, and type a few more characters. “When every last byte matters…”
Other space-saving techniques
REM – If space matters, leave out any REMarks in the code as they both take up extra space and slow the program down. I would sometimes comment my programs pretty heavily, then save the commented version out, and then delete the remmarks and save out the smaller version.
Spaces – Removing any unnecessary spaces (“GO TO 1000” becomes “GOTO1000”) helps.
Semicolons – A semi-colon is only required at the end of a PRINT line if you want to avoid having the carriage return printed, or when separating variables that BASIC cannot tell apart form keywords. For example, “PRINT A;B;C” is obviously needed because “PRINT ABC” would be a different variable. But, the semicolon is assumed between items you can print such as “PRINT VAL(A$);B$;CHR$(128)” which works just fine as “PRINT VAL(A$)B$CHR$(128)”. Heck, even when printing string variables like “PRINT A$B$C$D$” the semicolons are not needed. BUT, you still need it if BASIC can’t tell. If you wanted to print A$, B (number) and C$, you would need “PRINT A$B;C$” since BASIC needs to know where the B non-string variable ends.
Trailing Quotes – If a line ends in a quote, the quote can be left off. This goes for things like PRINT”SOMETHING” or even A$=”SOMETHING”. BASIC will figure it out when it gets to the end of the line.
Parenthesis – There are times when parenthesis are required to make math work (order of operations), but sometimes it is just done to make the code easier to read (like spaces and splitting up one instruction per line). In my X/Y example, I used “(Y*32)+X” but the parens don’t matter. The multiplication will happen first, then the addition, so “Y*32+X” will save two bytes. TEST FIRST! Some things look like they should work, but will require parens. (“1*256+1 – 1*256+1” feels like it should be zero, but it will be 2. For this you do need “(1*256+1)-(1*256+1)”.
And, of course, figuring out how to reduce code and combine things is always part of the process.
See also “Carl England’s CRUNCH program” which will do those steps for you.
There are many other tips and tricks for optimizing BASIC for speed or size, but hopefully these examples get you started in experimenting.
I know I must have learned at least some basic stuff about the layout of an RS-DOS disk, because I had a directory searching routine in my first commercial CoCo program – Huffman K1 Librarian sold by Rulaford Research.
That product was a MIDI program that would load or save sound patches (synthesizer voices) to and from a Kawai K1 synthesizer. For functions where you were going to send a patch, it would allow showing all directory of the files of that type. The extension was used to determine if it was a single patch or a block. Though today, I cannot remember the details on what a “block” was for the K1.
Looking at that program now, it would have been nice if I had allowed the user to just cursor around the files and select one, rather than having the user type the name in. Maybe I’ll fix that in a version 1.3 someday … though I sold my K1 long ago, as well as all my CoCo MIDI gear, so I wouldn’t have any way to actually test the update. So maybe I won’t.
Anatomy of an RS-DOS disk
Back in those days, we’d refer to Disk Extended Color BASIC (DECB) as “RS-DOS”. I’m not sure why “Radio Shack DOS” was used for a name, since I don’t recall it saying this anywhere on the screen or in the manuals, but someone must have come up with it and it caught on. (Much like the nickname “CoCo”.)
RS-DOS had a simple file system that was described in detail in the back of the Disk BASIC manual. Back then, most of this was probably beyond me, since looking at it today it still is. It’s interesting that it described the technical details of the disk beyond just the tracks and sector data that could actually be used from BASIC — at least in the 1981TRS-80 Color Computer Disk System Owners Manual & Programming Guide.
I also found it interesting that by the 1986 version of the manual, which was the version available after the CoCo 3 was released, this technical information had been removed.
Above, out of those 338 bytes, the only section we got to use was the 256 data bytes. The rest was used by the FD1773 floppy drive controller chip.
Looking back at the 1981 manual, the format of an RS-DOS disk was pretty clearly defined. The disk drive was a single-sided 35 track device, and each of those tracks contain 18 sectors. Each sector was 256 bytes. Each track of 18 256-byte sectors could hold 4608 bytes. This meant that a disk could hold 161,280 bytes of data! (35 * 18 * 256) Wow, 157K of storage!
Side Note: Although Radio Shack never updated the Disk BASIC ROM to take advantage of it, the floppy controller was capable of supporting up to three double-sided 80 track (720K) floppy drives. Others came up with patches (or replacement DOS ROMs) that let BASIC handle this. This was also supported in disk operating systems such as OS-9 (which was sold by Radio Shack) and FLEX. But, we’re sticking with generic Disk Extended Color BASIC for this article series, so 35 tracks it is…
While 157K sounds pretty amazing, we didn’t actually get to use all of that from BASIC. Track 17 was used to store the directory and file allocation table (FAT). Yep, even back then, Microsoft (who wrote this Disk BASIC) already had a FAT file system… Just not the FAT we became familiar with via PC-DOS/MS-DOS a few years later.
Since track 17 could not be used for data storage, that left us with 34 tracks we could use — 156,672 bytes. Oh well, 153K of high speed disk access sure beats cassette storage. Or, as we learned, “68 granules” of high speed disk access.
Side Note: Tracks are numbers from 0 to 34, so track 17 is actually the 18th track. Sectors, however, are numbers 1 to 18. Go figure. Drives were numbers 0 to 3, so sectors are really the odd one here.
Here is a representation of the 35 tracks on a disk (numbers 0 to 34):
Each of the available 34 tracks was split in half (each half being 9 256-byte sectors) and called granules. Each granule was, therefore, 9 * 256 bytes in size — 2304 bytes.
To see how much space was free on disk, we could type:
PRINT FREE(0)
…and that would print a number, such as 68 for a completely empty disk, or 0 for a full one. Granules never made much sense to me back then, and I don’t suppose they really do today except that it was “half a track.” I don’t know if that would have meant much to me before I got in to OS-9 years later and learned way more about floppy disks.
Track 17, where the directory and FAT were stored, was two more granules of storage we didn’t get to use.
Here is a representation of the granules of a disk, numbers 0 to 68:
Here is a simple program that displays this information on the CoCo 32-column screen. Use UP/DOWN arrows to go track by track, or SHIFT-UP/SHIFT-DOWN to go a page at a time:
10 'DISKMAP.BAS
20 '
30 ' TRACKS & SECTORS
40 '
50 CLS
60 PRINT"TRACK SECTORS"
70 PRINT" (1-9) (10-18)"
80 PRINT STRING$(27,"-");
90 ST=0:PG=12
100 FOR A=0 TO PG
110 TR=ST+A
120 PRINT@96+32*A,TR;
130 IF TR=17 THEN PRINT TAB(10);"FAT & DIRECTORY ";:GOTO 160
140 IF TR<17 THEN GR=(TR*2)+1 ELSE GR=((TR-1)*2)+1
150 PRINT TAB(9);"GRAN";GR;TAB(19);"GRAN";GR+1;
160 NEXT
170 A$=INKEY$:IF A$="" THEN 170
180 IF A$=CHR$(94) THEN ST=ST-1
190 IF A$=CHR$(95) THEN ST=ST-PG-1
200 IF A$=CHR$(10) THEN ST=ST+1
210 IF A$=CHR$(91) THEN ST=ST+PG+1
220 IF ST<0 THEN ST=0
230 IF ST>34-PG THEN ST=34-PG
240 GOTO 100
If you wanted to use all of an RS-DOS disk, a CoCo program could use disk access commands to read/write sectors to any spot on the disk — including track 17 — and manually use all 70 granules for storage. But, if it did that, typing “DIR” would not produce expected results (they would be no directory) and trying to SAVE something on this disk would overwrite data (if it worked at all; it would have needed valid directory information to even do this).
But I digress…
Track 17
Of the 18 sectors contained in track 17, sectors 1 and 12-18 were “for future use.” Radio Shack never used them, as far as I know, but third party patches to Disk BASIC did use them for other features, such as supporting 40 track drives.
Sector 1 – Unused (“for future use”)
Sector 2 – File Allocation Table (FAT)
Sectors 3-11 – Directory Entries
Sectors 13-18 – Unused (“for future use”)
FAT (File Allocation Table)
The first 68 bytes of Sector 2 contained the file allocation table. Each byte represented the status of one of the available granules on the disk. If the granule was not used by any file, the byte representing it would be set to 255 (&HFF). I expect that the FREE() command simply read Track 17, Sector 2, and quickly scanned the first 68 bytes, counting how many were 255.
DSKI$ / DSKO$
Let’s do a quick tangent here. Disk BASIC provided two commands for reading and writing sectors on the disk. DSKI (disk input) and DSKO (disk output) needed a drive number (0-3), track number (0-34), and a sector number (1-18) to read or write from/to. Since a Color BASIC string variable could not be longer than 255 (the maximum size a byte could represent for length), a string could not hold an entire sector. Because of this, DSKI and DSKO split the sector up in to two 128-byte strings like this:
CLERA 256
DSKI$ 0,17,2,A$,B$
Above, the CLEAR 256 is needed to increase string space from the default 200 bytes to enough to store the full sector in A$ and B$ and two 128 byte strings. Keep in mind, more memory will be needed when you do any manipulation on either of those strings. As you will see below, CLEAR 384 is really needed at the very least, since if you do a MID$() or LEFT$() on A$ or B$, enough string memory has to be available to hold a copy of that string (256+128 is 384). See my string abuse article for a deep dive in to why that is the case.
For DSKI$, the first parameter is the drive (0-3), the second is the track (0-34) and the third is the sector (1-18). After that are two string variables that will hold the two halves of the 256-byte sector. In this example, A$ holds the fist 128 bytes, and B$ holds the second 128 bytes.
I only wanted to mention this so I could show a BASIC program that calculates how much space is free on a disk by reading the FAT bytes. It might look something like this:
0 'DISKFREE.BAS
10 CLEAR 384
20 INPUT "DRIVE";DR
30 DSKI$ DR,17,2,A$,B$
40 FOR I=1 TO 68
50 IF MID$(A$,I,1)=CHR$(255) THEN FG=FG+1
60 NEXT
70 PRINT "FREE GRANULES:";FG
This could, of course, be made smaller and faster. And, if you wanted to show the free space in bytes, you could just multiply the free granules (FG) variable by 2304, the side of a granule:
70 PRINT "FREE SPACE:";FG*2304;"BYTES"
Of course, the FREE(0) command could also have been used for this, even getting the value in a variable:
But what if the granule is being used by a file? If you wanted to see the values in the non-free granules used on the disk, you could modify the program as follows:
0 'DISKFREE.BAS
10 CLEAR 384
20 INPUT "DRIVE";DR
30 DSKI$ DR,17,2,A$,B$
40 FOR I=1 TO 68
50 GN=ASC(MID$(A$,I,1))
55 IF GN=255 THEN FG=FG+1 ELSE PRINT GN;
60 NEXT:PRINT
70 PRINT "FREE GRANULES:";FG
If you run that, you will probably see values that are outside of the range of a granule number (0-67). This will be explained later when we discuss the FAT in more detail.
Directory
Track 17 sectors 3-11 are used for the directory. The 1981 Color Computer Disk System manual described the directory layout on page 58 as follows:
Using DSKI$, we can write a program that will display the names of files in the directory. We can do this by reading each sector and then parsing the 32-byte directory entries.
10 CLEAR 512:DIM SP$(1)
20 INPUT "DRIVE";DR
30 ' S - SECTOR NUMBER
40 FOR S=3 TO 11
50 ' SP$(0-1) - SECTOR PARTS
60 DSKI$ DR,17,S,SP$(0),SP$(1)
70 ' P - PART OF SECTOR
80 FOR P=0 TO 1
90 ' E - DIR ENTRY (4 P/SECT.)
100 FOR E=0 TO 3
110 ' GET 32 BYTE DIR ENTRY
120 DE$=MID$(SP$(P),1+E*32,32)
130 ' FB - FIRST BYTE OF NAME
140 FB=ASC(LEFT$(DE$,1))
150 ' SKIP DELETED FILES
160 IF FB=0 THEN 250
170 ' WHEN 255, DIR IS DONE
180 IF FB=255 THEN END
190 ' PRINT NAME AND EXT.
200 PRINT LEFT$(DE$,8);TAB(9);MID$(DE$,9,3);
210 ' FILE TYPE
220 PRINT TAB(13);ASC(MID$(DE$,12,1));
230 ' BINARY OR ASCII
240 IF ASC(MID$(DE$,13,1))=0 THEN PRINT "B" ELSE PRINT "A"
250 NEXT
260 NEXT
270 NEXT
Running this program will produce output similar to the DIR command, except without the file size. Calculating the file size is more involved, and requires scanning through the FAT to find the series of granules that are allocated for the file. We’ll get to that in the next part of this series.
Bonus: File Exists?
Here’s some useless code. This routine will determine if a file exists. Rather than parse each 32 byte portion of the sectors, I decided to use INSTR() to see if the target filename string exists anywhere in the sector strings. To make sure “EMP” didn’t show up as a match for a file named “EMPTY”, I pad the target string with spaces just like they are stored in the directory.
10 INPUT "FILE NAME";F$
20 ' PAD NAME WITH SPACES
30 F$=F$+STRING$(8-LEN(F$)," ")
40 FOR S=3 TO 17
50 DSKI$ 0,17,S,A$,B$
60 IF INSTR(A$,F$) OR INSTR(B$,F$) THEN PRINT "FOUND":END
70 NEXT
This could be improved by having the process stop as soon as it finds an entry starting with 255 (no more directories after that point). To keep the code small, a simple “55 IF ASC(LEFT$(A$,1))=255 THEN END” would be enough. It might still read a sector more than it needs to, since it’s not checking every entry in the string, but that would be a way to do it with minimal code.
I’ve mentioned the BASIC Utilities from Alex Evans quite a bit lately. These Python scripts can be run on any system that has Python. I have used them under Windows 11 and macOS. In addition to having scripts that will tokenize and de-tokenize BASIC programs, it has some other interesting capabilities. One is the ability to have a BASIC source file that is written in a more user-friendly way — using labels instead of line numbers, and then only having them where needed.
Consider this example of a “guess the number” game:
NUMBER = RND(100)
PRINT "I AM THINKING OF A NUMBER FROM"
PRINT "1 TO 100. CAN YOU GUESS IT?"
LOOP: INPUT "WHAT IS YOUR GUESS";GUESS
IF GUESS > NUMBER THEN PRINT "THAT IS TOO HIGH!"
IF GUESS < NUMBER THEN PRINT "THAT IS TOO LOW!"
IF GUESS = NUMBER THEN PRINT "YOU GOT IT!":END
GOTO LOOP
This code can be processed through “coco_pack.py” and it will become a tokenized program (guess.bas.pack) that runs on a CoCo. The PACK routine will do things like rename longer variables (such as “GUESS”) to valid short ones, as well as change labels (LABEL:) in to line numbers. Obviously, it also adds line numbers to everything in between, giving the programmer one less thing to worry about.
I processed this file using “coco_pack.py -t guess.bas” to have it generate a text file (-t) rather than a tokenized file. It produced “guess.bas.pack” as a text file which looked like this:
0 A=RND(100):PRINT"I AM THINKING OF A NUMBER FROM":PRINT"1 TO 100. CAN YOU GUESS IT?"
1 INPUT"WHAT IS YOUR GUESS";B:IFB>A THENPRINT"THAT IS TOO HIGH!"
2 IFB<A THENPRINT"THAT IS TOO LOW!"
3 IFB=A THENPRINT"YOU GOT IT!":END
4 GOTO1
You can see how it combined as much as possible on each line, taking three separate lines (RND, and two PRINTs) in to one. The INPUT needed to be on its own line since it will be the target of a GOTO loop. The first comparison (is guess too high) could be moved to the end of that line, but the next ones needed to be separate lines.
The result is something that looks a bit more friendly being turned in to something much more compact. Using “coco_pack guess.bas” without “-t” would have produced a tokenized file ready to load on the CoCo.
A warning about variable and label names
In BASIC, there are certain things you cannot use for variable names. You cannot have a variable named “FOR” since that is a keyword, but you can use “FO“. (Besides, if you use a variable name longer than two characters, the rest get ignored. “FOR” should be honored as “FO” and be allowed, but the BASIC parser has already found “FOR” and turned it in to a token … I think.)
Some variables work fine on Color BASIC, but cannot be used on Extended BASIC. “FN” is allowed on Color BASIC, but you cannot use “FN” as a variable under Extended/Disk BASIC because FN is a token (see DEF FN). “AS” works fine as a variable for Color or Extended BASIC, but cannot be used under Disk BASIC because “AS” is now a keyword (see the “FIELD” command).
The Alex Evans tools need to follow similar tools, since they are parsing the line looking for BASIC keywords to tokenize. If you tried to make a label called “ASK:”, as I initially did, you will find that a “GOTO ASK” does not work — it turns in to “GOTO ASx” where X is a variable it made, since it treated “AS” as a token, and then K as a variable. This is something that Alex may be able to resolve in a future update, but for right now, keep labels and long variables to things that are not conflicting with BASIC keywords.
Here’s an example of bad stuff. Notice that you can use leading tabs/spaces to help “prettify” the code if you want, but since it is still BASIC lines, everything that needs to be on the same line in BASIC will have to be on the same line of this code: