CoCo DISK BASIC sequential and direct access files

On a tape-based Color Computer (Color BASIC or Extended Color BASIC), you could write data out to a tape file by opening device #-1 for Output (“O”) like this:

0 'TAPEWRIT.BAS
10 OPEN "O",#-1,"MYFILE"
20 PRINT #-1,"THIS IS IN THE FILE"
30 PRINT #-1,"SO IS THIS"
40 PRINT #-1,"AND THIS IS AS WELL"
50 CLOSE #-1

By having a tape inserted in the recorder, and PLAY+RECORD pressed, when that program runs the cassette relay in the computer would click on, starting the tape motor, and the three lines of text would be written to the file. The file would look like this:

THIS IS IN THE FILE(enter)
SO IS THIS(enter)
AND THIS IS AS WELL(enter)

After rewinding the tape and pressing PLAY, running this program would open the same file for Input (“I”) and read and display that data:

0 'TAPEREAD.BAS
10 OPEN "I",#-1,"MYFILE"
20 IF EOF(-1)=-1 THEN 60
30 INPUT #-1, A$
40 PRINT A$
50 GOTO 20
60 CLOSE #-1

NOTE: In line 30, if you have Extended Color BASIC, if reading strings use LINE INPUT instead of INPUT. That will allow lines that have commas, quotes, and other special characters in it, which INPUT will not.

In line 20, the EOF function is used to check if there is more data in the file. If you knew exactly how much data was in the file (like a configuration file that always has the same information), you could just do that many INPUTs. If the amount of data is not known, EOF must be used to avoid an end-of-file error when you try to read past the end of data.

Now you have a simple program that would read and print as many lines as are in the file.

Disks Do More

When a floppy disk controller is added, Disk BASIC comes along for the ride. While cassettes use device #-1, disks can use devices #1 to #15. This allows multiple files to be open at the same time, and on different drives. (Disk BASIC supported four floppy drives simultaneously.)

We can change the above sequential file programs to work on a disk system just by changing device #-1 to be device #1:

0 'DISKWRIT.BAS
10 OPEN "O",#1,"MYFILE.TXT"
20 PRINT #1,"THIS IS IN THE FILE"
30 PRINT #1,"SO IS THIS"
40 PRINT #1,"AND THIS IS AS WELL"
50 CLOSE #1

…and…

0 'DISKREAD.BAS
10 OPEN "I",#1,"MYFILE.TXT"
20 IF EOF(1)=-1 THEN 60
30 LINE INPUT #1, A$
40 PRINT A$
50 GOTO 20
60 CLOSE #1

The only change I made other than the device number was adding an extension to the filename. Since a disk file can have a three-character extension, I used “.TXT”. If you leave off the extension, it will be created as “.DAT” for a data file.

With a sequential file, each entry is expected to have a carriage return at the end. You can write out a single line, like the earlier example:

PRINT #1,"THIS IS AN ENTRY"

…or even write out numeric data, separated by commas:

PRINT #1,A,B,C,D

In the disk file will either be “THIS IS AN ENTRY” with an ENTER at the end, or the three numbers with an ENTER at the end.

5 A=1:B=2:C=3
10 OPEN "O",#1,"NUMBERS"
20 PRINT #1,A,B,C
30 CLOSE #1
40 '
50 OPEN "I",#1,"NUMBERS"
60 INPUT #1,X,Y,Z
70 PRINT X;Y;Z
80 CLOSE #1

As the name implies, data is sequential — one after the next. If you had a file with 1000 entries in it, and wanted to get to the 1000th entry, you would have to read through the 999 entries first.

Direct Access

A far more powerful form of disk access is Direct. This allows you to create a file that is not made of arbitraty strings that end in a carriage return. Instead, the file can be a set of records (of a size you specify). This is done by using the “D”irect access mode and specifying the record size at the end of the OPEN.

With a direct access file, you can specify a record size, and then write or read to any record you want. (This is usually called “random access” these days.)

Here is an example that creates a file with 32-byte records, and writes three entries to it:

0 'DISKWRIT2.BAS
10 OPEN "D",#1,"MYFILE2.TXT",32
20 PRINT #1,"THIS IS IN THE FILE"
25 PUT #1,1
30 PRINT #1,"SO IS THIS"
35 PUT #1,2
40 PRINT #1,"AND THIS IS AS WELL"
45 PUT #1,3
50 CLOSE #1

For a direct access file, using PRINT (or the WRITE command, which seems to do the same thing), the data goes in to a buffer and won’t be written to the disk until PUT is used to tell it which record of the disk file to write it to. The file would look like this:

          11111111111222222222233
 12345678901234567890123456789012
+--------------------------------+
|THIS IS IN THE FILE             | record 1
+--------------------------------+
|SO IS THIS                      | record 2
+--------------------------------+
|AND THIS IS AS WELL             | record 3
+--------------------------------+

(There would also be an ENTER at the end of each line, normally.)

The program to read back and display the records looks like this:

0 'DISKREAD2.BAS
10 OPEN "D",#1,"MYFILE2.TXT",32
20 FOR R=1 TO LOF(1)
30 GET #1,R
40 LINE INPUT #1,A$
50 PRINT A$
60 NEXT
70 CLOSE #1

Above, you see the addition of “,32” to specify 32 byte records, and the use of LOF which is the length of file (number of records). In our example, this should be 3, matching the three records we wrote in the previous example.

To load a record in to the buffer, GET is used, followed by a LINE INPUT to read it in to a string.

Now if 1000 entries had been written in to a direct access file, we could retrieve any record we wanted just by using GET #1,42:LINE INPUT A$ or whatever.

Breaking Records

A record can be treated like a string of a maximum size. When you PRINT or WRITE that record, it must be smaller than the record size, and have the ENTER at the end. The ENTER is needed for INPUT/LINE INPUT to know where the end of that record is.

But, you can also break a record up in to specific entries. For instance, first name, middle initial, and last name. This is done using the FIELD command. You tell it the buffer (device) number and how many bytes to assign to a variable. For example, if you wanted a 32 byte record to look like this:

          11111            11111111
 12345678901234|1|12345678901234567
+--------------+-+-----------------+
| First Name   |I| Last Name       |
+--------------+-+-----------------+

…with fourteen (14) characters for the First Name, one (1) character for the Initial, and fifteen (17) characters for the Last Name, and you wanted them in variables F$, I$, L$, you would use:

FIELD #1,14 AS F$,1 AS I$,17 AS L$

(I wanted to use FN$ for first name but FN is a reserved keyword used for the DEF FN function and it cannot be used for a variable.)

If you do that, you no longer use INPUT/LINE INPUT. Instead, when you GET the record, it loads the appropriate bytes in to the variables for you! Nifty!

And, to write it, you reverse the process by loading the variables (using LSET or RSET) and then using PUT. Also nifty! Here is a program that adds three First/Initial/Last records:

0 'DISKWRIT3.BAS
10 OPEN "D",#1,"NAMES.DAT",32
15 FIELD #1,14 AS F$,1 AS I$,17 AS L$
20 LSET F$="ALLEN":LSET I$="C":LSET L$="HUFFMAN"
25 PUT #1,1
30 LSET F$="ARTHUR":LSET I$="P":LSET L$="DENT"
35 PUT #1,2
40 LSET F$="TRICIA":LSET I$="M":LSET L$="MCMILLAN"
45 PUT #1,3
50 CLOSE #1

If you try to just assign the variable and then PUT, it doesn’t work (or at least, did not for me). The example in the Disk BASIC manual show this being done with LSET and RSET to assign those variables to the buffer (left or right justified). After the write, the disk file looks something like this:

          11111            11111111
|12345678901234|1|12345678901234567
+--------------+-+-----------------+
|ALLEN         |C|HUFFMAN          | record 1
+--------------+-+-----------------+
|ARTHUR        |P|DENT             | record 2
+--------------+-+-----------------+
|TRICIA        |M|MCMILLAN         | record 3
+--------------+-+-----------------+

Using LSET puts the entry in to the left, and using RSET would right justify it instead. (What is this for, anyone know?) RESET would make the file look like this:

          11111            11111111
 12345678901234|1|12345678901234567
+--------------+-+-----------------+
|         ALLEN|C|          HUFFMAN| record 1
+--------------+-+-----------------+
|        ARTHUR|P|             DENT| record 2
+--------------+-+-----------------+
|        TRICIA|M|         MCMILLAN| record 3
+--------------+-+-----------------+

…and here is the program that reads them back and displays them:

0 'DISKREAD3.BAS
10 OPEN "D",#1,"NAMES.DAT",32
15 FIELD #1,14 AS F$,1 AS I$,17 AS L$
20 FOR R=1 TO LOF(1)
30 GET #1,R
40 PRINT F$;" ";I$;". ";L$
60 NEXT
70 CLOSE #1

With this in mind, we could make a program that dumps out all the bytes in a file by making the record size one (1) byte, like this:

0 'DIRECT.BAS
10 '
11 ' CREATE A FILE
12 '
20 OPEN "O",#1,"FILE.TXT"
30 PRINT #1,"DON'T PANIC!"
40 CLOSE #1
100 '
101 ' OPEN DIRECT ACCESS
102 '
110 OPEN "D",#1,"FILE.TXT",1
115 FIELD #1,1 AS BT$
120 NR=LOF(1)
130 PRINT "RECORDS: ";NR
140 FOR R=1 TO NR
150 GET #1,R
170 PRINT ASC(BT$);
180 NEXT
190 CLOSE #1

The important part are lines 100-190. You could remove the earlier lines that just make a test file, and modify this to “dump” any file you want. Here’s a simple HEX dump program:

0 'HEXDUMP.BAS
10 LINE INPUT "FILENAME:";F$
20 OPEN "D",#1,F$,1
30 FIELD #1,1 AS BT$
40 OF=0:C=0
50 FOR R=1 TO LOF(1)
60 IF C=0 THEN PRINT:PRINT USING"####   ";OF;
70 GET #1,R
80 BT=ASC(BT$)
90 IF BT<&H10 THEN PRINT "0";
100 PRINT HEX$(BT);" ";
110 C=C+1:IF C>7 THEN C=0
120 OF=OF+1
140 NEXT
150 CLOSE #1

There is more you can do with Disk BASIC, so here are a few references to get you started:

Until next time…

Shell script to rename odd numbered files

Recently, I accepted a Fiverr project to needed to sharpen and enhance a video. My Topaz Labs Video AI was unable to do anything useful, so I tried converting the entire video to photos and running through through their photo tools like Photo AI. Using the open source ffmpeg, I did it like this:

Movie to Images using ffmpeg

ffmpeg -i “input.mp4 frames-full/frame%d.jpg

That command creates jpg image files in a subfolder called “frames-full” for every frame of the original video. In my case, the original video was shot at 59.94 frames per second and created a ton of image files.

Processing 20,000+ images was going to take days, so I decided to just process it as 29.97 frames per second (standard US video rate) which would cut my processing time in half. To do this, I needed to delete every other image. I was able to use the “find” command to search for any matching filename that ended in 0, 2, 4, 6 or 8 (an even number):

find . -name "frame[0-9]*[02468].jpg"

One neat thing about the modern macOS file system is you can make a duplicate of a folder before doing something dangerous like command line deletes! The duplicate doesn’t take up extra space (beyond a bit of directory overhead) so that let me experiment over and over until I figured it out.

Now I had frame1.jpg, frame3.jpg, frame5.jpg and so on. After processing these files, I would need to re-assemble them using ffmpeg at the 29.97 frame rate. Here is the command for that:

ffmpeg -framerate 29.97 -I frames-full/frame%d.jpg -pix_fmt yuv420p output.m4v

Unfortunately, it expects all the files to be sequentially numbered (1, 2, 3, 4) and would not work with every other file missing.

To fix that, I needed a script that would rename files down from…

frame1.jpg
frame3.jpg
frame5.jpg
frame6.jpg
frame9.jpg

…to…

frame1.jpg
frame2.jpg (was 3)
frame3.jpg (was 5)
frame4.jpg (was 7)
frame5.jpg (was 9)

A bit of trail and error led me to this simple script that divides the number by 2, and since there is no floating point, all I needed to do was add one to the number and then do the division. 3 became 1+3 which is 4, which divided by 2 became 2:

1 -> 1+1 = 2 / 2 = 1
3 -> 3+1 = 4 / 2 = 2
5 -> 5+1 = 6 / 2 = 3
7 -> 7+1 = 8 / 2 = 4
9 -> 9+1 = 10 / 2 = 5

That let me simply rename the original number to the new number and get what I wanted. Here is the script, though it is hard coded to the number of files I needed. You’d have to change that since it’s not smart enough to figure this out (like, “stop when you find a file that doesn’t exist”). Also, it starts at 3 since frame1.jpg would be renamed to frame1.jpg which probably produces an error:

#!/bin/bash

# find . -name "frame[0-9]*[02468].jpg"

# Delete even-numbered files
for ((i=3;i<=26999;i=i+2))
do
    file="frame${i}.jpg"
    j=$((i/2+1))
    if [ -f "$file" ]; then
        echo "Renaming $file"
        mv "$file" "frame${j}.jpg"
    fi
done

I just wanted to post this here in case anyone else ever needs to do the same thing…

Until next time…

XRoar emulator and loading and saving to tape and disk

Here is a quick tutorial on an often-asked question about the XRoar emulator:

How do you load/save a program to/from disk/tape?

– Folks

Cassette Tapes

Since the XRoar emulator can emulate a floppy drive, there’s no real reason to mess with virtual cassette tapes unless you just want the experience, need the extra memory (disk systems have about 2K less memory), or are using software that does not support floppy disks.

But, if you still want to save your BASIC programs to a virtual cassette, and load it back, here are the steps.

From the manual, please note:

The tape used for writing is considered separate to the read tape (this is an emulator-friendly approach to prevent overwriting your programs, though it would have been possible with two cassette decks).

– https://www.6809.org.uk/xroar/doc/xroar.shtml#Cassettes

That is likely the cause of confusion for many/most/all who try to use this the first time.

To save your program to tape, you must first mount (or create new) an Output Tape:

File -> Cassette -> Output Tape…

That will pop up a file explorer, and you can browse to an existing tape image, or select a location and create a new tape like “tape.cas” (where “cas” means cassette).

Once that is there, you can save your program as normal using CSAVE (or CSAVEM for machine language):

10 PRINT "HELLO, TAPE!"
20 GOTO 10

CSAVE "HELLO"

That file is now saved to the virtual tape. On a real cassette system, you would have to rewind the tape before you can load the program back in. But, if you try to use “File -> Cassette -> Rewind Output Tape” you still won’t be able to load. That tape is output only. You would just be rewinding it so you can overwrite whatever is there if you save something new.

Instead, you mount this tape image as an Input Tape:

File -> Cassette -> Input Tape…

You then can browse and select the “tape.cas” you made earlier. Now you can load the program using CLOAD (or CLOADM for machine language) and it will be back in memory:

CLOAD "HELLO"

LIST
10 PRINT "HELLO, TAPE!"
20 GOTO 10
OK

To reload, you need to to “File -> Cassette -> Rewind Input Tape”.

It appears you can have the same tape mounted for both Input and Output, but you need to re-mount the input each time you want to see the updated files on the Output tape.

In my test, I mounted an Output tape and then did a CSAVE”HELLO” of the program. After that, I mounted the tape for Input and did a CLOAD to get it back. I then did CSAVE”HELLO2″ and CSAVE”HELLO3″ to add two more copies to the Output tape.

Rewinding the Input tape did not get me those new files, but re-mounting (File -> Cassette -> Input Tape…”) and selecting the same .cas file again did let me load all three.

I’d enjoy XRoar having a “one tape” mode that worked like a real cassette deck. That would probably be much easier for folks to start using.

Here’s a quick video showing the basic process:

Floppy Disks

XRoar handles floppy disks more like real hardware, but as of version 1.2, it still defaults to having disk images being temporary. If you mount and use a disk image, when you shut down, all your changes are lost. It was intended for casual users who would be mounting disks and running programs, as opposed to folks programming and saving new data. A menu option (or entry in the xroar.conf config file) changes disks images to “write back” and work like normal.

Here are the steps:

First, you need an existing disk image, or you can create one:

File -> Drive 1 -> New Disk…

Browse to where you want the new disk image to be saved, and give it a name like “mydisk.dsk”

Drive 1 (which is 0 to DISK BASIC) is now ready, but just like a real floppy, you must format it before you can use it. Use the disk initialize command to do this:

DSKINI 0

You should now be able to type “DIR” and see an empty disk. BUT, if you save things to it, those changes will be lost (or maybe not even be able to save, if the disk is write protected and gives you a ?WP ERROR just like a real disk did when you taped over the write protect notch).

Turn on “Write Enable” to make the disk NOT write protected, and turn on “Write Back” to ensure anything you write will actually go back on the disk and not be discarded on exit:

Now you can save and load programs as normal:

10 PRINT "HELLO, DISK!"
20 GOTO 10
SAVE "HELLO"
DIR

HELLO   BAS  0 B 1
OK

As long as Write Enable is checked, the disk won’t be write protected. As long as Write Back is checked, your changes should save when you exit XRoar (or eject the disk).

The next time you startup XRoar, you can use the Ctrl-1 shortcut to mount Drive 1, and just browse to your “mydisk.dsk” image and start using it.

You will still need to check Write Back every time, unless you add that to your xroar.conf config file. You can set that up so it automatically mounts disk images, tape files, etc. as well as setting default machine, memory size, TV mode (simulated, RGB monitor, etc.)

But that’s a topic for another article.

Here is a quick video showing the disk steps:

Have fun!

Until next time…

CoCo 3 and the 1988 Houston Boat Show

I graduated high school in 1987. Even though the CoCo 3 had come out the year before, I had remained with my CoCo 2. Sub-Etha Software co-founder, Terry, got his CoCo 3 first. I remember him asking me questions that I could not answer because he had lots of new features I never had seen.

By 1988 I had my own CoCo 3. I don’t recall when I got it, but it had to be in 1987 since I was writing CoCo 3 programs in January. One such program (or programs) was to display video titles. My father was producing a video which would be running at the Callaway Boatworks booth at the 1988 Houston Boat Show.

A few years ago, when I was going through 400+ floppy disks to archive them to a CoCoSDC, I found this disk but it had sector errors. While I could RUN some of the programs, many would not load due to disk errors.

Since then, I discovered a hard drive copy of the disk I had made to my KenTon RGB-DOS drive system. This image was intact! I wanted to go through the titles “some day” and see what all I had done.

“Some day” happened last week. I used the toolshed “decb” utility to pull each BASIC program off the disk image and convert it to ASCII. I then looked through all of them in Visual Studio Code on my Mac. Certain programs would daisy-chain to other programs, using a RUN”NEXTPROG” command at the end. Some paused for a key (at a black screen) before drawing the titles. The BREAK key (and CoCo 3 ON BRK command) was used to skip to the next program (why did I do it that way?).

I was able to come up with a list of two segments of daisy-chained titles, and then the rest were just one-off titles on their own. I recorded the two sequences, and all the separate images, and posted the video to YouTube:

1988 Houston Boat Show graphics done on a CoCo 3 in BASIC.

Some internet searching shows that Callaway Boatworks no longer exists. A few others in the video have since disappeared from the market, but the Houston Boat Show continues to this day.

I wrote them to see if they could provide a vendor list from 1988. I did not expect a response, but got one! They sent me a scan of the exhibitors from that year’s show, and I can now locate the two spots that Callaway Boatworks had that year.

A huge thank you to Lynette M at the boat show for taking time to get me this information. My father passed away a few years ago, so I did not have him to ask about these things.

More to come…

Changing default view for 360 video on Insta360 X2/X3 and others…

When you export 360 video to a format that can be uploaded to YouTube, Facebook, or other online service that supports 360 video, you get a wide, warped video file that looks like this:

For the Insta360 ONE X2 and X3 cameras, the front facing camera (the one opposite of the preview screen) will be the focal point of the video. In this case, it’s the entrance of the Whalebone Grill in the Awa (water) realm of the new-for-2022 Lost Island Themepark in Waterloo, Iowa. (This new park is pretty amazing with its backstory and unique themes.)

But, what if you wanted the 360 view to default to a different view when first played? Unfortunately, the Insta360 mobile app and desktop apps do not provide a way to do this (currently; folks have been asking about it for years, so maybe one day…). Often, the advice is to put the video in a video editor like Premier or Final Cut Pro and change it there.

Some quick web searching led me to this REDDIT post with a comment from user glitch007 explaining a way to use the free ffmpeg utility to reprocess 360 video and set the initial view:

ffmpeg has command line options to specify the X/Y adjustments (yaw and pitch) for the 360 video export. You can import the original MP4 file and export it out as a new MP4 with the view changed. If, for example, I wanted my Whalebone Grill video to start with folks facing the seating area, I could change that and it would look like this:

The command line option to do this is:

ffmpeg -i "input.mp4" -vf v360=e:e:yaw=90:pitch=0:roll=0 "output.mp4"

In this example, “yaw=90” tells it to change the X view by 90 degrees. You could pass in 180 to make the video face the opposite direction. The “pitch” controls the looking up and down, and “roll” controls tilt (I believe; I haven’t actually tested it).

For the curious, the command line options mean:

  • -i … Input file.
  • -vf … Video filter (and any parameters it needs).
  • v360 video filter:
    • e … Equirectangular projection (the type of 360 format the video is in).
    • yaw / pitch / roll … Set rotation for the output video. Values in degrees.

glitch007 shared a timesaver where you specify a start and end section of the video and can quickly process just a snippet so you can see the results before doing the entire video. Using “-ss” sets the starting section, and adding “-to” lets you specify the ending second:

ffmpeg -i "input.mp4" -ss 00:03 -to 00:08 -vf v360=e:e:yaw=90:pitch=0:roll=0 "output.mp4"

If you run that, you’d get a 5 second clips covering seconds 3 to 8 of the video, and could look at that and see how the view is. This allows quickly making changes to yaw/pitch/roll to get what you want.

I used the ffmpeg command line utility to do this, but there may be Windows/Mac programs that put a graphical user interface on it, making it easier for folks to use. If you know of a good one, please leave a link in the comments.

Thank you, glitch007, for this tip!

Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 7

See also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7.

Updates:

  • 2022-12-30 – Update to Jason’s final version to make it two bytes smaller.

In this final (?) installment, I wanted to share some other approaches that were taken to by members of the CoCo community draw this:

…including one that immediately was smaller than the version I did.

Rick Adams – PDP8/I

Early on, a version was shared by legendary CoCo programmer Rick Adams. His version was not for the CoCo – he chose to do it “in a very primitive BASIC, BASIC8 on a simulated PDP8/I running the TSS8 OS”…


0 'RICK ADAMS
12 FOR B = 1 TO 4
14 GOSUB 2000
20 NEXT B
22 C = 0
24 D = 0
30 FOR I = 1 TO 9
32 READ A, B
34 GOSUB 1000
36 NEXT I
50 FOR B = 4 TO 1 STEP -1
52 GOSUB 2000
58 NEXT B
200 DATA 0, 17, 1, 15, 2, 13, 3, 11, 4, 9, 3, 11, 2, 13, 1, 15, 0, 17
300 STOP
1000 PRINT TAB(A);
1010 FOR J = 1 TO B
1020 PRINT "*";
1030 NEXT J
1040 PRINT TAB(A + B + C);
1050 FOR J = 1 TO D
1060 PRINT "*";
1070 NEXT J
1080 PRINT
1090 RETURN
2000 A = 4
2002 D = B
2010 C = 9 - 2 * B
2020 GOSUB 1000
2030 RETURN
2046 END

I am unfamiliar with the BASIC on this machine, but at least it doesn’t require using “LET“. This version can run on the CoCo as well, and correctly reproduces the pattern.

Jim Gerrie – MC-10/CoCo

Next, take a look a this one by MC-10 BASIC-meister, Jim Gerrie:

Jim Gerrie’s fancier solution

His approach uses DATA statements and then draws the star in an interesting way.

Jason Pittman

In the comments on an earlier installment, Jason shared his attempt. His approach was realizing that the shape was just “four overlapping right triangles.”

1 FORX=64TO416STEP32:L=X/32:T$=STRING$(L,42):PRINT@X-28,T$;:PRINT@(X-19-L),T$;:PRINT@544-X+4,T$;:PRINT@557-X-L,T$;:NEXT:GOTO1

This version is just 100 bytes! Due to the CoCo’s 32 column screen being too short, it doesn’t draw the top and end lines of the pattern, so it wouldn’t meet the challenge requirements. To fix that, he needed to add an IF:

1 FORX=32TO416STEP32:L=X/32:T$=STRING$(L,42):PRINT@X-28,T$;:PRINT@(X-19-L),T$;:IF X>32THEN PRINT@544-X+4,T$;:PRINT@557-X-L,T$;
2 NEXT
3 GOTO3

Since the CoC 3 also has a 40×24 and 80×24 screen, the entire pattern could fit on those screens. Version three looked like this:

1 WIDTH40:FORX=1TO13:L$=STRING$(X,42):LOCATE14-X,X:PRINTL$;:LOCATE14-X,18-X:PRINTL$;:LOCATE5+L,X:PRINTL$;:LOCATE5,18-X:PRINTL$;:NEXT:GOTO1

That one is a mere 88 bytes! And, the GOTO1 at the end is just to make it keep redrawing, else it stops near the top and would print the “OK” in the middle of the pattern.

I’d say the “WIDTH40:” is not required, since you could just say “run this from the 40 column screen.” And, to keep the loop, starting on LINE 0 allows just saying “GOTO” with no line number:

0 FORX=1TO13:L$=STRING$(X,42):LOCATE14-X,X:PRINTL$;:LOCATE14-X,18-X:PRINTL$;:LOCATE5+L,X:PRINTL$;:LOCATE5,18-X:PRINTL$;:NEXT:GOTO

By my count, that turns in to 83 bytes! Amazing.

UPDATE: L. Curtis Boyle pointed out there was an unnecessary “+L” left in the code, which can be removed to make this 81 bytes. More amazing!

0 FORX=1TO13:L$=STRING$(X,42):LOCATE14-X,X:PRINTL$;:LOCATE14-X,18-X:PRINTL$;:LOCATE5,X:PRINTL$;:LOCATE5,18-X:PRINTL$;:NEXT:GOTO

Here is what it looks like, though I paused it to capture the full image:

Please read his comments to part 1 for more background and earlier versions he shared.

I’m really blown away by this.

Are we done? Is this as small as it gets?

Unless there are more ideas, I think that is the end.

Merry Christmas, everyone!

Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 6

See also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7.

Just when I thought I was out… they pull me back in.

Michael Corleon, Godfather III

Sometimes clever isn’t as good as brute force. In this installment, I’ll present a hybrid approach to the challenge of displaying the Logiker 2022 holiday image.

Instead of writing code to handle each section of the pattern, perhaps taking the simpler approach of just doing Run Length Encoding (sorta) might be smaller. I do not know where I first learned about RLE, but I implemented a simple version in Sub-Etha Software’s graphical “CoCoFEST Simulation” text adventure back in the early 1990s. The images in the graphical adventure were large, and I came up with a simple way to represent repeated data in them. (I do not know if someone gave me this idea, or if I worked it out on my own; in those pre-Internet days, knowledge was alot more organic.)

Basically, RLE looks for repeating sequences and replaces them with a count value and the sequence. The Wikipedia article gives this example:

Consider a screen containing plain black text on a solid white background. There will be many long runs of white pixels in the blank space, and many short runs of black pixels within the text. A hypothetical scan line, with B representing a black pixel and W representing white, might read as follows:

WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWBWWWWWWWWWWWWWW

With a run-length encoding (RLE) data compression algorithm applied to the above hypothetical scan line, it can be rendered as follows:

12W1B12W3B24W1B14W

– https://en.wikipedia.org/wiki/Run-length_encoding

Above, if there are twelve “W” characters in a row, you would encode the value 12 followed by the “W”.

I wrote a simple program that will input a string and then scan it looking for repeating characters. When it finds a new character, it prints the count of the previous characters as well as what character that was.

0 ' RLE.BAS
10 LINE INPUT "STRING:";A$
20 LC=ASC(LEFT$(A$,1)):L=1
30 FOR I=2 TO LEN(A$)
40 C=ASC(MID$(A$,I,1))
50 IF C<>LC THEN PRINT L;LC:L=0:LC=C
60 L=L+1:NEXT
70 PRINT L;LC

I pasted this code in to chat.openai.com and it correctly recognized it as an RLE routine! Here is how it described my program:

This looks like a BASIC program that encodes a string using run-length encoding (RLE). RLE is a simple form of data compression, where runs of repeated characters are replaced by a count and the character. For example, the string “AAABBCCC” would be encoded as “3A2B3C”.

The program starts by prompting the user to input a string, and then assigns the first character of the string to the variable LC (which stands for “last character”). The variable L is set to 1, and then a FOR loop is used to iterate through the remaining characters in the string.

For each character in the string, the program checks if it is different from the last character. If it is, it prints the length of the previous run and the character itself, and then resets the length and last character variables. If the character is the same as the last character, the length is incremented. This continues until the end of the string is reached, at which point the final run is printed.

For example, if the input string is “AAABBCCC”, the output would be:

3 65
2 66
3 67

This indicates that there were three instances of the character “A” (ASCII value 65), two instances of the character “B” (ASCII value 66), and three instances of the character “C” (ASCII value 67).

– chat.openai.com

Wow. Just wow.

Here is an example of the program running:

RLE (Run Length Encoding) in BASIC

Since the Logiker Challenge image only contains spaces and asterisks, we don’t need a full RLE to encode it. I chose to use alternating numbers to indicate how many spaces, then how many asterisks.

Ultimately, I tried to save code space by breaking the image in to sections, and using two main routines to print that section within FOR/NEXT loops.

But … is that overkill? Instead of using multiple PRINT routines, what if I only needed one? By expanding the image data so each line covers the entire width of the CoCo’s 32-column screen, I could do away with the “end of line” markers in the data, and replace them with a larger series of spaces that goes from the end of the data on that line to the start of the data on the next line:

[12345678901234567890123456789012]
"           *       *            "
"           **     **            "
"           ***   ***            "
"           **** ****            "
"       *****************        "
"        ***************         "
"         *************          "
"          ***********           "
"           *********            "
"          ***********           "
"         *************          "
"        ***************         "
"       *****************        "
"           **** ****            "
"           ***   ***            "
"           **     **            "
"           *       *            "
[12345678901234567890123456789012]

Above, at the end of the first line’s asterisks, there are 12 spaces to the end of that line. For the next line, there are 11 spaces to get to the start of the next asterisks. That means after printing the last asterisks in line 1 we can just print 23 spaces and be at the start of the next line.

Assuming we start with a SPACE then an ASTERISK then a SPACE and do on, the data for the first two lines would look like this:

11 - print11 spaces
1 - print 1 asterisk
7 - print 7 spaces
1 - print 1 asterisk
23 - print 23 spaces (to move to the start of data in the second line)
2 - print 2 asterisks
5 - print 5 spaces
2 - print 2 asterisks
...and so on...

I was going to convert all the PRINT lines of the original version I started with to DATA statements and write a program to count this for me, but that sounded complicated. I just counted, and came up with the following numbers:

11 1 7 1 23 2 5 2 3 3 23 4 4 4 23 5 16 15 16 17 18 16 5 2 3 3 23 4 4 4 23 5 18 2 5 2 1 1 7 1

I could store those in a DATA statement:

DATA 11,1,7,1,23,2,5,2,3,3,23,4,4,4,23,5,16,15,16,17,18,16,5,2,3,3,23,4,4,4,23,5,18,2,5,2,1,1,7,1

But, that takes up alot of room. There is a comma between each number, so for 50 numbers we’d be adding 49 commas, basically doubling the size of the data. Also, two digit numbers like 10 take up two bytes. I thought about using HEX numbers (0-15 turns in to 0-F) but the data has some values that are larger than 15 (the highest value that fits in a single character of a HEX value).

HEX is BASE-16 (0-F to represent 0-15) and what I really need is at least BASE-23 (0-23, the larger number I need). Since there are 26 letters in the alphabet, I could use all of them and get BASE-26 leaving me room to spare!

If A=1, B=2 and so on, the above series of numbers could be turned in to:

K A G A W B E B W C C C W D A D S Q P O R M T K V I V K T M R O P Q S D A D W C C C W B E B W A G A

I could then turn those in to DATA:

DATA K,A,G,A,W,B,E,B,W,C,C,C,W,D,A,D,S,Q,P,O,R,M,T,K,V,I,V,K,T,M,R,O,P,Q,S,D,A,D,W,C,C,C,W,B,E,B,W,A,G,A

…and read them as a string (READ A$) and then convert that string to a number by subtracting 63 (ASCII for A is 64, so if I read an A and get 64, subtracting 63 turns that in to 1):

READ A$
V=ASC(A$)-64

While this saves a byte for every number that was two digits, the extra code to convert from ASCII to a number may be larger than what we saved.

Since we have 49 commas, we could get rid of those and add code to parse a long string. As long as that code is smaller than 49 bytes, we come out ahead.

DATA KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA

Now I could read that as a string and parse it in to numbers:

0 'STRTONUM.BAS
10 READ A$
20 FOR I=1 TO LEN(A$)
30 PRINT ASC(MID$(A$,I,1))-64;
40 NEXT
50 DATA KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA

And, if I want to use that series of numbers in a loop that prints alternating strings of spaces and asterisks, I don’t even need to bother with it being in a DATA statement. I could just embed it directly in the MID$() command and hard code the lengthof the string, like this:

0 'STRTONUM2.BAS
20 FOR I=1 TO 50
30 PRINT ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I,1))-64;
40 NEXT

And if I can do that, the only thing left is to figure out when to print a space and when to print an asterisks.

An easy way to do that is looking at the I variable in the FOR/NEXT loop. As it counts from 1 to 2 to 3 to 4, I can use AND to check bit 1. For odd numbers, that bit is set. For even numbers, it is not.

0 = 0000000
1 = 0000001
2 = 0000010
3 = 0000011
4 = 0000100
5 = 0000101
...and so on...

This means a simple check for “I AND 1” in an IF statement can help me decide which to print. Something like:

IF (I AND 1) THEN PRINT space ELSE PRINT asterisk

That gets me to something like this:

0 ' LOGIKER-ALPHA2.BAS
10 FORI=1TO50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 IF I AND 1 THEN PRINT STRING$(L,32); ELSE PRINT STRING$(L,42);
40 NEXT

Perhaps I can get rid of one of those PRINT STRING$ commands… Since I know a space is ASCII 32 and an asterisk is ASCII 42, I could start with the 32 and add 10 if it’s the asterisk case. To do that, I need to see the result that comes back from AND:

PRINT 1 AND 1
1

PRINT 2 AND 1
0

So if the condition is TRUE (bit 1 is set, meaning the value is odd), I get a 1. If the condition is FALSE (bit 1 is clear, meaning the value is even), I get a 0.

Since I want to print spaces on the odd values, I need to use the 1 (odd) to mean 32, and the 0 (even) to mean 42. I’ll reverse my logic a bit and always start with 42 (asterisks) and multiply it by 10 times the result of (I AND 1). Something like this should work:

0 ' LOGIKER-ALPHA3.BAS
10 FOR I=1 TO 50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 PRINT STRING$(L,42-(I AND 1)*10);
40 NEXT

And that gives me the pattern I want, with far less code. I can remove unneeded spaces and combine everything in to one line and see how big it is.

Unneeded Spaces

A quick thing about unneeded spaces. There are spaces that BASIC itself doesn’t need, but the tokenizer that turns what you type in to the program DO need. For example:

FOR I=100 TO 5000

None of those spaces are needed, because BASIC knows where a keyword ends (FOR) and can tell the variable will be whatever is there before the “=”. The same is true for the numbers, since it can tell where a number ends and know to look for “TO”.

FORI=100TO5000

BUT, if you were using variables in that loop…

FOR I=B TO E

…and you took the spaces out:

FORI=BTOE

…how does BASIC know what your variable is? Is it “B”? Or “BT”? Or maybe “BTOE”? You will get an “?SN ERROR” if you try that because BASIC sees a non-number after the “=” and switches to parsing it as if it were a variable. To get around this, we have to put a space after it like this:

FORI=B TOE

That allows the tokenizer to work fine.

However If you were manually creating the BASIC program by packing bytes together in a file, you could omit that space and it will run just fine. Utilities such as Carl England’s CRUNCH do this trick to save a byte. BUT, if you were to CRUNCH the program then try to EDIT that line, you’d no longer have code that would run because updating the line requires it to be re-tokenized. #TheMoreYouKnow

Why is that important?

I mention this because in my above program, I wanted to remove spaces from this line:

30 PRINT STRING$(L,42-(I AND 1)*10);

I can remove all but one, since I need a space between “I” and “AND” for the same reason I just mentioned:

30 PRINTSTRING$(L,42-(I AND1)*10);

But… instead of “I AND 1” I could change it to “1 AND I” and get the same result, but no longer need the space because BASIC can tell where a number stops:

30 PRINTSTRING$(L,42-(1ANDI)*10);

And that, my friends, is how you save one more byte.

Would it be possible to also get rid of those parenthesis? Right now, I need to take my asterisk value (42) and subtract either 0 or 10. I need the results of “1 AND I” multiplied by 10, and if I removed the parens…

42-1 AND I*10

…BASIC would do the math first (42-1 and I*10) and if “I” was 3 at the time, I would get this:

42-1 AND 3*10
41 AND 30

…and that’s not at all what we want.

Can it be done? I moved things around but it really looks like the result of “1 AND I” has to be in parens. Can you figure a way to save those two bytes?

With that said, I present this version:

10 FOR I=1 TO 50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 PRINT STRING$(L,42-(1ANDI)*10);
40 NEXT

…which can be packed in to this version:

10 FORI=1TO50:L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64:PRINTSTRING$(L,42-(1ANDI)*10);:NEXT

And that shows up as 114 bytes!

Oh, one thing I should also mention — during last year’s challenge, a comment was made about how ASC() works. If you give it a string, it returns the ASCII value of the first character. So ASC(“A”) returns 64, just like ASC(“ALLEN”) does. They said instead of using MID$(A$,I,1) to get one character, you can leave off that third parameterand MID$ returns the rest of the string:

A$="HELLO"
PRINT MID$(A$,2,1)
C

PRINT MID$(A$,2)
ELLO

If we were trying to print or use just one letter, we need that third parameter. But since I am passing it in to ASC, I could still give it the longer string and it would work fine:

PRINT ASC("E")
69

PRINT ASC("ELLO")
69

Thus, I can leave off that third parameter and save the two bytes that “,1” took up.

Neat!

Are we done? Can we save any more?

Until next time…

Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 5

See also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7.

The challenge continues. From humble beginnings of using PRINT, to fancier methods of encoding the image as a series of spaces and asterisks, we eventually ended up with an even fancier method that used only 1/4 of the image data to represent the entire symmetrical image.

That approach could work for any image that is symmetrical vertically and horizontally, and typically general purpose routines are not as small as custom routines that know what they will be doing.

Knowing what we now know…

WIth that said, looking at this image, there is another shortcut that I missed:

The entire image is centered over one column… This means the amount of spaces on the left is unimportant — we just need to center the following lines:

*       *
**     **
***   ***
**** ****
*****************
***************
*************
***********
*********
***********
*************
***************
*****************
**** ****
***   ***
**     **
*       *

And, since we know it’s symmetrical both vertically and horizontally, we really only need to worry about this:

*    | <- 1 asterisk, 4 spaces
**   | <- 2 asterisks, 3 spaces
***  | <- 3 asterisks, 2 space
**** | <- 4 asterisks, 1 space
*****************| 17
***************| 15
*************| 13
***********| 11
*********| 9

I’m not sure what the pattern is as I type this, but I am expecting there is one. Here is a quick program that prints the rows of the shape using FOR/NEXT loops (uncentered):

0 ' LOGIKER13.BAS
10 FOR I=1 TO 4
20 PRINT STRING$(I,"*");STRING$(1+(4-I)*2," ");STRING$(I,"*")
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 PRINT STRING$(I,"*")
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 PRINT STRING$(I,"*")
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 PRINT STRING$(I,"*");STRING$(1+(4-I)*2," ");STRING$(I,"*")
120 NEXT

If each of those lines were centered, we’d have our shape. Let’s try that by creating a string for the row, and then using the LEN() of that string to know how to center it using TAB().

0 ' LOGIKER14.BAS
10 FOR I=1 TO 4
20 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
25 PRINT TAB(16-LEN(A$)/2);A$
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 A$=STRING$(I,"*")
55 PRINT TAB(16-LEN(A$)/2);A$
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 A$=STRING$(I,"*")
85 PRINT TAB(16-LEN(A$)/2);A$
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
115 PRINT TAB(16-LEN(A$)/2);A$
120 NEXT
130 GOTO 130

That produces our desired shape (though it does leave a blank line at the end, which our original version avoided by having a semi-colon on the PRINT and just breaking lines when we went to the next one).

The first thing I see it that the centering code on line 25, 55, 85 and 115 is the same. Subroutine!

0 ' LOGIKER15.BAS
10 FOR I=1 TO 4
20 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
25 GOSUB 150
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 A$=STRING$(I,"*")
55 GOSUB 150
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 A$=STRING$(I,"*")
85 GOSUB 150
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
115 GOSUB 150
120 NEXT
130 GOTO 130
150 PRINT TAB(16-LEN(A$)/2);A$:RETURN

Next, we see that the string building code for the top and bottom are the same, so 20 and 110 are the same (it’s the value of I that changes how it prints), and then 50 and 80 are the same. Subroutines!

0 ' LOGIKER16.BAS
10 FOR I=1 TO 4
20 GOSUB 200
25 GOSUB 150
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 GOSUB 250
55 GOSUB 150
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 GOSUB 250
85 GOSUB 150
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 GOSUB 200
115 GOSUB 150
120 NEXT
130 GOTO 130
150 PRINT TAB(16-LEN(A$)/2);A$:RETURN
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):RETURN
250 A$=STRING$(I,"*"):RETURN

Next, I notice the subroutines of 200 and 250 both have the centering PRINT called after them, so maybe we change it up a bit…

0 ' LOGIKER17.BAS
10 FOR I=1 TO 4
20 GOSUB 200
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 GOSUB 250
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 GOSUB 250
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 GOSUB 200
120 NEXT
130 GOTO 130
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN

What else? The FOR/NEXT loops are basically all the same, except for the start and end value and the step value… Maybe we could come up with a way to have only one, and feed it those values using DATA statements?

10 FOR I=1 TO 4
...
40 FOR I=17 TO 9 STEP-2
...
70 FOR I=11 TO 17 STEP 2
...
100 FOR I=4 TO 1 STEP-1

500 DATA 1,4,1
510 DATA 17,9,-2
520 DATA 11,17,2
530 DATA 4,1,-1

If they all went to the same GOSUB routine this would be easy, but they don’t. The go 200, 250, 250, 200. We could add a fourth element in the DATA that tells it which routine to go to and “IF X=1 THEN GOSUB Y ELSE GOSUB Z” or something. That adds more code. Perhaps we don’t need the DATA since we know it alternates? Still, we’d have to track it ourselves with an IF or something. For now, let’s just try this:

0 ' LOGIKER18.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT I
60 NEXT J
70 GOTO 70
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0

And that still produces your original shape. But is it any smaller?

In part 4, we had a version that (using my default XRoar emulator running DISK EXTENDED COLOR BASIC) showed 22499 bytes free after loading. This new version shows 22567 bytes free. So yes, it is smaller! And, we can pack those lines and make it even smaller than that. (And NEXT doesn’t near the variable — in fact, using “NEXT I” is slower than just saying “NEXT” so I’ll remove those here as well.)

0 ' LOGIKER19.BAS
10 FOR J=1 TO 4:READ A,B,C,D:FOR I=A TO B STEP C:IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT:NEXT
70 GOTO 70
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN:DATA 1,4,1,0,17,9,-2,1,11,17,2,1,4,1,-1,0

That version shows me 22609 free, which is even smaller — and we could still make this a bit smaller by getting rid of unnecessary spaces in the code.

Side note: I am being lazy and just showing the BASIC “PRINT MEM” values rather than calculating the actual size of the program. On my configuration, 22823 is how much memory is there on startup. So, 22823-22609 shows that this program is 214 bytes. It uses more memory for the strings when running, but I don’t think that matters for this challenge.

What else can we do to save a few bytes? Well, STRING$() takes two parameters. The first is the count of how many times to repeat the second parameter. The second parameter can be a quoted character like “*”, or a number like 42 (the ASCII value of the asterisk). 42 is one by smaller than “*” so we can do that as well as use 32 (the ASCII value for space) instead of ” “:

200 A$=STRING$(I,42)+STRING$(1+(4-I)*2,32)+STRING$(I,42):GOTO 300
250 A$=STRING$(I,42)

Another thing we know is that in the shape there are always the same number of spaces before the top and bottom sections, so we really don’t need to center it. We could just hard code a PRINT TAB for that instead of building a string and calling a center subroutine:

200 PRINT TAB(11);STRING$(I,42);STRING$(1+(4-I)*2,32);STRING$(I,42)
210 RETURN

The middle section is similar. Since we know the length, we could calculate how many spaces to tab using that number:

250 PRINT TAB(16-I/2);STRING$(I,42)
260 RETURN

And that removes a subroutine, leaving us with this (not line packed yet):

0 ' LOGIKER20.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT
60 NEXT
70 GOTO 70
200 PRINT TAB(11);STRING$(I,42);STRING$(1+(4-I)*2,32);STRING$(I,42)
210 RETURN
250 PRINT TAB(16-I/2);STRING$(I,42)
260 RETURN
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0

Two FOR/NEXT loops, a READ, an IF, and two PRINT subroutines.

Maybe we don’t need those subroutines, now that we have an “IF” in line 40 that decides what to do?

0 ' LOGIKER21.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN PRINT TAB(11);STRING$(I,42);STRING$(1+(4-I)*2,32);STRING$(I,42) ELSE PRINT TAB(16-I/2);STRING$(I,42)
50 NEXT
60 NEXT
70 GOTO 70
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0

That’s an ugly line 40, but it got rid of two GOSUBs and two RETURNS. Plus every line takes up 5 bytes, so by removing two of those (each subroutine must start on its own line) we saved even more.

Not only is this smaller, it’s faster, too, since it doesn’t have to spend time seeking through the program looking for a subroutine line number for each line it prints.

If we pack this version, removing unnecessary spaces and semicolons and such, and remove the “GOTO” loop (the challenge does not require the screen to be cleared, and says it can return to a prompt after running), we get:

0 ' LOGIKER22.BAS (don't include this line)

1 FORJ=1TO4:READA,B,C,D:FORI=A TOB STEPC:IFD=0THENPRINTTAB(11)STRING$(I,42)STRING$(1+(4-I)*2,32)STRING$(I,42)ELSEPRINTTAB(16-I/2)STRING$(I,42)
2 NEXT:NEXT:DATA1,4,1,0,17,9,-2,1,11,17,2,1,4,1,-1,0

151 bytes of BASIC code!

Is there more that can be done? Comment with your ideas! I think I’m out of them, here.

Until next time…

Wokwi online Arduino/ESP32 simulator

Oh, Wokwi! Where have you been all my life? Or at least where were you a few years ago when I was working on Arduino projects?

I initially started using this Sub-Etha Software blog for Arduino projects. I did crazy things like porting a Color BASIC program to Arduino C, and fun things like figuring out how to write Pac-Man using the Arduio TVOut library. Eventually, I merged my interests in the old Radio Shack Color Computer (CoCo) with Arduino (and ESP8266/ESP32) and experimented with a serial port sound player and WiFi modem.

Prototype “Sir Sound” sound module for the CoCo (or anything with a serial port, actually).

So … many … wires.

At the time, I was hoping to find some kind of Arduino emulator so I could write and test code without hooking up hardware. I found nothing.

But that seems to have changed. I just learned about Wokwi which allows one to “simulate IoT projects in your browser.” In a nutshell, it’s a website that has a code editor (which appears to be Microsoft Visual Studio Code), compiler, and virtual target hardware like Arduino and ESP32 devices. It even supports some add-on hardware, like buttons, LCD displays, LEDs and more.

Here’s a project someone made that simulates an Arduino hooked to a numeric keypad and LCD display:

And you can build and run it right there!

There is a library of devices that are supported, and you can add them to your project and wire them up to the computer’s I/O pins. For example, as I write this blog post, I opened up a starter project that is an Arduino and a two-line LCD display. I then added a pushbutton to it.

I could then move the button to where I wanted it, then click on the connectors and draw wire lines between it and I/O pins on the Arduino. By hooking one side to an I/O pin, and the other to ground, I could then modify the program to read that button and, for this example, increment a counter while the button is being held done.

It’s just that easy! I had no idea!

The files can be downloaded and used on real hardware, or you can make an account and log back in to continue working on them. (It has an unusual way to log in — it sends you an e-mail and you click a link to log in, rather than having a username and password. This seems to mean I cannot log in from any system that I don’t have my e-mail account configured on, but I do see options for using a Google or Github login.)

I hope you find this as useful as I already have.

Happy coding!

Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 4

See also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7.

The story so far…

In the beginning Logiker offered up a programming challenge to produce this simple image:

From its humble beginnings as a bunch of PRINT statements, to a DATA driven output routine, we have now ended up with code that can print the top half of the symmetrical image by only having data to describe one quarter of the image.

When we last left off, I had code that would read this data, print the left half, then print the right half, mirroring the same data. It worked well, but only printed the top half of the image.

I showed that code to OpenAI and asked how it would accomplish the task of reversing the data to print the bottom portion:

To make the program print the rows in reverse order after it has finished printing all of the rows, you will need to make some changes to the code. One way to do this would be to add another loop after the main loop that reads the data from the DATA statements and prints the rows in reverse order. You can use the “REVERSE” statement in BASIC to reverse the order of the rows in the DATA statements, and then use the same code as in the main loop to read the data and print the rows. This will cause the program to print the rows in the original order, and then again in reverse order.

– chat.openai.com

I’ve never heard of the REVERSE keyword, and the CoCo’s BASIC doesn’t have it. But if I did have it, this task would be pretty easy. I informed the A.I. that I had no such command, and it said:

If your BASIC does not have a REVERSE command, you can still print the rows in reverse order by using a different approach. One way to do this would be to store the data from the DATA statements in an array, and then loop through the array in reverse order to print the rows. In BASIC, you can use the “DIM” statement to create an array, and the “READ” statement to read data from the DATA statements into the array. You can then use a loop to iterate through the array in reverse order, using the same code as in the main loop to print the rows. This will cause the program to print the rows in the original order, and then again in reverse order.

– chat.openai.com

That’s the approach I was thinking of taking, so… okay!

Side Note: I did ask the A.I. to modify my program to do this, and it gave me something that would have been a good starting point for the rest of this article. I could have then discussed things with the A.I. a bit more and encouraged it to evolve the program until it met the requirements. (One issue with BASIC is that the A.I. inevitably tries to do things from other dialects of BASIC that Color BASIC does not have, such as the REVERSE command.) I hope to demonstrate one of these co-programming sessions in a future article series, provided access to ChatGPT remains available to me.

I changed my routine so it loaded all the DATA values in to a dimension. Since I knew how many entries there would be (27), I could eliminate the need for having a -1 to mark the end of data.

Then, in the PRINT routine, instead READing A, B and C and looking at them, I’d use the D() dimension variable as D(x), D(x+1) and D(x+2) to get to the same three values.

At the end of each line (three numbers, or two numbers followed by a zero), I would increment my index by three so it starts at the next set of numbers in the array.

I used a new variable, M, to indicate the mode (forward or reverse). When it reaches the end of the data (D goes past the 27 entries), it sets that variable to 1, and other code now runs decrementing the index by 3 each time until it reaches the top (0).

Here is my new code:

0 ' LOGIKER11.BAS
10 CLS:DIM D(26)
11 FOR I=0 TO 26:READ D(I):NEXT:D=0:M=0
15 PRINTTAB(6);
20 PRINT STRING$(D(D),32);
25 IF D(D+2)=0 THEN PRINT STRING$(D(D+1)*2-1,42);STRING$(D(D),32);:GOTO 40
30 PRINT STRING$(D(D+1),42);STRING$(D(D+2)*2-1,32);STRING$(D(D+1),42);
40 IF M=0 THEN D=D+3:IF D<27 THEN PRINT:GOTO 15 ELSE M=1
41 IF D>0 THEN PRINT:D=D-3:GOTO 15
50 DATA 5,1,4
60 DATA 5,2,3
70 DATA 5,3,2
80 DATA 5,4,1
90 DATA 1,9,0
100 DATA 2,8,0
110 DATA 3,7,0
120 DATA 4,6,0
130 DATA 5,5,0
220 GOTO 220

OpenAI describes it as follows:

This program is similar to the previous ones, but it uses a slightly different approach to read the data from the DATA statements and print the rows. The program starts by defining an array D to store the data from the DATA statements, and then reads the data into the array. The program then enters a loop that prints the rows of the pattern. In each iteration of the loop, the program prints a row of the pattern by printing a string of spaces, followed by a string of asterisks, and then another string of spaces. The number of spaces and asterisks in each string is determined by the data in the array.

After the program has printed all of the rows in the original order, it enters another loop that prints the rows in reverse order. In each iteration of the loop, the program prints the same row as in the previous loop, but in reverse order. This causes the program to print the rows of the pattern in the original order, and then again in reverse order. However, without more information about the data in the DATA statements, it is not possible to say exactly what the output of the program would look like.

– chat.openai.com

Nicely done, A.I.

When I run this program, I get the desired output!

It is, of course, slower than the original brute-force PRINT version. But, we are going for size. And speaking of size, lets try to pack this down as much as possible and see where we stand:

0 ' LOGIKER12.BAS
10 CLS:DIM D(26):FOR I=0 TO 26:READ D(I):NEXT:D=0:M=0
15 PRINTTAB(6);STRING$(D(D),32);:IF D(D+2)=0 THEN PRINT STRING$(D(D+1)*2-1,42);STRING$(D(D),32);:GOTO 40
30 PRINT STRING$(D(D+1),42);STRING$(D(D+2)*2-1,32);STRING$(D(D+1),42);
40 IF M=0 THEN D=D+3:IF D<27 THEN PRINT:GOTO 15 ELSE M=1
41 IF D>0 THEN PRINT:D=D-3:GOTO 15
50 GOTO 50:DATA 5,1,4,5,2,3,5,3,2,5,4,1,1,9,0,2,8,0,3,7,0,4,6,0,5,5,0

Loading the first version gives me 22431 bytes free. Loading the second gives me 22499 – about 68 bytes smaller! Compare that to the original brute-force PRINT version (which was 22309 free), we have saved 190 bytes so far.

And, we could remove spaces, get rid of the REM comment at the start, and save even more.

But is that enough for the challenge? One thing the challenge says it you are not required to clear the screen, and you can return to an OK/Ready prompt. That means I could remove the CLS and the GOTO loop at the end, saving even more.

But saving even more is not enough. I think there’s a few more things we can do, especially now that I understand what it takes to draw this shape.

Until next time, take a look at what I have done in LOGIKER11.BAS and see what suggestions you can come up with.