Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

42 Astoundingly Useful Scripts and Automations for the Macintosh

Work faster and more reliably. Add actions to the services menu and the menu bar, create drag-and-drop apps to make your Macintosh play music, roll dice, and talk. Create ASCII art from photos. There’s a script for that in 42 Astounding Scripts for the Macintosh.

SuperBASIC for the TRS-80 Color Computer

Jerry Stratton, March 18, 2020

Color Computer 2 Reverse video

The CoCo, like most computers of its era, did not have lowercase built in.

There are a lot of things I’m nostalgic for about the early era of home computers. The cameraderie. The home-brew spirit. The assumption that we were going to control the computer rather than the computer control us—there were so many cartoons making fun of the Pavlovian response to computer notifications that seem sadly prophetic today. One thing I am not nostalgic for is programming in the BASIC of the era. I wrote a lot about that in Learning to program without BASIC. BASIC programs then were a sludgy melt of undifferentiated words and numbers.

But I have an old computer, and I want to program it. It is the Tandy Color Computer 2, and it is not an easy computer to write programs on. Even the TRS-80 Model 1 could show 1,024 characters at a time, 16 lines of 64 characters. The Color Computer had half that, at 16 lines of 32 characters each. There is no holding anything but the simplest subroutine onscreen intact.

That’s part of why I wrote basicize, to help me write programs for emulators using a modern text editor on a modern computer. Not having to deal with line numbers was also a major impetus, and labeling made it a lot easier to see where the code was going. But I mainly use basicize for modifying existing code. I’ll take the code, organize it into sections, and label them. This does not help structure the code, nor does it make the code self-commenting. The variables are still one to two characters and there are no loops other than FOR/NEXT and GOTO.

It occurred to me while writing a BASIC program to examine the video modes of the CoCo 21 that I should be able to easily convert long variables into short ones using Perl. It’s practically what Perl is designed for. And simple loops? It’s BASIC because it’s basic. That should be easy.

What I ended up with is a preprocessor that makes BASIC fun again. It will take something like this:

[toggle code]

  • loop
    • loop
      • %key$=inkey$
    • endloop unless (%key$="")
    • print asc(%key$)
  • endloop

and generate something like this2:

  • 10 KE$=INKEY$:IF KE$="" THEN 10
  • 20 PRINT ASC(KE$)
  • 30 GOTO 10

Here’s an example of what an actual program in superBASIC looks like. This code draws a simple clock on the screen, and then moves the three hands appropriately.

[toggle code]

  • # display clock with minute, hour, and seconds hand
  • # Jerry Stratton
  • # hoboes.com/coco
  • %xcenter% = 128
  • %ycenter% = 96
  • %second% = 0
  • cls
  • wrap-center \^LHigh resolution or \^Lcolors?
  • print
  • loop
    • %mode$=inkey$
  • endloop unless (%mode$ <> "H" and %mode$ <> "C")
  • wrap-center Enter the hour and minute to start the clock at. Once the clock circle forms, press any key to start the clock.
  • print
  • loop
    • input "Hour:minute"; %hour%, %minute%
  • endloop unless (%hour% < 0 or %minute% < 0 or %hour% > 23 or %minute% > 59)
  • if %hour% >= 12 then %hour% -= 12
  • switch
  • case (%mode$ = "H")
    • pmode 4,1
    • color 2,1
    • %delay% = 297
  • case (%mode$ = "C") #colors are orange(0), white(1), green(2), magenta(3)
    • pmode 3,1
    • color 3
    • %delay% = 303
  • endswitch
  • #switch to graphics screen
  • pcls
  • screen 1,1
  • circle(%xcenter%,%ycenter%),%ycenter%-1
  • circle(%xcenter%,%ycenter%),%ycenter%-2
  • circle(%xcenter%,%ycenter%),%ycenter%-3
  • %firstTime% = 1
  • color 2
  • pause
  • loop
    • #second hand
    • line(%xcenter%,%ycenter%)-(%secondX%,%secondY%),preset
    • %length% = 88
    • %degrees% = %second%
    • gosub getEndpoint
    • %secondX% = x
    • %secondY% = y
    • line(%xcenter%,%ycenter%)-(%secondX%,%secondY%),pset
    • switch
    • case (%second% = 0 or %second%-1 = %minute%) #minute hand
      • line(%xcenter%,%ycenter%)-(%minuteX%,%minuteY%),preset
      • %length% = 77
      • %degrees% = %minute%
      • gosub getEndpoint
      • %minuteX% = x
      • %minuteY% = y
      • line(%xcenter%,%ycenter%)-(%minuteX%,%minuteY%),pset
    • case (%second% = 0 and %minute% = 0 or %second%-1 = %hour%*5 or %firstTime% = 1) #hour hand
      • line(%xcenter%,%ycenter%)-(%hourX%, %hourY%),preset
      • %length% = 55
      • %degrees% = %hour%*5
      • gosub getEndpoint
      • %hourX% = x
      • %hourY% = y
      • line(%xcenter%,%ycenter%)-(%hourX%,%hourY%),pset
    • endswitch
    • circle(%xcenter%,%ycenter%),8
    • for i = 1 to %delay%:next i
    • %second%++
    • switch
    • case (%second% = 60)
      • %second%=0
      • %minute%++
    • case (%minute% = 60)
      • %minute%=0
      • %hour%++
    • case (%hour% = 12)
      • %hour%=0
    • endswitch
    • %firstTime% = 0
  • endloop
  • #endpoint using length and degrees
  • sub getEndpoint
    • #adjust clock seconds to radians
    • %degrees% *= 6
    • %degrees% -= 90
    • %radians% = %degrees%/57.29577951
    • #calculate cartesian x and y from polar coordinates
    • x = %length% * cos(%radians%) + %xcenter%
    • y = %length% * sin(%radians%) + %ycenter%
  • endsub

Turn this code into Extended Color BASIC for the CoCo by piping the output of superbasic to a file:

  • superbasic clock.txt > CLOCK.BAS

Here’s the BASIC code it generates from that file:

[toggle code]

  • 10 REM DISPLAY CLOCK WITH MINUTE,
  • 11 REM HOUR, AND SECONDS HAND
  • 20 REM JERRY STRATTON
  • 30 REM HOBOES.COM/COCO
  • 40 XC = 128
  • 50 YC = 96
  • 60 SE = 0
  • 70 CLS
  • 80 PRINT "   hIGH RESOLUTION OR cOLORS?"
  • 90 PRINT
  • 100 MO$=INKEY$:IF MO$ <> "H" AND MO$ <> "C" THEN 100
  • 110 PRINT "  ENTER THE HOUR AND MINUTE TO"
  • 111 PRINT "  START THE CLOCK AT. ONCE THE"
  • 112 PRINT " CLOCK CIRCLE FORMS, PRESS ANY"
  • 113 PRINT "    KEY TO START THE CLOCK."
  • 120 PRINT
  • 130 INPUT "HOUR:MINUTE"; HO, MI:IF HO < 0 OR MI < 0 OR HO > 23 OR MI > 59 THEN 130
  • 140 IF HO >= 12 THEN HO=HO-12
  • 150 IF MO$ = "H" THEN PMODE 4,1:COLOR 2,1:DE = 297
  • 160 REM COLORS ARE ORANGE(0),
  • 161 REM WHITE(1), GREEN(2),
  • 162 REM MAGENTA(3)
  • 170 IF MO$ = "C" THEN PMODE 3,1:COLOR 3:DE = 303
  • 180 REM SWITCH TO GRAPHICS SCREEN
  • 190 PCLS
  • 200 SCREEN 1,1
  • 210 CIRCLE(XC,YC),YC-1
  • 220 CIRCLE(XC,YC),YC-2
  • 230 CIRCLE(XC,YC),YC-3
  • 240 FT = 1
  • 250 COLOR 2
  • 260 A$=INKEY$:IF A$ = "" THEN 260
  • 270 REM SECOND HAND
  • 280 LINE(XC,YC)-(SX,SY),PRESET
  • 290 LE = 88
  • 300 DG = SE
  • 310 GOSUB 1000
  • 320 SX = X
  • 330 SY = Y
  • 340 LINE(XC,YC)-(SX,SY),PSET
  • 350 REM MINUTE HAND
  • 360 IF SE = 0 OR SE-1 = MI THEN LINE(XC,YC)-(MX,MY),PRESET:LE = 77:DG = MI:GOSUB 1000:MX = X:MY = Y:LINE(XC,YC)-(MX,MY),PSET
  • 370 REM HOUR HAND
  • 380 IF SE = 0 AND MI = 0 OR SE-1 = HO*5 OR FT = 1 THEN LINE(XC,YC)-(HX, HY),PRESET:LE = 55:DG = HO*5:GOSUB 1000:HX = X:HY = Y:LINE(XC,YC)-(HX,HY),PSET
  • 390 CIRCLE(XC,YC),8
  • 400 FOR I = 1 TO DE:NEXT I
  • 410 SE=SE+1
  • 420 IF SE = 60 THEN SE=0:MI=MI+1
  • 430 IF MI = 60 THEN MI=0:HO=HO+1
  • 440 IF HO = 12 THEN HO=0
  • 450 FT = 0
  • 460 GOTO 270
  • 470 END
  • 1000 REM ENDPOINT USING LENGTH
  • 1001 REM AND DEGREES
  • 1010 REM ADJUST CLOCK SECONDS TO
  • 1011 REM RADIANS
  • 1020 DG=DG*6
  • 1030 DG=DG-90
  • 1040 RA = DG/57.29577951
  • 1050 REM CALCULATE CARTESIAN X
  • 1051 REM AND Y FROM POLAR
  • 1052 REM COORDINATES
  • 1060 X = LE * COS(RA) + XC
  • 1070 Y = LE * SIN(RA) + YC
  • 1080 RETURN

For a cheap clock, it’s fairly accurate. Since the Color Computer does not have a built-in real-time clock, this uses a FOR/NEXT loop to delay between each iteration of the second hand. This works because on these old computers the program you’re running is the only process on the computer. Which means that the code takes a predictable amount of time.

Because it takes longer to draw high-resolution clock hands than medium-resolution ones, the delay is shorter for the high-resolution clock.3

This code is a lot easier to read and follow than standard old-school BASIC. Variables have self-commenting names, subroutines are named, and loops, and their conditions, are clearly visible. SuperBASIC converts these subroutines, loops, and switch/case sections to standard BASIC. Unlike basicize, superbasic doesn’t support GOTO at all. I haven’t found any need for it yet. Of course, its loops and so on are handled under the hood by conditional GOTO statements.

Loops can be nested, as indefinitely as your memory allows.4 Switches cannot be nested. I don’t think it will be difficult, but I haven’t had a need for it yet. I don’t like to add complexity where it isn’t needed, and I also prefer to have a necessary example to code new features from instead of a made-up example. I also wanted to better understand how a multi-section block would work without the complications of multiple levels. I am not a language programmer, and am learning this as I go.

High-Resolution clock on the Color Computer 2

The wondrous high resolution mode of the CoCo!

SuperBASIC will attempt to compress CASE blocks and LOOPS into one line if it can do so—basically, if there are no IF/THEN lines in that block of code.5 For short LOOP blocks, it will successfully create the standard 10 A$=INKEY$:IF A$="" THEN 10 type of loop.

If you look at the help text, you’ll see some features that aren’t used in either CLOCK.BAS or MODE.BAS, but I’m using them in other programs I’m writing, which I hope to post soon. SuperBASIC really has made writing BASIC fun again. For example, I don’t use it in the clock (it would make timing wonky) but I have two versions of Hunt the Wumpus: one for use on my Color Computer with a speech synthesizer, and one for use in XRoar, which does not have a speech synthesizer. When creating the Color Computer, with speech, code, I use:

  • superbasic --switch speech cwumpus.txt > CWUMPUS.BAS

The option --switch speech turns on the speech synthesizer code.

The exception is LOOP WHILE. I thought I needed this, but every instance of it turned out to make more sense as ENDLOOP UNLESS, so I’m not currently using this in any code. I have a suspicion that LOOP WHILE will be most useful for iterative tasks such as reducing an existing variable to a standard range, such as:

[toggle code]

  • loop while (%degrees%>=90)
    • %degrees%-=90
  • endloop

Currently, one-line loops will turn this into:

  • 100 IF DE>=90 THEN DE=DE-90:GOTO 100

Much in the same way that CASE statements are collapsed to a single line when possible. However, I don’t have any code that uses loops with opening conditionals, so don’t be surprised if I alter how conditions work at the start of a loop, or remove starting-loop conditions entirely, once I have real examples in hand.

SuperBASIC also supports a handful of directives that are replaced directly with BASIC code. PAUSE creates a FOR/NEXT loop that assumes $seconds is the number of iterations needed for one second. I took this number directly from the Color Computer manual. WRAP takes the line of given text and prints it to the screen so that no word is chopped unless it’s longer than 32 characters. It can also WRAP-CENTER. DATA creates a series of DATA/ENDDATA lines that top out at 32-characters each for readability. You can see the latter in the included MODE.BAS code, which uses DATA statements to store the names of the screen colors.

  • #border/foreground names
  • DATA
  • BLACK
  • GREEN
  • ORANGE
  • ENDDATA
  • #background names
  • DATA
  • GREEN
  • ORANGE
  • CYAN
  • BROWN
  • ENDDATA

This becomes:

  • 3100 REM BORDER/FOREGROUND NAMES
  • 3110 DATA BLACK,GREEN,ORANGE
  • 3200 REM BACKGROUND NAMES
  • 3210 DATA GREEN,ORANGE,CYAN
  • 3220 DATA BROWN

Variable names begin with the percent symbol, and end with either the percent symbol (for numeric variables) or the dollar sign (for strings). Because single-letter variables are so ubiquitous in boilerplate code, SuperBASIC assumes that your programs will use single-letter variables. This means you can still write, for example, FOR I=1 TO 13:NEXT I. When and if you use variables directly, always use single-character variables to avoid conflicting with an auto-generated variable. SuperBASIC creates only two-character variables, and in fact disallows a one-character variable name for its own use; %a$ not only isn’t a valid SuperBASIC variable, it won’t be detected as one. This isn’t necessary, I just find it completely unreadable.

If you want to see what variables SuperBASIC is creating, add --variables to the command line:

  • superbasic clock.txt --variables

This lists the superBASIC variables in the clock program, along with their ECB variable names:

VARIABLES:

DE:%delay%
DG:%degrees%
FT:%firstTime%
HO:%hour%
HX:%hourX%
HY:%hourY%
LE:%length%
MI:%minute%
MO$:%mode$
MX:%minuteX%
MY:%minuteY%
RA:%radians%
SE:%second%
SX:%secondX%
SY:%secondY%
XC:%xcenter%
YC:%ycenter%

It also lists the subroutines and the line number they ended up on. The clock only has one subroutine:

GETENDPOINT:1000

Here are the various things that superBASIC supports. You can see the same list by typing superbasic --help.6

SuperBASIC variables
%ABC123_$:string variable
%ABC123_%:numeric variable
%ABC123_%++/--:increment/decrement variable
%ABC123_%+=/*=/-=//=:add to, multiply by, subtract from, divide into variable
SuperBASIC statements
IF (condition) THEN:start IF block
ELSE:inverse portion of IF block
ENDIF:end IF block
LOOP:start loop; also LOOP WHILE (condition)
CONTINUE:go to start of current loop
ENDLOOP:end loop; also ENDLOOP UNLESS (condition)
SUB name:start subroutine
ENDSUB:end SUB
SWITCH:start a series of CASEs
CASE (condition):a CASE inside of a SWITCH
CASE (DEFAULT):a CASE that applies to every case that reaches it
BREAK:go to end of SWITCH or LOOP; also BREAK IF (condition)
ENDSWITCH:end a series of CASEs
DATA [maxlines ##]:begin a series of DATA lines wrapped to 32 characters
ENDDATA:end a DATA series
%DATASETS!:the number of data/enddata sets in the program
PAUSE[ seconds]:pause for that many seconds or until key pressed
WRAP <text>:wrap text to PRINT statements of 32 characters; also WRAP-CENTER
#:REMARK line; # REMARKs can also be added to end of CASEs
\^L:force the next letter to be lower-case on the CoCo
\^L…\^Q:force the enclosed letters to be lower-case on the CoCo
Preprocessor statements
//:comment lines not included in BASIC
/* … */:comment block not included in BASIC
#IFDEF switch:start block of code only for particular switch
#ENDIFDEF:end block of switch-only code

I’m not trying to hide the underlying BASIC, just make it easier to create while making the code much more maintainable. That’s why the loops use ENDLOOP UNLESS instead of ENDLOOP IF. The latter would require reversing the logic of the underlying IF/THEN line. For example, ENDLOOP UNLESS (%mode$<>"H" AND %mode$<>"C") produces the BASIC line IF MO<>"H" AND MO<>"C" THEN <start-of-loop>; but ENDLOOP IF (%mode$ = "H" OR %mode$ = "C") would require inverting the logic, to IF NOT(MO="H" OR MO="C") THEN <start-of-loop>.

It’s not a huge deal, I’d just like to have what’s written be what’s used where possible. It makes it harder to see how the underlying BASIC code works if the underlying BASIC code is the inverse of the code that created it. I can easily see myself changing my mind. I can easily see preferring ENDLOOP IF, or, less easily, LOOP UNTIL.

When programming for the CoCo on my Macintosh, I find the XRoar emulator very useful for testing code as I write, and the CoCo SDC invaluable for transferring the resulting program to my Color Computer 2.

Unlike basicize, I wrote this specifically for the Color Computer. There’s nothing in it that couldn’t be easily modified for other BASICs, however. It does not enforce any particular kind of BASIC in the lines that are not SuperBASIC directives. The SuperBASIC directives themselves create relatively standard BASIC statements for the time, at least among the Microsoft variants. The only things that are CoCo-specific are the aforementioned $seconds for delay loops, and the screen width of 32 in $screenWidth. They should each be able to be changed simply by altering the value of the variable. The screen width is only used by SuperBASIC’s hash-to-REM, DATA, and WRAP directives, and the loops-per-second is only used by PAUSE.

I wrote SuperBASIC in Perl, and it should work on any computer that has Perl installed. The zip file includes SuperBASIC and three sample utilities: the clock program above, the program I first used SuperBASIC on, for flipping through the CoCo2’s video modes, and a program for displaying the ASCII value of keyboard keys and the joystick position and button status.

SuperBASIC (Zip file, 18.9 KB)

In response to TRS-80 Color Computer Programming Tools: The TRS-80 Color Computer was a fascinating implementation of the 6809 computer chip, and was, with from the Color Computer 1 through 3, possibly the longest-running of the old-school personal computers.

  1. The CoCo 2b was the last iteration of the CoCo 2 line, and it included a hidden option for lowercase characters.

  2. It will also have an END statement at the very end.

  3. I’ve found that the xroar emulator is slightly off from the CoCo I’m using. It is slightly slower. When pausing between individual seconds in the clock, I pause for 303 iterations for the medium-resolution clock and 297 for the high-resolution clock. Xroar is more accurate at 302 and 295, respectively. That’s pretty close!

  4. In these times of plenty, writing code for an early computer on a modern computer should mean that for all practical purposes loops can be nested infinitely without Perl crashing: it will fill up the RAM on a CoCo 2 before it causes any problem whatsoever in Perl on a modern computer.

  5. Interestingly, having multiple statements per line appears to make the code run slower. I had to drop the per-second delay for the high-resolution clock from 298 to 296 after implementing one-line blocks for the CASE statement, and the medium-resolution clock from 304 to 303.

  6. And in fact, I created this table by piping superbasic --help to makeHTMLTable from 42 Astounding Scripts.

  1. <- Rainbow BASIC preflight
  2. superBASIC manual ->