See also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7.
The design is one row taller than will fit on the CoCo’s 32×16 text screen, but it would easily fit on the 40 or 80 column screen of the CoCo 3. For this article, I am going to stick with the standard text screen and just let it scroll one row off the top of the screen. When I have something figured out, it might only require modifying the centering code to display on the 40/80 column screen.
Let there be code!
At this stage, the design is being centered using the TAB command. Putting a “TAB(7)” at the start of each string takes up 3 bytes of programming space. It seems “TAB(” is tokenized, then there is the 3 character, followed by the “)” character. I had thought using PRINT@ might save some space, but the “@” takes a byte, then the screen position numbers follow it, and a comma is required. “PRINT@7,” takes the same amount of code space as “PRINTTAB(7)” so no savings there.
The biggest savings is going to come from eliminating the repeated use of the “* characters in the strings. Since the entire image is made up of spaces or asterisks, it could be represented by data that says how many spaces then how many asterisks then how many spaces, etc.
Here is what the image looks like centered to 32-columns:
+--------------------------------+ | * * | | ** ** | | *** *** | | **** **** | | ***************** | | *************** | | ************* | | *********** | | ********* | | *********** | | ************* | | *************** | | ***************** | | **** **** | | *** *** | | ** ** | | * * | +--------------------------------+
The first line has 11 spaces, 1 asterisks, 7 spaces, then 1 asterisk.
The second line has 11 spaces, 1 asterisks, 5 spaces, then 2 asterisks.
And so on… The first line could be represented in DATA to tell how many repeating groups of spaces and asterisks there are such as 11,1,7,1,0 (using 0, to indicate the end of the line).
For something this small, I could have just counted up the spaces and asterisks myself, but it’s more fun to spend ten minutes writing a BASIC program to do what would have taken me 60 seconds to do manually.
I converted the pattern in to DATA statements and created a program that would scan each line looking for repeated characters. When the character changes, it prints how many of the previous repeating characters it saw and resets the counter.
0 ' LOGIKER5.BAS
1 ' LC$=LAST CHAR
2 ' DC=DUPLICATE CHAR COUNT
10 READ A$:IF A$="END" THEN END
11 LC$=LEFT$(A$,1):DC=1
12 FOR I=2 TO LEN(A$)
13 IF MID$(A$,I,1)=LC$ THEN DC=DC+1 ELSE PRINT DC",";:DC=1:LC$=MID$(A$,I,1)
14 NEXT
15 PRINT DC", 0"
16 LC$=""
17 GOTO 10
50 DATA" * *"
60 DATA" ** **"
70 DATA" *** ***"
80 DATA" **** ****"
90 DATA"*****************"
100 DATA" ***************"
110 DATA" *************"
120 DATA" ***********"
130 DATA" *********"
140 DATA" ***********"
150 DATA" *************"
160 DATA" ***************"
170 DATA"*****************"
180 DATA" **** ****"
190 DATA" *** ***"
200 DATA" ** **"
210 DATA" * *"
220 DATA "END"
Rather than explain how the program works, like I often try to do, I just submitted this code to the OpenAI ChatGPT system and let it do the work for me:
This BASIC program reads in a series of strings, one at a time, and outputs the number of times each character appears in the string.
Here is a brief explanation of the code:
- Lines 10-16: This is the main loop of the program, where each string is read in, processed, and output.
- Line 11: This line initializes the
LC$
variable to the first character of the string and theDC
variable to1
.- Lines 12-14: This is a
FOR
loop that iterates through each character of the string.- Line 13: This line compares the current character with the last character (stored in
LC$
). If they are the same, it increments theDC
variable. If they are different, it outputs the value ofDC
and then resetsDC
to1
andLC$
to the current character.- Line 15: This line outputs the value of
DC
after the loop has finished processing the string.- Lines 50-220: These are the data lines that contain the strings to be processed.
- Line 220: This is the sentinel value that indicates the end of the data.
Note that this code uses a number of built-in BASIC functions, such as
– chat.openai.comLEFT$
,MID$
, andLEN
, which may not be available in all versions of BASIC. You may need to modify the code to use equivalent functionality if it is not available in the version of BASIC you are using.
Seriously, folks. You have to go play with ChatGPT. It’s amazingly fun.
OpenAI ChatGPT Digression
After showing this program to ChatGPT, I asked “is there a way to do it better?” It responded with:
There are a few ways that this routine could be improved. Here are a few suggestions:
- Use a
DO
loop instead of aGOTO
statement to control the main loop. This would make the code easier to read and maintain.- Use a
SELECT CASE
statement to handle the different cases in line 13 instead of anIF
statement. This would make the code more concise and easier to read.- Use a
DICTIONARY
orMAP
data structure to store the counts of each character instead of using separate variables for each character. This would make the code more efficient and allow it to handle an arbitrary number of characters without having to hard-code variables for each one.- Add error-checking to the code to handle cases where the input data is invalid (e.g. empty strings, strings with invalid characters, etc.). This would make the code more robust and prevent it from crashing or producing incorrect results in these cases.
Here is an example of how the code could be modified to incorporate some of these changes:
– chat.openai.com
The code it provided was not something that would work on ancient Microsoft BASIC, but it was interesting to see:
DO
READ A$
IF A$="END" THEN EXIT DO
' Initialize the character counts to 0
CHAR_COUNTS = DICTIONARY()
' Loop through each character of the string
FOR i = 1 TO LEN(A$)
ch = MID$(A$, i, 1)
IF ch = " " THEN CONTINUE ' Skip whitespace
' Increment the count for this character
IF CHAR_COUNTS.EXISTS(ch) THEN
CHAR_COUNTS[ch] = CHAR_COUNTS[ch] + 1
ELSE
CHAR_COUNTS[ch] = 1
END IF
NEXT
' Output the counts for each character
FOR ch = "A" TO "Z"
PRINT CHAR_COUNTS[ch]; ",";
NEXT
PRINT "0"
LOOP
50 DATA" * *"
60 DATA" ** **"
70 DATA" *** ***"
80 DATA" **** ****"
90 DATA"*****************"
100 DATA" ***************"
110 DATA" *************"
120 DATA" ***********"
130 DATA" *********"
140 DATA" ***********"
150 DATA" *************"
160 DATA" ***************"
170 DATA"*****************"
180 DATA" **** ****"
190 DATA" *** ***"
200 DATA" ** **"
210 DATA" * *"
220 DATA "END"
I have no idea what version of BASIC that code would work on, and the program it wrote doesn’t really look like it’s doing the same thing, but still neat. In a future article (provided they continue to give us access to this AI), I plan to share some of the interesting things it can do with BASIC programs (including writing them from scratch).
But I digress…
Driving Miss Data
When I run the program, it starts printing out rows of numbers. Since there are seventeen rows of the graphic, it prints out seventeen lines of numbers. I needed to pause the program mid-printing so I can write down the numbers before they scroll off. (A fancier way would have been to have the program write out an ASCII BASIC program with the line numbers and DATA statements already in it which I could load back later. I think I showed how I do that in an earlier article somewhere on this site.)
![](https://i0.wp.com/subethasoftware.com/wp-content/uploads/2022/12/Logiker2022-3.png?resize=625%2C518&ssl=1)
Now that we have numbers for our DATA statements, a new version of the program could be written to generate the image using them. I will make use of the STRING$ that creates a string of a specific character of a specific length…
0 ' LOGIKER6.BAS
10 CLS
15 CH=32:PRINTTAB(7);
20 READ A:IF A=-1 THEN 220
25 IF A=0 THEN PRINT:GOTO 15
30 PRINT STRING$(A,CH);
35 IF CH=32 THEN CH=42 ELSE CH=32
40 GOTO 20
50 DATA 4,1,7,1,0
60 DATA 4,2,5,2,0
70 DATA 4,3,3,3,0
80 DATA 4,4,1,4,0
90 DATA 17,0
100 DATA 1,15,0
110 DATA 2,13,0
120 DATA 3,11,0
130 DATA 4,9,0
140 DATA 3,11,0
150 DATA 2,13,0
160 DATA 1,15,0
170 DATA 17,0
180 DATA 4,4,1,4,0
190 DATA 4,3,3,3,0
200 DATA 4,2,5,2,0
210 DATA 4,1,7,1,0
215 DATA -1
220 GOTO 220
Obviously those data statements could be combined in to fewer lines, but for this version I wanted them to match the same line number the original PRINT was on. You can easily compare the results:
50 DATA" * *" 50 DATA 4,1,7,1,0 60 DATA" ** **" 60 DATA 4,2,5,2,0 70 DATA" *** ***" 70 DATA 4,3,3,3,0 80 DATA" **** ****" 80 DATA 4,4,1,4,0
Before I show you the results, can you see the flaw in my program?
I’ll give you a hint… Line 170.
Close but no cigar
My program assumes each line starts with a space, so the first value will be printed as spaces, then the next value as asterisks, and so on. This causes a problem when it gets to the row that is entirely the asterisk it reads the first number and prints it as spaces, giving me this incorrect result:
![](https://i0.wp.com/subethasoftware.com/wp-content/uploads/2022/12/Logiker2022-4.png?resize=625%2C518&ssl=1)
I can think of several ways to solve this:
- Use a different value other than 0 for “end-of-line” and make 0 mean “nothing to print, just switch to the astrisk”. That would change line 170 to be “DATA 0,17,X” (where “X” is the new end-of-line marker. This would probably require a new bit of IF logic to handle.
- Make each group of data two bytes that specifies the character to print, and how many. Printing 17 asterisks would be “17,42”. Printing four spaces would be “4,32”. This would make the program logic simpler, but would double the size of the data. Depending on how much smaller the logic is, this might be a winner. (And I can think of optimizations to that as well, such as using 0 and 1 for the data to print and just printing “32+X*10” so it prints either 32 (if the value is 0) or 42 (if the value is 1). This is normally how I would have started, but I was trying to make the data as small as possible.
- I could just encode the leading spaces at the start of each line rather than using TAB(7). By doing this, every line would start with a space. This would work for this specific challenge, but not be flexible for patterns that don’t start with a space.
For now, let’s make a quick change and try #3 by simply adding 7 to the first number in each DATA statement, and adding a 7 to line 170 which is the row that doesn’t have a space at the start. I think it would look like this:
0 ' LOGIKER7.BAS
10 CLS
15 CH=32
20 READ A:IF A=-1 THEN 220
25 IF A=0 THEN PRINT:GOTO 15
30 PRINT STRING$(A,CH);
35 IF CH=32 THEN CH=42 ELSE CH=32
40 GOTO 20
50 DATA 11,1,7,1,0
60 DATA 11,2,5,2,0
70 DATA 11,3,3,3,0
80 DATA 11,4,1,4,0
90 DATA 7,17,0
100 DATA 8,15,0
110 DATA 9,13,0
120 DATA 10,11,0
130 DATA 11,9,0
140 DATA 10,11,0
150 DATA 9,13,0
160 DATA 8,15,0
170 DATA 7,17,0
180 DATA 11,4,1,4,0
190 DATA 11,3,3,3,0
200 DATA 11,2,5,2,0
210 DATA 11,1,7,1,0
215 DATA -1
220 GOTO 220
Running this program produces the desired results! But, it has a drawback:
The data size grew. Not only did we add “7,” (two bytes) to line 170, but eleven other lines went from a 1-digit value to a 2-digit value. This means our data grew by 13 bytes. If we saved 13 bytes in the decoding routine, this is a win. If we did not, it is not an acceptable fix.
When I load the previous version of the program in to the XRoar emulator and PRINT MEM, it shows 22425 free. When I do the same with this version, I get 22416 — less memory free, so a larger program. This is bad, but the previous version is still missing the code to handle that line 170.
Perhaps, instead of adding 7 to each line to center on the screen, each line could just add 1 (so it doesn’t create two-digit values) and we can use TAB(6). That would look like this:
0 ' LOGIKER8.BAS
10 CLS
15 CH=32:PRINTTAB(6);
20 READ A:IF A=-1 THEN 220
25 IF A=0 THEN PRINT:GOTO 15
30 PRINT STRING$(A,CH);
35 IF CH=32 THEN CH=42 ELSE CH=32
40 GOTO 20
50 DATA 5,1,7,1,0
60 DATA 5,2,5,2,0
70 DATA 5,3,3,3,0
80 DATA 5,4,1,4,0
90 DATA 1,17,0
100 DATA 2,15,0
110 DATA 3,13,0
120 DATA 4,11,0
130 DATA 5,9,0
140 DATA 4,11,0
150 DATA 3,13,0
160 DATA 2,15,0
170 DATA 1,17,0
180 DATA 5,4,1,4,0
190 DATA 5,3,3,3,0
200 DATA 5,2,5,2,0
210 DATA 5,1,7,1,0
215 DATA -1
220 GOTO 220
Doing a PRINT MEM on that one shows 22421, so it is four bytes larger than the original, and still smaller than the “add 7” version. Perhaps that is good enough for now?
Combining all the lines to make a smaller program would look like this:
0 ' LOGIKER9.BAS
10 CLS
15 CH=32:PRINTTAB(6);
20 READ A:IF A=-1 THEN 220 ELSE IF A=0 THEN PRINT:GOTO 15
30 PRINT STRING$(A,CH);:IF CH=32 THEN CH=42 ELSE CH=32
40 GOTO 20
50 DATA 5,1,7,1,0,5,2,5,2,0,5,3,3,3,0,5,4,1,4,0,1,17,0,2,15,0,3,13,0,4,11,0,5,9,0,4,11,0,3,13,0,2,15,0,1,17,0,5,4,1,4,0,5,3,3,3,0,5,2,5,2,0,5,1,7,1,0,-1
220 GOTO 220
Better! But we can make it more better.
In the next installment, we will do something that I learned from studying the Atari 2600’s Adventure program…
To be continued…
I’m not going to try this and I’m just “thinking out loud”, but I wonder if, since it is a mirror image, you could get away with basically including half the data and printing it twice? Like, look at the narrowest horizontal line in the center and pretend that everything below it doesn’t exist…that would be in your data, and for each line, it would print once, then somehow print the same lines again in reverse order? Or, maybe you’d draw that top half from the data (including the center line) and then loop through the top 8 lines in reverse order, peek the contents of the 32 characters of each line and print them again? I’m not sure if the size of the logic would outweigh the data size savings or not.
I have one version coming up that tries to do that. And the image is mirrored left/right and top/bottom so you can save only 1/4 the data.
Pingback: CoCo Nation News stories for Episode 291, December 10, 2022 -
Pingback: CoCo Nation News stories for Episode 292, December 10, 2022 -