Category Archives: Color BASIC

OpenAI writes a BASIC program!

Note: A slightly updated version of this article appeared earlier on Vintage is the New Old.

I asked:

can you write a program in Microsoft BASIC that encodes a message and can decode it later?

– me, to chat.openai.com

It initially showed me this:

REM This is a simple program that uses a substitution cipher to encode and decode a message in Microsoft BASIC

REM Define the alphabet and the substitution key
DIM alphabet(26)
DIM key(26)
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
key = "QWERTYUIOPASDFGHJKLZXCVBNM"

REM Define the message to encode
DIM message(40)
message = "HELLO, WORLD!"

REM Define the encoded message
DIM encoded(40)

REM Define the decoded message
DIM decoded(40)

REM Encode the message
FOR i = 1 TO LEN(message)
  encoded(i) = key(INSTR(alphabet, UCASE$(message(i))))
NEXT

REM Print the encoded message
FOR i = 1 TO LEN(encoded)
  PRINT encoded(i);
NEXT

REM Decode the message
FOR i = 1 TO LEN(encoded)
  decoded(i) = alphabet(INSTR(key, UCASE$(encoded(i))))
NEXT

REM Print the decoded message
FOR i = 1 TO LEN(decoded)
  PRINT decoded(i);
NEXT

Well, that might be some type of BASIC, but it wasn’t what I meant. So I added:

convert that to older interpreted BASIC, which requires line numbers. Make sure no variables are longer than two characters. All variables should be in uppercase.

– me, to chat.openai.com

It took a few back-and-forths as it generated code that wasn’t quite compatible with my CoCo. It initially was using strings similar to how C works, declaring a string array for the number of letters in the string. It would then try to access them as A$(1), A$(2), etc. That seemed silly, and I told it so.

It removed the DIMs and just started using strings normally.

When it was trying to build a string that way, I’d tell it “my BASIC can’t do that. use MID$ instead” and it would switch out.

A few rounds of this, and is had created a program that almost worked. There were some errors, caused by the INSTR command returning 0. I told it “if the string is not found, INSTR returns 0 which cannot be used in that MID$”.

It corrected that.

It worked, but I didn’t like the output, so I told it:

print the original message at the start. you can remove the ; on teh PRINT statements so they print a carraige return

– me to chat.open.ai

And at least, this AI-generated masterpiece was complete. And I didn’t write a line of it — I just told it when something wouldn’t work on my BASIC…

10 REM Simple substitution cipher
20 A$="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
30 K$="QWERTYUIOPASDFGHJKLZXCVBNM"
40 M$="HELLO, WORLD!"
50 PRINT "Original message: "; M$
60 E$=""
70 FOR I=1 TO LEN(M$)
80 C=INSTR(A$,CHR$(ASC(MID$(M$,I,1))))
90 IF C>0 THEN E$=E$+MID$(K$,C,1) ELSE E$=E$+MID$(M$,I,1)
100 NEXT
110 PRINT "Encoded message: "; E$
120 D$=""
130 FOR I=1 TO LEN(E$)
140 C=INSTR(K$,CHR$(ASC(MID$(E$,I,1))))
150 IF C>0 THEN D$=D$+MID$(A$,C,1) ELSE D$=D$+MID$(E$,I,1)
160 NEXT
170 PRINT "Decoded message: "; D$

And, I could have kept giving it more suggestions on what to do. Make them in to subroutines? Sure! Change the encoding method? No problem.

BASIC written by the OpenAI!

We live in wondrous times, for sure!

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…

Tackling the Logiker 2022 Vintage Computing Christmas Challenge – part 1

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

Here we go again! Over in the Facebook Color Computer group, David M. shared a link to this year’s Vintage Computing Christmas Challenge from Logiker. Although I did not submit an entry, I did play with last year’s challenge on my TRS-80 Color Computer.

Last year, it was this:

This year, the challenge is a bit more challenging. Per the challenge website, here is sample code for Commodore:

 10 print"{clear}"
 20 print""
 30 print""
 40 print""
 50 print"               *       *"
 60 print"               **     **"
 70 print"               ***   ***"
 80 print"               **** ****"
 90 print"           *****************"
100 print"            ***************"
110 print"             *************"
120 print"              ***********"
130 print"               *********"
140 print"              ***********"
150 print"             *************"
160 print"            ***************"
170 print"           *****************"
180 print"               **** ****"
190 print"               ***   ***"
200 print"               **     **"
210 print"               *       *"
220 goto 220

Starting with that un-optimized version, I will change it to work on the CoCo 1/2/3’s 32-column screen by adjusting it to be properly centered on that display.

 10 CLS
 50 PRINT"           *       *"
 60 PRINT"           **     **"
 70 PRINT"           ***   ***"
 80 PRINT"           **** ****"
 90 PRINT"       *****************"
100 PRINT"        ***************"
110 PRINT"         *************"
120 PRINT"          ***********"
130 PRINT"           *********"
140 PRINT"          ***********"
150 PRINT"         *************"
160 PRINT"        ***************"
170 PRINT"       *****************"
180 PRINT"           **** ****"
190 PRINT"           ***   ***"
200 PRINT"           **     **"
210 PRINT"           *       *"
220 GOTO 220

Unfortunately, this design is 17 rows tall, and the CoCo’s standard display is only 16. It won’t fit:

We should still be able to enter the challenge by having the program print this pattern, even if it scrolls off the screen a bit. To get one extra line there, we can get rid of the line feed at the end of the final PRINT statement in line 210 by adding a semi-colon to the end:

210 PRINT"           *       *";

And so it begins…

And so it begins

The goal is to make this as small as possible. There were many ways to approach last year’s Christmas tree challenge, and you can read about the results and a follow-up with suggestions folks gave to save a byte or two.

A simple thing is to remove the spaces at the front and replace them with the TAB() command:

 10 CLS
 50 PRINTTAB(7)"    *       *"
 60 PRINTTAB(7)"    **     **"
 70 PRINTTAB(7)"    ***   ***"
 80 PRINTTAB(7)"    **** ****"
 90 PRINTTAB(7)"*****************"
100 PRINTTAB(7)" ***************"
110 PRINTTAB(7)"  *************"
120 PRINTTAB(7)"   ***********"
130 PRINTTAB(7)"    *********"
140 PRINTTAB(7)"   ***********"
150 PRINTTAB(7)"  *************"
160 PRINTTAB(7)" ***************"
170 PRINTTAB(7)"*****************"
180 PRINTTAB(7)"    **** ****"
190 PRINTTAB(7)"    ***   ***"
200 PRINTTAB(7)"    **     **"
210 PRINTTAB(7)"    *       *";
220 GOTO 220

Although this only looks like it saves a character per line (“TAB(8)” versus “seven spaces”), the code itself will be smaller since the TAB command tokenizes down to one (or maybe two?) bytes.

Also, the ending quote is not needed if it’s the last thing on a line, so they could be removed:

 50 PRINTTAB(7)"    *       *
 60 PRINTTAB(7)"    **     **
 70 PRINTTAB(7)"    ***   ***

That would save one byte per line.

But, each line number consumes 5-bytes on it’s own, so a better way to save space would be to pack the lines together. Each line you eliminate saves five bytes. That would become pretty unreadable though, but let’s do it anyway:

10 CLS:PRINTTAB(7)"    *       *":PRINTTAB(7)"    **     **":PRINTTAB(7)"    ***   ***":PRINTTAB(7)"    **** ****":PRINTTAB(7)"*****************":PRINTTAB(7)" ***************":PRINTTAB(7)"  *************":PRINTTAB(7)"   ***********"
130 PRINTTAB(7)"    *********":PRINTTAB(7)"   ***********":PRINTTAB(7)"  *************":PRINTTAB(7)" ***************":PRINTTAB(7)"*****************":PRINTTAB(7)"    **** ****":PRINTTAB(7)"    ***   ***":PRINTTAB(7)"    **     **"
210 PRINTTAB(7)"    *       *";
220 GOTO 220

That’s quite the unreadable mess!

This could still be made better, since the text lines were kept under the input buffer limitation size, but when you enter that line, BASIC compresses it (tokenizes keywords like PRINT, TAB and GOTO) making it take less space. You can then sometimes EDIT the line, Xtend to the end and type a few more characters.

That may or may not be allowed for the Logiker challenge. And since I want to provide code here you could copy and then load in to an emulator, I’ll keep it to the limit of what you could type in.

In the next installment, I’ll see if my brane can figure out a way to generate this code using program logic rather than brute-force PRINT statements.

Until then…

Carl England must be stopped.

Earlier this month, I received a message on my Sub-Etha Software Facebook page from author (and former/current CoCo programmer) Carl England:

I knew CHR$(13) was a carriage return, so this was creating a file name that contained carraige returns. What would that do?

I typed it in to find out…

How appropriate, because the first thing I said when i saw this code was “weird.”

Of course, now I have a file on my disk that would be hard to delete. Carl said just doing:

KILL A$

…would take care of it. Provided, of course, you did that after running the program, while A$ still existed. Otherwise you’d have to manually type that line again and replace the SAVE with KILL.

Fun.

Character 8 is a backspace, so I could sorta see what this was doing. “TEST” then four backspaces, then a period, then three more backspaces?

Well.

Also weird.

I could have had so much fun with this trick back when I was swapping disks with friends.

Thanks, Carl!

Pop-Up windows in Color BASIC?

In 1987, I wrote a simple routine to display a pop-up window on the 32-column text screen of the CoCo. I am not sure what I wrote this for, but it appears I did use this routine (or a version of it) in my TIDY DISK program a year later:

Tidy Disk – help screen 1

It is sometimes interesting to look back on something I did 35 years ago and see how (or if) I would do it differently today. So let’s do that.

On the Color Computer Archive site is a disk image containing two versions of this routine. The first was done on 8/8/1987, and it looks like a 1.1 was created the next day. I am pleased to see I was retaining versions back then.

Using the wonderful toolshed utility decb, I was able to get a text version of the tokenized BASic program easily:

$decb dir WNDO.DSK
Directory of: WNDO.DSK,

WNDO     BAS  0  B  1
WNDO11   BAS  0  B  1

$decb list -t WNDO.DSK,WNDO.BAS
999 END
1000 REM WINDOW SUBROUTINE
1001 REM S-START W-IDTH
1002 REM A$-TEXT L-ENGTH
1005 PRINT@S,STRING$(W+1,140)CHR$(141);:FORA=1TOL:PRINT@A*32+S,CHR$(133)STRING$(W,32)CHR$(128);:NEXTA:PRINT@32*L+S,CHR$(131)STRING$(W+1,128);:B=1
1010 FORA=W TOW-8STEP-1:IFMID$(A$,A,1)=" "THENPRINT@S+1+32*B,LEFT$(A$,A);:A$=RIGHT$(A$,LEN(A$)-A)ELSENEXTA:PRINT@S+1+32*B,A$;:GOTO1020
1015 B=B+1:GOTO1010
1020 PRINT@S+32*(L-1)+(W/2-5),"press any key";
1025 IFINKEY$=""THEN1025ELSERETURN

$ decb list -t WNDO.DSK,WNDO11.BAS
999 END
1000 REM WINDOW SUBROUTINE 1.1
1001 REM  BY ALLEN C. HUFFMAN
1002 REM      8/8, 8/9/87
1003 C=4
1005 PRINT@S,CHR$(140)STRING$(W,140)CHR$(141);:FORA=1TOL:PRINT@A*32+S,CHR$(133)STRING$(W,32)CHR$(133+16*C);:NEXTA:PRINT@32*L+S,CHR$(131)STRING$(W,131+16*C)CHR$(135+16*C);:B=1
1010 FORA=W TOW-8STEP-1:IFMID$(A$,A,1)=" "THENPRINT@S+1+32*B,LEFT$(A$,A);:A$=RIGHT$(A$,LEN(A$)-A)ELSENEXTA:PRINT@S+1+32*B,A$;:GOTO1020
1015 B=B+1:GOTO1010
1020 PRINT@S+32*(L-1)+W/2-6,"press"CHR$(128)"any"CHR$(128)"key";
1025 IFINKEY$=""THEN1025ELSERETURN

It appears the first version had help in the comments, and the second replaced that with my name and version dates. Looking at a WinMerge diff of the two versions, I see this:

It looks like I basically added color support rather than just a black drop shadow, and must have fixed a bug where width needed 1 added to it. The only other change was printing “PRESS ANY KEY” with black blocks between each word in stead of spaces. I can therefore tell I used at least this 1.1 version in my TIDY DISK program.

The subroutine requires the user to set a few variables before calling it:

  • S – PRINT @ position for the top left of the pop-up.
  • W – width of the pop-up (in characters).
  • L – length (height) of the pop-up (in lines).
  • A$ – message to display inside the pop-up. The message will be word-wrapped by the routine.

It appears I also allowed the user to set the color. In line 1003, the routine sets it to 3 (white), but the user could also set C ahead of time and then GOSUB to 1005 to bypass that line and use their own color. So I’ll add:

  • C – color of the drop shadow (use GOSUB 1005) if setting C.

GOSUB 1000 uses a default color, or set it yourself and GOSUB 1005. Nice.

10 C=6:S=39:W=16:L=10
20 A$="HELLO, 1987. THIS IS A COCO WINDOW POP-UP."
30 GOSUB 1005

The routine itself is not very smart. It doesn’t calculate anything. The user has to specify top left, width and height and do all that work. If you want a nicely centered box, you have to figure that out. But, if you wanted to make windows at different positions, I guess it allowed that flexibility. (I expect that’s why I wrote it this way.)

We can do better.

How many lines is it, anyway?

For most uses, a centered pop-up seems good enough. The issue is knowing how many lines the message is going to take to display after being word-wrapped. The current routine scans the string starting at “width” and moving backwards until it finds a space character. It then uses LEFT$ to print just that portion, and then trims that part off using RIGHT$.

To know how many lines it will take, we have to scan the string first to count them. Then we do it again to print them. This will slow things down, but make things easier on the user.

Anatomy of a Word Wrap

There are some good old articles here about word wrap routines, with some very fast and clever ones contributed by others (way better than mine). Check out those articles for better methods that what I am about to present.

Because I need to scan the line twice — one time to count the lines, and the second for the actual word wrap — I cannot mess with the user’s string. Instead, I’ll just use variables that sell me the Start Position (SP) within the string and End Position (EP) of the width-sized chunk I am going to scan. I’ll use a Position (P) variable and start at the end and scan backwards looking for a space. If I find one, I can count that as a line (increase the line count) and then move to the next character (to skip the space) and reset the Start Position to that. The process will repeat.

Since I do not want to duplicate code, I’ll embed the “print this section of the string” code in this routine, but use another variable to determine if I actually print it or now. I’m calling it Show (SH), and if SH=0 then it just counts, otherwise it prints as it counts each line.

It’s messy, but it works.

Here is a diagram showing the process:

Anatomy of a Word Wrap routine…

When the space is located, it knows that from SP to that space position is something to print (a line).

Easy peasy.

Except it took me way too much brane power to figure this out, and I am sure it could be massively improved.

The routine will use the user’s width (W) variable to know how wide to make the box, and adjust the text space inside accordingly (W-2, leaving room for the borders).

The user can specify C for color, or if it’s not set (0) it will be assigned a color.

The starting location (S) must also be set. This is the top left of the pop-up box, in PRINT@ format (i.e. 0 is the top left of the screen, 32 is the start of the second line, etc.)

Here is what I came up with, along with a few lines at the start to test it out. It will keep displaying random sized pop-ups with the same text over and over and over. I used this to make sure it would work with the various size and color valuies.

NOTE: If you send in a width that is too narrow, it won’t work properly. Ideally I should check for that and have a minimum size. (I’ll try to add that in the final version, since I know it needs to be at least wide enough to fit “PRESS ANY KEY”.)

10 A$="THIS IS A LONG STRING THAT I WANT TO USE TO TEST WORD WRAPPING IN MY WINDOW ROUTINE."
20 W=RND(10)+20:S=48-W/2:C=RND(8)-1
30 CLS:GOSUB 1000
40 GOTO 20

999 END
1000 REM WINDOW SUBROUTINE 2.0
1001 REM  BY ALLEN C. HUFFMAN
1002 REM 8/8, 8/9/87 & 7/27/22
1993 REM    S-START W-IDTH
1004 REM    A$-TEXT C-COLOR
1005 REM
1010 IF C=0 THEN C=4
1020 SH=0:GOSUB 1120
1030 PRINT@S,CHR$(140)STRING$(W-2,140)CHR$(141);
1040 FOR A=1 TO LC+1
1050 PRINT@S+32*A,CHR$(133)STRING$(W-2,32)CHR$(133+16*C);
1060 NEXTA
1070 PRINT@S+32*(LC+1),CHR$(131)STRING$(W-2,131+16*C)CHR$(135+16*C);
1080 SH=1:GOSUB 1120
1090 PRINT@S+32*LC+W/2-7,"press"CHR$(128)"any"CHR$(128)"key";
1100 IF INKEY$="" THEN 1100 ELSE RETURN
1110 ' CALC LINE COUNT/PRINT
1120 LC=1:SP=1:LN=LEN(A$)
1130 EP=SP+W-2:P=EP
1140 IF EP>LN THEN 1190
1150 IF MID$(A$,P,1)<>" " THEN P=P-1 ELSE 1170
1160 IF P>SP THEN 1150 ELSE 1190
1170 IF SH<>0 THEN PRINT@S+32*LC+1,MID$(A$,SP,P-SP);
1180 LC=LC+1:SP=P+1:GOTO 1130
1190 IF SH<>0 THEN PRINT@S+32*LC+1,MID$(A$,SP);
1200 LC=LC+1:RETURN
1210 RETURN

One bad thing about this is all the variables I am using. And it could also be made faster by combining lines and such.

Here are some things I would like to do next:

  • Check for a minimum width (if W<x then W=x).
  • Combine lines to make it smaller and faster.
  • Rename variables to all start with Z.

Z? Since my subroutine uses many variables, the calling program would have to make sure to not use any of them since the subroutine would change them. Today I like the approach of starting all my subroutine variables with Z, and just noting that “Zx variables are reserved for subroutines.” That way I can use any variables I want in my main program — other than the ones that are specifically used to pass values in to the subroutine — without worrying about a conflict.

I guess that means there will be a part 2. Until then, please comment with any ideas you have on improving this.

Until next time…

Pac-Man maze in 64x32 semigraphics.

Drawing a maze level on the CoCo – part 5

See Also: part 1, part 2, part 3, part 4 and part 5.

ASCII HEX data to maze graphics

As our spiraling trip down the rabbit hole continues, we finally return to drawing stuff instead of converting stuff.

We now have DATA statements that contain 2-byte hex numbers. Each hex number represents a byte (0-255) and each of those bytes represents 8 maze blocks on the screen.

For use in a Color BASIC program, we might want to take this HEX data and convert it in to string data (containing the final CHR$ values) for fast printing on the screen. If we were doing this in 6809 assembly, we might have the binary data contained in the executable, and convert it to a buffer that can then be copied to the screen as needed.

We’ll start with a simple (verbose) Color BASIC version. Since I am using the XRoar emulator to CLOAD this text file, I had to shorten the DATA lines a bit since they are now on higher lines, thus instead of loading “1DATA” it is loading “360DATA” taking up more bytes. I just moved some hex values to the next line.

I also adjusted my parser a bit. In my binary example, I wanted to handle binary numbers of any length. i.e, “111” would be 7, and “11111111” would be 255. To do this, I started with 0, and each time I saw a 1 I added one. For each new digit, I shifted the bits left by multiplying by 2. That way if it parsed “111” it would be “1 *2 +1 *2 +1” and so on for longer values.

For the maze, everything has to be a byte, so I took the opposite approach by starting with a bit value of 128 (representing the high bit in an 8-bit byte). Each time I found a new digit, I divided the bit value by 2 and either added it (if the new digit was a “1”, or “X” in the case of the maze) or not. That would make a maze of “XXXXXXXX” be “128 + 16 + 32 + 16 + 8 + 4 + 1” giving 255. If the maze data ran short, so the last few bytes were just “XXXX” it would be “128 + 64 + 32 + 16” which is &HF0 in hex (11110000), exactly what we want. If I had done it the other way, it would end up with 1111 (&H0F) and maze data would look like this:

GOOD:
-------- -------- ----
XXXXXXXX XXXXXXXX XXXX &HFF &HFF &HF0 -> 11111111 11111111 11110000

BAD:
-------- -------- ----
XXXXXXXX XXXXXXXX XXXX &HFF &HFF &H0F -> 11111111 11111111 00001111

I hope that makes sense. Anyway, here is the program I came up with… It will scan the hex values and build an array of strings (LN$) for each line. That way, the converted maze can be quickly printed after it has been generated once.

0 REM HEXMAZE.BAS
10 ' STRING SPACE IS MW*(MH+1)
20 CLEAR 28*32
30 ' SIZE OF MAZE IN DATA
40 MW=28:MH=31
50 ' ROOM FOR CONVERTED MAZE
60 DIM MZ$(MH):LN=0
70 CLS
80 ' READ A LINE OF HEX DATA
90 READ A$:IF A$="" THEN 340
100 ' EVERY 2 CHARS IS A HEX
110 FOR A=1 TO LEN(A$) STEP 2
120 ' COVERT HEX TO A VALUE
130 V=VAL("&H"+MID$(A$,A,2))
140 ' CONVERT BITS TO BLOCKS
150 BT=128
160 IF (V AND BT)=0 THEN B$=CHR$(128) ELSE B$=CHR$(175)
170 ' ADD BLOCK/SPACE TO STRING
180 MZ$(LN)=MZ$(LN)+B$
190 ' IF LEN >= MAZE W, NEXT
200 IF LEN(MZ$(LN))>=MW THEN 280
210 ' SHIFT BIT RIGHT
220 BT=INT(BT/2)
230 ' IF MORE BITS, REPEAT
240 IF BT>0 THEN 160
250 ' GET NEXT CHARACTER
260 GOTO 310
270 ' ELSE NEW MAZE LINE
280 PRINT MZ$(LN)
290 LN=LN+1
300 ' NEXT CHARACTER
310 NEXT
320 ' MORE LINES TO DO?
330 IF LN<MH THEN 90
340 END
350 REM MAZE AS HEX DATA
360 DATAFFFFFFF080060010BDF6FBD0BDF6FBD0BDF6FBD080000010BDBFDBD0BDBFDBD081861810FDF6FBF005F6FA0005801A0005BFDA00FDA05BF000204000FDA05BF005BFDA0005801A0005BFDA00FDBFDBF080060010BDF6FBD0BDF6FBD08C000310EDBFDB70EDBFDB7081861810BFF6FFD0BFF6FFD080000010
370 DATAFFFFFFF0,""

This program works, but it’s very slow, taking about 45 seconds to run. But, it’s verbose enough to (hopefully) explain what is going on.

By removing spaces, swapping out a integer values for faster HEX values, changing a CHR$() to a variable and adding a bit more string space, removing INT (and making one adjustment needed due to that), and combining lines, it can be sped up to just over 32 seconds.

0 REM HEXMAZE2.BAS
10 ' STRING SPACE IS MW*(MH+1)
20 CLEAR 28*32+200
30 ' SIZE OF MAZE IN DATA
40 MW=28:MH=31
50 ' ROOM FOR CONVERTED MAZE
60 DIM MZ$(MH):LN=0:BL$=CHR$(175)
70 CLS
90 READA$:IFA$=""THEN340
110 FORA=1TOLEN(A$)STEP2:V=VAL("&H"+MID$(A$,A,&H2)):BT=&H80
160 IF(V ANDBT)=0THENB$=BK$ELSEB$=BL$
180 MZ$(LN)=MZ$(LN)+B$:IFLEN(MZ$(LN))>=MW THEN280
220 BT=BT/2:IFBT>=1THEN160
260 GOTO310
280 PRINTMZ$(LN):LN=LN+1
310 NEXT:IFLN<MH THEN90
340 END
350 REM MAZE AS HEX DATA
360 DATAFFFFFFF080060010BDF6FBD0BDF6FBD0BDF6FBD080000010BDBFDBD0BDBFDBD081861810FDF6FBF005F6FA0005801A0005BFDA00FDA05BF000204000FDA05BF005BFDA0005801A0005BFDA00FDBFDBF080060010BDF6FBD0BDF6FBD08C000310EDBFDB70EDBFDB7081861810BFF6FFD0BFF6FFD080000010
370 DATAFFFFFFF0,""

And this is before translating that blocky maze into the “rounded corners” and such we want. This may be far too slow to do in BASIC since most folks don’t want to “Please wait 3.5 minutes while loading…” like a program my Commodore 64 friend once showed me. Assembly language helper routines might be needed for the impatient players out there…

Blocky to less blocky

After this code converts HEX values to maze data in a string array, we can process those strings and change the characters to have the “rounded” corners and such. Here is what I came up with, based on the original version that PEEKed and POKEed displayed bytes on the screen. This time, it’s scanning strings using MID$() and altering CHR$() values.

Side Note: Does everyone here know that, while you can’t use LEFT$ or RIGHT$ to change the left or right portion of a string, you can use MID$ to change characters? If you have A$=”1234567890″ and you want to change two characters starting at the third one, you can do MID$(A$,3,2)=”XX” and you get “12XX567890”. I’ll be using that…

Here is what I came up with… Instead of PEEKing memory, I used MID$(). It’s quite the mess, and very slow since I went back to the first version of the hex-to-maze print routine, just for clarity.

0 REM HEXMAZE3.BAS
10 ' STRING SPACE IS MW*(MH+1)
20 CLEAR 28*32
30 ' SIZE OF MAZE IN DATA
40 MW=28:MH=31
50 ' ROOM FOR CONVERTED MAZE
60 DIM MZ$(MH):LN=0
70 CLS
80 ' READ A LINE OF HEX DATA
90 READ A$:IF A$="" THEN 340
100 ' EVERY 2 CHARS IS A HEX
110 FOR A=1 TO LEN(A$) STEP 2
120 ' COVERT HEX TO A VALUE
130 V=VAL("&H"+MID$(A$,A,2))
140 ' CONVERT BITS TO BLOCKS
150 BT=128
160 IF (V AND BT)=0 THEN B$=CHR$(128) ELSE B$=CHR$(175)
170 ' ADD BLOCK/SPACE TO STRING
180 MZ$(LN)=MZ$(LN)+B$
190 ' IF LEN >= MAZE W, NEXT
200 IF LEN(MZ$(LN))>=MW THEN 280
210 ' SHIFT BIT RIGHT
220 BT=INT(BT/2)
230 ' IF MORE BITS, REPEAT
240 IF BT>0 THEN 160
250 ' GET NEXT CHARACTER
260 GOTO 310
270 ' ELSE NEW MAZE LINE
280 PRINT MZ$(LN)
290 LN=LN+1
300 ' NEXT CHARACTER
310 NEXT
320 ' MORE LINES TO DO?
330 IF LN<MH THEN 90

340 ' CONVERT LINES
350 BL$=CHR$(128):PRINT
360 FOR LN=0 TO MH-1
370 FOR C=1 TO LEN(MZ$(LN))
380 IF MID$(MZ$(LN),C,1)=BL$ THEN 510
390 V$=CHR$(175)
400 IF LN>0 THEN U$=MID$(MZ$(LN-1),C,1):UR$=MID$(MZ$(LN-1),C+1,1) ELSE U$=BL$:UR$=BL$
410 IF LN>0 THEN IF C>1 THEN UL$=MID$(MZ$(LN-1),C-1,1) ELSE ELSE UL$=BL$
420 IF C>1 THEN L$=MID$(MZ$(LN),C-1,1) ELSE L$=BL$
430 IF C<LEN(MZ$(LN)) THEN R$=MID$(MZ$(LN),C+1,1) ELSE R$=BL$
440 IF LN<MH-1 THEN D$=MID$(MZ$(LN+1),C,1) ELSE D$=BL$
450 IF LN<MH-1 THEN IF C>1 THEN DL$=MID$(MZ$(LN+1),C-1,1) ELSE ELSE DL$=BL$
460 IF LN<MH-1 THEN IF C<MW THEN DR$=MID$(MZ$(LN+1),C+1,1) ELSE ELSE DR$=BL$
470 IF U$<>BL$ THEN IF L$<>BL$ THEN GOSUB 600
480 IF U$<>BL$ THEN IF R$<>BL$ THEN GOSUB 700
490 IF D$<>BL$ THEN IF L$<>BL$ THEN GOSUB 800
500 IF D$<>BL$ THEN IF R$<>BL$ THEN GOSUB 900
510 NEXT:PRINT MZ$(LN):NEXT
520 END

600 ' UP and LEFT set
610 IF UL$<>BL$ THEN V$=CHR$(ASC(V$) AND NOT 8)
620 IF R$=BL$ THEN IF DR$=BL$ THEN IF D$=BL$ THEN V$=CHR$(ASC(V$) AND NOT 1)
630 MID$(MZ$(LN),C,1)=V$:RETURN

700 ' UP and RIGHT set
710 IF UR$<>BL$ THEN V$=CHR$(ASC(V$) AND NOT 4)
720 IF L$=BL$ THEN IF DL$=BL$ THEN IF D$=BL$ THEN V$=CHR$(ASC(V$) AND NOT 2)
730 MID$(MZ$(LN),C,1)=V$:RETURN

800 ' DOWN and LEFT set
810 IF DL$<>BL$ THEN V$=CHR$(ASC(V$) AND NOT 2)
820 IF U$=BL$ THEN IF UR$=BL$ THEN IF R$=BL$ THEN V$=CHR$(ASC(V$) AND NOT 4)
830 MID$(MZ$(LN),C,1)=V$:RETURN

900 ' DOWN and RIGHT set
910 IF DR$<>BL$ THEN V$=CHR$(ASC(V$) AND NOT 1)
920 IF L$=BL$ THEN IF UL$=BL$ THEN IF U$=BL$ THEN V$=CHR$(ASC(V$) AND NOT 8)
930 MID$(MZ$(LN),C,1)=V$:RETURN

1000 REM MAZE AS HEX DATA
1010 DATAFFFFFFF080060010BDF6FBD0BDF6FBD0BDF6FBD080000010BDBFDBD0BDBFDBD081861810FDF6FBF005F6FA0005801A0005BFDA00FDA05BF000204000FDA05BF005BFDA0005801A0005BFDA00FDBFDBF080060010BDF6FBD0BDF6FBD08C000310EDBFDB70EDBFDB7081861810BFF6FFD0BFF6FFD080000010
1020 DATAFFFFFFF0,""

Testing on the XRoar emulator shows this whole process takes about 161 seconds.

That’s so slow. Perhaps removing some of the CHR$() stuff and changing strings to values (like the originally PEEK version), skipping some of the extra IFs (like, if you are on line 0, there is no up, up left, or up right; if you are on the last line, there is no bottom, bottom left, or bottom right) and some other minor things might help…

340 ' CONVERT LINES
350 BL=128:PRINT
360 FOR LN=0 TO MH-1
365 LL=LEN(MZ$(LN))
370 FOR C=1 TO LL
380 IF ASC(MID$(MZ$(LN),C,1))=BL THEN 510
390 V=175

395 IF LN=0 THEN U=BL:UR=BL:UL=BL:GOTO 420
400 U=ASC(MID$(MZ$(LN-1),C,1)) ELSE U=BL
405 IF C<LL THEN UR=ASC(MID$(MZ$(LN-1),C+1,1)) ELSE UR=BL
410 IF C>1 THEN UL=ASC(MID$(MZ$(LN-1),C-1,1)) ELSE UL=BL

420 IF C>1 THEN L=ASC(MID$(MZ$(LN),C-1,1)) ELSE L=BL
430 IF C<LL THEN R=ASC(MID$(MZ$(LN),C+1,1)) ELSE R=BL

435 IF LN=MH-1 THEN D=BL:DL=BL:DR=BL:GOTO 470
440 D=ASC(MID$(MZ$(LN+1),C,1)) ELSE D=BL
450 IF C>1 THEN DL=ASC(MID$(MZ$(LN+1),C-1,1)) ELSE DL=BL
460 IF C<LL THEN DR=ASC(MID$(MZ$(LN+1),C+1,1)) ELSE DR=BL

470 IF U<>BL THEN IF L<>BL THEN GOSUB 600
480 IF U<>BL THEN IF R<>BL THEN GOSUB 700
490 IF D<>BL THEN IF L<>BL THEN GOSUB 800
500 IF D<>BL THEN IF R<>BL THEN GOSUB 900
510 NEXT:PRINT MZ$(LN):NEXT
515 PRINT TIMER,TIMER/60
520 END

600 ' UP and LEFT set
610 IF UL<>BL THEN V=(V AND NOT 8)
620 IF R=BL THEN IF DR=BL THEN IF D=BL THEN V=(V AND NOT 1)
630 MID$(MZ$(LN),C,1)=CHR$(V):RETURN

700 ' UP and RIGHT set
710 IF UR<>BL THEN V=(V AND NOT 4)
720 IF L=BL THEN IF DL=BL THEN IF D=BL THEN V=(V AND NOT 2)
730 MID$(MZ$(LN),C,1)=CHR$(V):RETURN

800 ' DOWN and LEFT set
810 IF DL<>BL THEN V=(V AND NOT 2)
820 IF U=BL THEN IF UR=BL THEN IF R=BL THEN V=(V AND NOT 4)
830 MID$(MZ$(LN),C,1)=CHR$(V):RETURN

900 ' DOWN and RIGHT set
910 IF DR<>BL THEN V=(V AND NOT 1)
920 IF L=BL THEN IF UL=BL THEN IF U=BL THEN V=(V AND NOT 8)
930 MID$(MZ$(LN),C,1)=CHR$(V):RETURN

Well, that brings it down to 155 seconds and that simply won’t do. There are many other optimizations to this BASIC program that could be done, and I bet we could even double the speed. But even then, that’s far too long to wait for a maze level to be drawn.

It looks like an assembly language routine might be our only option. We’ll take a break for now, but revisit this later after some discussion on assembly language. At some point. Eventually.

Until next time…

Pac-Man maze in 64x32 semigraphics.

Drawing a maze level on the CoCo – part 4

See Also: part 1, part 2, part 3, part 4 and part 5.

ASCII character data to 8-bit ASCII HEX data.

Let’s jump right in and write the program that writes the DATA statements for this maze.

It should generate a string that will start with a line number, the keyword “DATA”, and then add 2-digit hex values to it until it reaches that magic 250 character limit. When that happens, we write it out to cassette or disk, and start a new line. (Of course, this could also be adjusted to make each line 31 characters or less, to fit nicely on the CoCo’s screen. That’s the magic of computer generated code — you can easily make it change the output rather than having to do it by hand.)

I also want to make it get as many bytes as possible on the line, so I’ll start with single digit line numbers, and increment by 1. “1DATA” in ASCII gives me four extra bytes than “1000 DATA” and, when they get tokenized (either by loading from tape/disk in ASCII, or typing them in) they will take up the same amount of room since line numbers are all stored as two byte values in the program code. The result can then be RENUMbered to start at whatever line is needed for the final program.

Here is my very slow, very large version:

10 CLEAR 500
20 ' MAX OUTPUT LINE LENGTH
30 INPUT "MAX LINE LENGTH";MX
40 IF MX=0 OR MX>250 THEN MX=250
50 ' GET/SET DEVICE NUMBER
60 DV=0
70 PRINT "OUTPUT TO D)ISK, P)RINTER"
80 INPUT "S)CREEN OR T)APE";A$
90 IF A$="D" THEN DV=1 ELSE IF A$="P" THEN DV=-2 ELSE IF A$="S" THEN DV=0 ELSE IF A$="T" THEN DV=-1 ELSE 70
100 ' CREATE OUTPUT FILE
110 OPEN "O",#DV,"MAZEDATA.BAS"
120 ' LINE NUMBER/RESET STRING
130 LN=1:LN$=""
140 ' READ LINE OF MAZE DATA
150 READ A$:IF A$="" THEN 470
160 PRINT A$
170 ' START AT FIRST CHARACTER
180 CH=1
190 ' INIT BIT VALUE
200 BT=128
210 ' RESET OUTPUT VALUE
220 V=0
230 ' IF X, ADD 1 (SET BIT)
240 IF MID$(A$,CH,1)="X" THEN V=V+BT
250 PRINT MID$(A$,CH,1);
260 ' SHIFT BIT LEFT
270 BT=INT(BT/2)
280 ' IF MORE BITS, DO NEXT CH
290 IF BT>0 THEN CH=CH+1:GOTO 240
300 ' CREATE 2-DIGIT HEX VALUE
310 HX$=HEX$(V):IF V<16 THEN HX$="0"+HX$
320 PRINT ,HX$;CH
330 ' START LINE IF NEEDED
340 ' TRIM FIRST CHAR
350 IF LN$="" THEN LN$=STR$(LN)+"DATA":LN$=MID$(LN$,2)
360 ' ADD HEX VALUE
370 LN$=LN$+HX$
380 'IF LINE FULL, WRITE IT OUT
390 IF LEN(LN$)>=MX-2 THEN PRINT #DV,LN$:LN$="":LN=LN+1
400 ' GO TO NEXT CH
410 CH=CH+1
420 ' IF PAST END, NEW LINE
430 IF CH>LEN(A$) THEN 150
440 ' OTHERWISE, NEXT BYTE
450 GOTO 200
460 ' WRITE ANY REMAINING DATA
470 PRINT #DV,LN$
480 CLOSE #DV
490 END
1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X            XX            X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X                          X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X      XX    XX    XX      X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1100 DATA "     X XXXXX XX XXXXX X     "
1110 DATA "     X XX          XX X     "
1120 DATA "     X XX XXXXXXXX XX X     "
1130 DATA "XXXXXX XX X      X XX XXXXXX"
1140 DATA "          X      X          "
1150 DATA "XXXXXX XX X      X XX XXXXXX"
1160 DATA "     X XX XXXXXXXX XX X     "
1170 DATA "     X XX          XX X     "
1180 DATA "     X XX XXXXXXXX XX X     "
1190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1200 DATA "X            XX            X"
1210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1230 DATA "X   XX                XX   X"
1240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1260 DATA "X      XX    XX    XX      X"
1270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1290 DATA "X                          X"
1300 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1310 DATA ""

Let’s walk through the code…

  • Line 10 – We need more than the default 200 bytes of string space.
  • Lines 20-40 – Just for fun, I decided to let the user choose how long the output lines will be. 32 would give DATA statements that fit on the screen, or maybe 80 for a printer. 250 is the largest allowed.
  • Lines 50-90 – Just to be fancy, the user can choose Disk, Printer, Screen or Tape output. When writing this, I learned you can still use OPEN “O”,#-2,”FILE” even on the printer or screen! The filename is ignored and no error is generated. Very cool.
  • Lines 100-110 – Create the output file.
  • Lines 120-130 – LN is the initial line number for the DATA statements, and LN$ will be the buffer for the line we are creating.
  • Lines 140-160 – Read a line of MAZE DATA and display it. (This program is slow, so I thought some output would be fun to look at.)
  • Lines 170-180 – CH will be the current character of the line we are checking to be an “X” or not.
  • Lines 190-200 – BT is the bit value. For 8-bits, we start with the high bit, which is a value of 128. Unlike my previous example, I think this might be a simpler approach for making the value as we scan 8 characters.
  • Lines 210-220 – V will be the output value (8 characters turned in to a number).
  • Lines 230-250 – If the current character is “X”, add the BT value to V. We then print the character that is being processed.
  • Lines 260-270 – To get to the next bit value, we divide by 2. 128 becomes 64, 64 becomes 32, etc.
  • Lines 280-290 – As long as BT>0, we have more bits to do so we can move to the next CH character of the line. GOTO 240 resumes checking the character.
  • Lines 300-320 – If out of bits, we make a HEX$ out of V. If it is a value less than 16 (0-15 would be 0-F in hex), we add a leading zero to make it always two characters. We then print that to the screen.
  • Lines 330-350 – If LN$ is empty, we need to start it with a line number and “DATA”. When using STR$(x) to turn a number into a string, a leading space will be added. This means 1 becomes ” 1″. If this was negative, that would be used for the minus sign — i.e., -1 would be “-1”. Since we know our number will be positive, we can just get rid of the first character. Using a trick I recently learned about, MID$() will give the right side of a string starting with a position if you leave out the third parameter (length). Thus, instead of RIGHT$(LN$,LEN(LN$)-1), I can do MID$(LN$,2) to give me everything from character 2 on. Wish I knew that in the 80s! (Hat tip to Robin at 8-Bit Show and Tell since I think that is where I learned this.)
  • Lines 360-370 – We add the HX$ to the end of our LN$ (which is either the one we just started, or one that is slowly growing as we keep adding values).
  • Lines 380-390 – If the LN$ reaches our max (minus 2, since we just added a 2 character hex value), we print it out to the selected device, then reset LN$ and increment our line number.
  • Lines 400-410 – Move to the next CH character position.
  • Lines 420-430 – If CH is past the end of the length of our A$ data, go to line 150 to read the next maze DATA.
  • Lines 440-450 – If there’s still more characters in the line, go back to 150 and get the next one.
  • Lines 460-480 – If done, we need to output whatever is left. The earlier output only did that when the line reached max size. This takes care of the final line that is less than that. We then close the file. (Or, if this is printer or screen, this doesn’t do anything.)

Done! Whew…

Running this using a max of 250 produces this in the file:

1DATAFFFFFFF080060010BDF6FBD0BDF6FBD0BDF6FBD080000010BDBFDBD0BDBFDBD081861810FDF6FBF005F6FA0005801A0005BFDA00FDA05BF000204000FDA05BF005BFDA0005801A0005BFDA00FDBFDBF080060010BDF6FBD0BDF6FBD08C000310EDBFDB70EDBFDB7081861810BFF6FFD0BFF6FFD080000010FFFF
2DATAFFF0

Our maze data converts to just two hex values more than what would fit on one line of BASIC. So close! To make it look prettier (but take up more program space) we could have done it with shorter lines:

1DATAFFFFFFF080060010BDF6FBD0BDF6FBD0BD
2DATAF6FBD080000010BDBFDBD0BDBFDBD08186
3DATA1810FDF6FBF005F6FA0005801A0005BFDA
4DATA00FDA05BF000204000FDA05BF005BFDA00
5DATA05801A0005BFDA00FDBFDBF080060010BD
6DATAF6FBD0BDF6FBD08C000310EDBFDB70EDBF
7DATADB7081861810BFF6FFD0BFF6FFD0800000
8DATA10FFFFFFF0

But whatever style you choose, you can then CLOAD/LOAD this ASCII BASIC program and RENUM it to 1000 or wherever you want it to go.

And now that we have our 8-bits maze DATA, we need to write code to read, parse, and display it.

Until next time…

Drawing a maze level on the CoCo – part 3

See Also: part 1, part 2, part 3, part 4 and part 5.

Converting data to other data

So far, we’ve talked about how to represent a simple maze level, and how to make the maze level have more detail than the data used to represent it. We still need to code a way to represent the maze as bits rather than ASCII characters. Let’s try to get that one figured out.

First, we’ll use the DATA statements shown in part 2 and make a program that scans them and outputs the hex bytes we’ll want for DATA statements.

1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X            XX            X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X                          X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X      XX    XX    XX      X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1100 DATA "     X XXXXX XX XXXXX X     "
1110 DATA "     X XX          XX X     "
1120 DATA "     X XX XXXXXXXX XX X     "
1130 DATA "XXXXXX XX X      X XX XXXXXX"
1140 DATA "          X      X          "
1150 DATA "XXXXXX XX X      X XX XXXXXX"
1160 DATA "     X XX XXXXXXXX XX X     "
1170 DATA "     X XX          XX X     "
1180 DATA "     X XX XXXXXXXX XX X     "
1190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1200 DATA "X            XX            X"
1210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1230 DATA "X   XX                XX   X"
1240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1260 DATA "X      XX    XX    XX      X"
1270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1290 DATA "X                          X"
1300 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1310 DATA ""

The idea would be to build bytes by scanning 8 characters and setting a 1 bit if there is an “X”, or a 0 bit if there is a ” ” space. In C I’d do this using bit shifting. BASIC doesn’t have that. Except it kinda does.

I have previously done some YouTube videos about bit manipulation in BASIC, but basically you just divide by 2 to shift right, or multiply by 2 to shift left. Here is a sample that will take an ASCII binary string and turn it in to a numeric variable:

0 REM BITPARSE.BAS
10 INPUT "BINARY STRING";A$
20 BT=0
30 ' SCAN LEFT TO RIGHT
40 FOR A=1 TO LEN(A$)
50 ' SHIFT LEFT
60 BT=BT*2
70 ' IF 1, ADD 1 BIT
80 IF MID$(A$,A,1)="1" THEN BT=BT+1
90 NEXT
100 PRINT A$;" =";BT
110 GOTO 10

This lets me type a binary string like “111” and see that it represents 7. “11111111” will show 255. You can even go beyond 8 bits, though I did not test to see what the upper limit of BASIC’s floating point variables are before they start showing bogus values.

BASIC Bit Parsing

If it’s easy to build a value out of ASCII ones and zeros, it should be just as easy to do it looking for ASCII Xs and spaces. The only new part will be stopping every 8 characters to output the number, then starting over with the next 8 characters. Here is the BITPARSE program updated to do just that. Changes are bolded.

0 REM MAZE2NUM.BAS
10 READ A$:IF A$="" THEN END
15 FOR B=1 TO LEN(A$) STEP 8
20 BT=0
30 ' SCAN LEFT TO RIGHT
40 FOR A=B TO B+7
50 ' SHIFT LEFT
60 BT=BT*2
70 ' IF 1, ADD 1 BIT
80 IF MID$(A$,A,1)="X" THEN BT=BT+1
90 NEXT
100 PRINT MID$(A$,B,8);TAB(8);" =";BT
105 NEXT
110 GOTO 10

1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X            XX            X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X                          X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X      XX    XX    XX      X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1100 DATA "     X XXXXX XX XXXXX X     "
1110 DATA "     X XX          XX X     "
1120 DATA "     X XX XXXXXXXX XX X     "
1130 DATA "XXXXXX XX X      X XX XXXXXX"
1140 DATA "          X      X          "
1150 DATA "XXXXXX XX X      X XX XXXXXX"
1160 DATA "     X XX XXXXXXXX XX X     "
1170 DATA "     X XX          XX X     "
1180 DATA "     X XX XXXXXXXX XX X     "
1190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1200 DATA "X            XX            X"
1210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1230 DATA "X   XX                XX   X"
1240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1260 DATA "X      XX    XX    XX      X"
1270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1290 DATA "X                          X"
1300 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1310 DATA ""

This was substantially easier to do than I expected.

Text to bits to numbers…

And I got lucky, thanks to something BASIC does I was not aware of. The strings being parsed at not multiples of 8 characters. Each line is 28 characters long. Since I parse each one 8 characters at a time, I should be getting:

  1. MID$(A$,1,8) – first 8 characters.
  2. MID$(A$,9,8) – second 8 characters.
  3. MID$(A$,17,8) – third 8 characters
  4. MID$(A$,25,8) – fourth 8 characters … but there are only 2 left in the line!

MID$() does something that saved me a ton of work. Say you have a ten character string, and you want to print 4 characters starting at position 2:

A$="1234567890"

PRINT MID$(A$,2,4)
2345

But what if you wanted to print four characters starting at position 9? There are only two characters left in the string:

A$="1234567890"

PRINT MID$(A$,9,4)
90

We asked for four… we got two … no error was given. What is BASIC returning? I think this is kind of a bug, because you can do this:

A$="1234567890"

PRINT MID$(A$,25,4)

Tada! Even though you asked for a character beyond the end of the string, it does not error. It just returns … nothing. Literally nothing — the empty string.

A$="1234567890"

IF MID$(A$,200,4)="" THEN PRINT "NOTHING"
NOTHING

And since our parser is looking for an “X” to set a bit, and anything else for 0, nothing counts as anything else and it just works.

Nice. (And I’m too embarrassed to show the overcomplex version I came up with the first time I tried to do this…)

Hexing

It is to make the program print out HEX values by using HEX$, but there is a problem… HEX$ only prints the characters it needs to. If you print HEX$(255) you will get FF. If you print HEX$(15) you will get F. We want our hex values to always be two digits, so we have to add some extra code to make sure any single digit values (0-15) have a 0 added to the start of them.

The simplest way is probably this — assuming we are just dealing with 2-digit hex values (0-255):

V=15
HX$=HEX$(V):IF LEN(HX$)<2 THEN HX$="0"+HX$

Or, since we know only values 0-15 will be one hex digit, this may be both smaller and faster:

V=15
HX$=HEX$(V):IF V<16 THEN HX$="0"+HX$

Or we could go all complicated and build a string like “00” and use MID$ to insert the new string, either one or two characters long.

V=15:V$=HEX$(V)
HX$="00":MID$(HX$,LEN(HX$)-LEN(V$)+1)=V$

That one is cool because you can make HX$=”00″ for two digits, or “0000” for four digits. While the first two are smaller (and probably faster), they are hard-coded for 2 digit hex values.

Which is all we are using here, so I’ll just go with one of those.

Make the computer do the work…

We could now just output all the hex values and put them in DATA statements. Perhaps we copy them down from the screen, or maybe we have the program create a tape or disk text file and write them to it. We could even make them write out as if they were an ASCII program that could be loaded back in to BASIC.

That sounds fun, so we’ll do that.

Here is a simple program that will create a text file on tape (or disk) and write out a few lines of BASIC. You can then load this file in using LOAD or CLOAD and it will load as if it were a program saved in ASCII format.

10 OPEN "O",#-1,"DATA.BAS"
20 PRINT #-1,"1000 DATA 1,2,3"
30 CLOSE #-1

You can change the device from #-1 (cassette) to #1 for disk.

LINE LENGTH WARNING: Lines loaded in this was have a limit of no more than 250 characters. That’s all the room the BASIC input buffer has. If you have a line longer than that, the end of it will be cropped off. Here is the longest line you could load this way (100 1’s, 100 2’s, then 32 dashes, and some numbers so I could count how far it went easily on the screen).

1DATA11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222--------------------------------1234567890AB

A space after the line number can always be left out of ASCII BASIC program files, and BASIC shows a space there when you LIST it anyway. But if you want a cleaner look with a space between “DATA” and your data, just remember 250 is the limit.

With this in mind, we should be able to generate a string that starts with “1DATA” then packs as many 2 character HEX values as will fit up to 250.

Tangent about line lengths…

Something interesting… If you try typing in BASIC, you will see it stops you after 249 characters (7 full lines plus 25 more characters). I don’t know why they are different.

Also, if you type a long line like this, and press ENTER, the line gets tokenized and takes up less space (assuming it has any BASIC keywords in it like “DATA”). You may then be able to EDIT the line, ‘X’ to the end and type more characters. This is referred to as “packing” the line. However, if you do this EDIT trick, and type until it stops you again, the final character gets dropped. I’m betting that has something to do with the 250 versus 249 difference somewhere in the ROM. I’ll have to look in to that some day.

Also also, you can start the lines out as one digit line numbers, like 1, 2, 3, etc. That gives you more room than if you used line “1000”. Then, you can rename it later, and the tokenized line will still have your full data. Line numbers are represented by 2-bytes once the program is tokenized so it doesn’t matter if it’s line 1 or line 60000 it takes up the same amount of space.

But I digress…

In the next installment, I’ll come up with some kind of program that makes the program so we don’t have to.

Until then…

Drawing a maze level on the CoCo – part 2

See Also: part 1, part 2, part 3, part 4 and part 5.

Making a maze of blocks look less blocky

Okay, where were we?

Right. We wanted to take this…

Pac-Man maze in ASCII

…and turn it in to this…

Pac-Man maze in 64x32 semigraphics.
Pac-Maze in 64×32 semigraphics.

…while storing it like this…

DATA FFFFFFF0

To round or not to round…

If the maze were stored as bits representing blocks that were either set or unset (32×16 blocks will fit on the CoCo 32 column screen), we could make the program handle changing those blocks in to ones with “rounded” corners, or open areas in the middle or large blocks.

First, I needed to decide what the rules are for this, so I did some drawings.

For each corner (top left, top right, bottom left, bottom right) it seemed safe to round if the block above, beside, and diagonally from the corner were empty (empty red square). BUT, only round if the opposite sides were NOT vacant (red X). If that second part isn’t there, a single block with a path all around it would have all of its corners rounded. That’s fine on high resolution graphics (it would be a circle?), but when each block is made up of 2×2 mini squares, you can’t remove each corner or you’ll have nothin’ left!

As I found out.

Next, there are large areas made up of double blocks. On the Pac-Man maze these are larger “open” areas with thin walls around them, like the four along the top of the arcade Pac-Man game:

Arcade Pac-Mac (image from Wikipedia)

Don’t fill me in…

To figure out what the rules were for this, I did more drawings…

This one took me a bit longer to figure out. Above, each mini square of block is represented by the graph. Each white box is 2 blocks by 2 blocks.

After way too much trial and error, I worked it out that any corner that has an occupied square above, beside, and diagonally qualifies to be removed.

At first I tried to brute-force this in BASIC. It was way too long, and way too slow.

Then I decided as I was scanning each block location, I’d use variables to PEEK the contents of what was up, down, left or right from the blocks, as well as up-left, up-right, down-left and down-right from it. Using variables greatly sped things up (though they are still way too slow).

Once I had those variables, I could apply some simply logic.

I noticed that in both CORNERS and CENTERS, there is a need to check for an OCCUPIED block beside and either above or below. I started there, since I could do that check once, then dispatch to a second routine to determine if it was doing a corner, a center, or both.

I ran in to many bugs during all of things, and learned I had to pass each block through all the checks… Originally, I’d find a corner, remove it, and move to the next block. That would leave other corners that should have been removed, or center bits…

Finally, I came up with this (slow) proof-of-concept:

0 REM MAZEVERT.BAS

10 CLS:FOR I=0 TO 15:READ A$:PRINT@2+32*I,A$;:NEXT
20 BL=96
30 FOR M=1024 TO 1535
40 IF PEEK(M)=BL THEN 140
50 POKE M,175:V=175
60 IF M>1055 THEN U=PEEK(M-32):UR=PEEK(M-31) ELSE U=BL:UR=BL
70 IF M>1056 THEN UL=PEEK(M-33) ELSE UL=BL
80 L=PEEK(M-1):R=PEEK(M+1)
90 DL=PEEK(M+31):D=PEEK(M+32):DR=PEEK(M+33)

100 IF U<>BL THEN IF L<>BL THEN GOSUB 200
110 IF U<>BL THEN IF R<>BL THEN GOSUB 300
120 IF D<>BL THEN IF L<>BL THEN GOSUB 400
130 IF D<>BL THEN IF R<>BL THEN GOSUB 500
140 NEXT
150 FOR M=1024 TO 1535:IF PEEK(M)=96 THEN POKE M,128
160 NEXT
170 GOTO 170

200 ' UP and LEFT set
210 IF UL<>BL THEN V=V AND NOT 8
220 IF R=BL THEN IF DR=BL THEN IF D=BL THEN V=V AND NOT 1
230 POKE M,V:RETURN

300 ' UP and RIGHT set
310 IF UR<>BL THEN V=V AND NOT 4
320 IF L=BL THEN IF DL=BL THEN IF D=BL THEN V=V AND NOT 2
330 POKE M,V:RETURN

400 ' DOWN and LEFT set
410 IF DL<>BL THEN V=V AND NOT 2
420 IF U=BL THEN IF UR=BL THEN IF R=BL THEN V=V AND NOT 4
430 POKE M,V:RETURN

500 ' DOWN and RIGHT set
510 IF DR<>BL THEN V=V AND NOT 1
520 IF L=BL THEN IF UL=BL THEN IF U=BL THEN V=V AND NOT 8
530 POKE M,V:RETURN

999 GOTO 999

1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X            XX            X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X                          X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X      XX    XX    XX      X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1100 DATA "     X XXXXX XX XXXXX X     "
1110 DATA "     X XX          XX X     "
1120 DATA "     X XX XXXXXXXX XX X     "
1130 DATA "XXXXXX XX X      X XX XXXXXX"
1140 DATA "          X      X          "
1150 DATA "XXXXXX XX X      X XX XXXXXX"
1160 DATA "     X XX XXXXXXXX XX X     "
1170 DATA "     X XX          XX X     "
1180 DATA "     X XX XXXXXXXX XX X     "
1190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1200 DATA "X            XX            X"
1210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1230 DATA "X   XX                XX   X"
1240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1260 DATA "X      XX    XX    XX      X"
1270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1290 DATA "X                          X"
1300 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"

The program has three phases:

  1. Draw the ASCII maze (or at least what will fit on the screen for this test).
  2. Scan the screen and convert non-blanks to semigraphics blocks, then check to see if a corner needs to be removed (because it is a corner or in the center of a larger group of blocks).
  3. Scan the screen one more time, changing any blank space blocks to black.

The end result is this:

Pac-Man maze in 64x32 semigraphics.

The whole process takes just over 40 seconds. It could be made faster by going in to double speed mode with POKE 65495,0 (or 65497,0 on a CoCo 3), but even then it would still be way too long for the user to wait for a game screen to draw.

Plus, this doesn’t even have the code to convert the less-memory hex string representation of the maze into the actual maze.

Which means we have more to do…

To be continued…

Pac-Man maze in 64x32 semigraphics.

Drawing a maze level on the CoCo – part 1

See Also: part 1, part 2, part 3, part 4 and part 5.

A maze of ideas

Ready to fall down a rabbit hole, exploring ways to represent a maze screen efficiently?

Neither was I.

Somewhere on this site is an article series I wrote about programming a simple maze type game on the CoCo. In it, I presented an ASCII version of the Pac-Man maze. The arcade maze looks like this:

Actual size (224×228) Pac-Man screen.

And you can learn a ton about how the game works on the Pac-Man Dosier website. But basically, the game screen is a grid of 28 x 36 tiles. In the arcade original, each tile is 8×8 pixels. With the CoCo 1/2s text screen being 32×16, this means we could fit 28 tiles horizontally with room to space, but we’d be 20 tiles short vertically. (Pac-Man used a monitor mounted sideways to get this aspect ratio.) My solution was to scroll the maze up and down. I also removed the top and bottom rows that showed score and such, so this will be using only 31 of the games original 36 rows.

A portion displayed in ASCII text would look like this:

Pac-Man maze in ASCII

Though it wouldn’t look like the arcade, this is an accurate representation of the 28×36 tiles of the original game (well, 28×16 seen any any time). There are elements of Pac-Man that couldn’t really be recreated with this low resolution (such as the way Pac can hug to corners to go around them faster), but at least the maze layout could be accurate.

But ASCII is ugly, so we’d probably want to use the semigraphics characters (128-255). Using CHR$(175) for a blue block looks like this:

Pac-Man maze in 32x16 semigraphics.
Pac-Man maze in 32×16.

That’s better, but still a bit ugly. The background could at least be made black:

Pac-Man maze in 32×16 with black background.

That’s more better, but it looks more reminiscent of blocky Atari VCS games like Adventure.

By taking advantage of the 64×32 resolution semigraphics, the corners could be at least “rounded” a bit (if round meant “still square”), and the centers of the areas could be opened up like the arcade original:

Pac-Man maze in 64x32 semigraphics.
Pac-Man maze in 64×32 semigraphics.

At this resolution, I’m not sure if we can really do any better than that ;-)

The maze data could be represented as ASCII strings, either directly PRINTed on the screen, or read from DATA statements to be PRINTed or POKEd:

Pac-Maze DATA statements.

This would make creating new mazes and levels as easy as typing. But storing 28 x 26 characters is 1008 bytes of memory. Imagine trying to do that on a 4K Color Computer or a 5K Commodore VIC-20. (The highest resolution graphics screen on a CoCo 1/2 takes up 6144 bytes, compared to the 512 bytes of the text screen.)

Here’s the ASCII DATA:

1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X            XX            X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X                          X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X      XX    XX    XX      X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1100 DATA "     X XXXXX XX XXXXX X     "
1110 DATA "     X XX          XX X     "
1120 DATA "     X XX XXXXXXXX XX X     "
1130 DATA "XXXXXX XX X      X XX XXXXXX"
1140 DATA "          X      X          "
1150 DATA "XXXXXX XX X      X XX XXXXXX"
1160 DATA "     X XX XXXXXXXX XX X     "
1170 DATA "     X XX          XX X     "
1180 DATA "     X XX XXXXXXXX XX X     "
1190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1200 DATA "X            XX            X"
1210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1230 DATA "X   XX                XX   X"
1240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1260 DATA "X      XX    XX    XX      X"
1270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1290 DATA "X                          X"
1300 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"

If we tried to use numeric DATA statements to represent the screen blocks, it would be 4 times longer since every entry would be a three digit number with a comma:

DATA 175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175,175

But, if we were just using blocks and spaces, we could have a number represent eight of them. Each number could represent a byte (0-255) and each byte contains 8 bits, therefore each number could represent 8 character blocks.

In a DATA statement with numbers, the top row would look like:

1000 DATA 255,255,255,240

The storage space for “255,255,255,240” is 15 bytes which is less than the 28 string characters (plus quotes, if needed — like if the line started with spaces or had special characters in it like a comma or ‘ REM abbreviation, which this won’t).

I suppose we could use numbers representing 16-bit values (0-65535) and have only two numbers to represent that line:

1000 DATA 65535,65520

That brings us down to 11 bytes to represent the 28 block row.

The 4K CoCo BASIC did not have hexadecimal numbers, but on Extended BASIC machines we could convert to HEX:

1000 DATA &HFF,&HFF,&HFF,&HF0

…but that is 19 bytes. While that would be better than ASCII, it would be worse than two decimal values. Doubling up to 16 bit values results in:

1000 DATA &HFFFF,&HFFF0

That gets us down to 12 bytes so it looks like needing that &H at the start loses out — 65535 versus &HFFFF. Even worse when 1 has to be three times larger as &H1. Though 15 is only one by larger as &HF. But always larger.

Hex values could be written as a string without the &H, and that could be added by the READ routine:

1000 DATA FF,FF,FF,F0

…which gives us 11 bytes, matching the two decimal values. But reading those would require some extra code which might negate the savings:

READ A$:V=VAL("&H"+A$)

…versus just reading a decimal value…

READ V

Using hex without the “&H” requires an extra 15 bytes of code space (actually, maybe a little less, since the “VAL” keyword probably tokenizes in to one or two bytes).

But, we could also pack the hex values together:

1000 DATA FFFFFFF0

That would only take 8 bytes to represent a row. But even more code would be needed to parse that:

READ A$:FOR A=1 TO LEN(A$) STEP 2:V=VAL("&H"+MID$(A$,A,2)):NEXT

The code gets even longer. However, if we had enough data (and 36 rows of data probably is enough), the code space savings should be smaller overall. And clever programming could be done to have one massive hex string and decode it knowing how many bytes go to each line.

But this only gets us to displaying the maze in ASCII or as colored blocks. If we wanted to get that “rounded” corner look of a 64×32 display, we’d need twice as many bytes.

Or we could just make the computer do it for us.

To be continued…