Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 2

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 the DC variable to 1.
  • 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 the DC variable. If they are different, it outputs the value of DC and then resets DC to 1 and LC$ 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 LEFT$, MID$, and LEN, 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.

– chat.openai.com

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:

  1. Use a DO loop instead of a GOTO statement to control the main loop. This would make the code easier to read and maintain.
  2. Use a SELECT CASE statement to handle the different cases in line 13 instead of an IF statement. This would make the code more concise and easier to read.
  3. Use a DICTIONARY or MAP 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.
  4. 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.)

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:

I can think of several ways to solve this:

  1. 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.
  2. 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.
  3. 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…

2 thoughts on “Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 2

  1. Jason Pittman

    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.

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.