Change the step value in line 5 to make the spiral tighter (.2) or wider (.05).
0 'SPIRAL.BAS
5 ST=.1
10 PMODE 4,1:PCLS:SCREEN 1,1
20 FOR R=1 TO 165
30 CIRCLE(128,96),R,1,1,S,S+ST
40 S=S+ST:IF S>1 THEN S=S-1
50 NEXT
999 GOTO 999
As an experiment, I switched it to PMODE 0 so there could be eight graphics pages of spirals which could be page flipped to make a cool hypnotic animation.
2023-02-14 – See the comments from William Astle for more background on the memory usage.
Hello once again from the land of Disk BASIC. Today we look at another command: FILES
Device numbers in Color BASIC are hard-coded, meaning a specific number goes to a specific device. Here are some common device numbers:
#-2 – printer
#-1 – cassette
#0 – screen
#1 to #15 – disk
When Disk BASIC is added to the system, an extra 2K of memory is reserved for disk functionality. This memory is located directly after the 32-column screen memory (1024-1536) and it looks like this:
This is why a disk-based CoCo has less memory available for programs than a non-disk CoCo.
BASIC also reserves four pages of graphics memory (6144 bytes total) after this, which is why a BASIC program starts at 9729 in memory on a disk-based system. Memory locations 25 and 26 track where a BASIC program begins:
PRINT PEEK(25)*256+PEEK(26)
9729
While you can use the PCLEAR command to increase reserved graphics memory to eight pages maximum (PCLEAR 8), you cannot get rid of all of them. Due to an oversight or bug in Extended BASIC, “PCLEAR 0” is not valid. You always have to have at least 1536 bytes set aside for graphics, even if you aren’t using them. (Thus why I think this is a bug.) Look at my PCLEAR 0 article for details on how to perform a PCLEAR 0 and get the most memory for BASIC.
For this article, however, I wanted to dive a bit in to how memory is being used for Disk BASIC. If BASIC starts at 9729, and graphics memory is 6144 bytes before it, and the 32-column screen ends at 1536, I should be able to figure out how much memory is reserved for Disk BASIC.
Subtracting the size of graphics memory from the top of BASIC tells me where graphics memory should begin:
PRINT 9729-1-6144
3584
I subtract one there because the BASIC program starts at 9729, meaning the last byte of graphics memory is actually at 9728, one byte earlier.
And since I know the 32-column screen ends at 1535, bytes 1536 to 3135 should be for disk use:
PRINT 3584-1536
2048
Disk Basic is using memory from 1536 to 3584, followed by the graphics memory starting at 3585.
As the FILES manual entry states, by default there are two disk buffers (devices) reserved – #1 and #2. If you need more, you use the FILES command. You can go all the way to FILES 15 and have the ability to open fifteen files at the same time!
If you have enough memory, that is.
On startup, a Disk BASIC system has 22823 bytes available for BASIC. That includes room for two disk devices. If you aren’t using them, you can type FILES 0 and get some extra memory for BASIC:
Above, you can subtract the “after” memory 23335 minus the “before” memory 23823 and get 512. Disk device #1 and #2 take up 512 bytes.
And this tells me that manual entry is possibly wrong since it says “If you do not use FILES, the computer reserves enough memory space for two buffers (Buffer 1 and 2), and reserves a total of 256 bytes for those buffers.” (emphasis mine)
If FILES 2 reserves 256, going to FILES 0 should have only increased memory by 256 — not 512. Shouldn’t it?
I decided to write a program to show the memory after each amount of FILES buffers. I quickly learned that when you use the FILES command, it erases variables. There are other things in BASIC that will erase variables, like PCLEAR. It looks like BASIC just clears out variables rather than relocate them if they change.
Since I could not use a variable, doing this with a FOR/NEXT loop was not possible. So, BRUTE FORCE FOR THE WIN!
The semicolon at the end of the PRINT in line 160, and the endless loop GOTO on 170 just keep all fifteen lines on the screen without scrolling the top one off for the “OK” prompt. Running it gives me this:
Let’s look at that in text form, and I’ll highlight some odd entries:
FILES 0 – 22953
FILES 1 – 22953
FILES 2 – 22441
FILES 3 – 22441
FILES 4 – 21929
FILES 5 – 21417
FILES 6 – 21417
FILES 7 – 20905
FILES 8 – 20905
FILES 9 – 20393
FILES 10 – 20393
FILES 11 – 19881
FILES 12 – 19881
FILES 13 – 19369
FILES 14 – 19369
FILES 15 – 19369
The values immediately look odd, since memory doesn’t change between FILES 0 and FILES 1. But, FILES 0 seems to work. If you do that, you can’t OPEN “O”,#1,”FILES” without getting a ?DN ERROR (device number).
When FILES 2 happens, memory goes down by 512 bytes. FILES 3 doesn’t change anything, so it looks like it is allocating two buffers at a time, even if you just wanted one.
FILES 3 to FILES 4 goes down by 512 then FILES 4 to FILES 5 goes down by 512 as well.
That’s clearly not a pattern. In the above list, FILES 4 and FILES 13 to 15 stand out. For FILES 4, it jumps 512 for just that one addition device number, and for 13-15 they all report the same amount of memory.
I do not understand why. But, now that I know this, I can see you might as well use FILES 15 even if you only wanted FILES 13 because they take the same amount of memory.
Or do they?
Not-so-top secret FILES
There is a second option to FILES which is total size for how much memory is reserved for the buffers. But, it doesn’t work quite like I would expect. For example, if you do:
FILES 15,1000
PRINT MEM
…you should see 18215. 1000 bytes are being reserved for all fifteen buffers. But then if you do…
FILES 15,2000
PRINT MEM
…you might expect it to be 1000 less (17215) but on my system I see 17191 – 1024 bytes less. That’s 1K, so perhaps it’s just rounding to some multiple allocation size. We can test…
That wonderful brute force program shows us something interesting:
Even though each allocation is only asking for 100 bytes more, we only see increases in multiples of 512. That must be the allocation size Disk BASIC is using. This may mean that when we are allocating more buffers, the actual space they need in not a multiple of 512 so that produces the odd increases the first example demonstrated.
If you run this program, it starts off with 0 bytes used (MU) of an allocation of 0 (MA). It asks how much you want to allocate, and you can type in 100 bytes (A). If there isn’t enough memory allocated (MA) to fit another 100 bytes, it will add a new block of 512 (SZ) and continue.
I have not looked through the Disk BASIC manual to see if this is explained, nor have I looked at Disk Basic Unravelled, but I expect the answer is found in one of those.
For now, let’s just understand that using FILES with different values changes how much memory is reserved.
FILES this under…
With that understanding, the FILES command may be tricky to use efficiently in a complex program. Much like the CLEAR command reserving string space, knowing how much memory you need may take some thought if you don’t have enough free memory to just specify “a bunch.”
In a future article, I’d like to explore more about how these buffers are used, but in general it’s probably safe to assume that when we specify a record size for a direct access disk file:
OPEN "D",#1,"USERLOG",256
…and then use GET to read a record, that record has to go somewhere in memory. Above, with a record size of 256, I suspect we’d need at least a 256 byte buffer. Indeed, if you try to do this, you will get an ?OB ERROR (out of buffer space):
10 FILES 1,256
20 OPEN "D",#1,"USERLOG",300
...
But, changing the FILES command to specify 300 bytes for the buffer allows it to work:
10 FILES 1,300
20 OPEN "D",#1,"USERLOG",300
...
Oh. Maybe it is that easy to understand after all? And I suppose when we use multiple buffers (like reading from #1 and writing to #2), we’d need double that amount…
10 FILES 2,600
20 OPEN "D",#1,"OLDFILE",300
30 OPEN "D",#2,"NEWFILE",300
...
Indeed, FILES 2,600 works, but FILES 2,599 will show ?OB ERROR.
In a way, I’m surprised. I kind of expected the memory would be checked until you actually went to GET something, but I guess it pre-allocates.
This leaves me with another… What about FIELD? That command allows a record to be split up in to different fields and assigned to different variables. For example, if I wanted the 300 byte record to contain three 100 byte fields, I’d add:
10 FILES 2,600
20 OPEN "D",#1,"OLDFILE",300
25 FIELD #1,100 AS A$,100 AS B$,100 AS C$
30 OPEN "D",#2,"NEWFILE",300
...
…and this seems to work fine, so it doesn’t appear FIELD needs any extra space. (Note to self: look up how that works in the Unraveled disassembly book.)
I suppose this rabbit hole is pretty deep right now, so I’ll end this article by saying…
2023-02-09 – In the comments, Lee pointed out a type in the first example, which has been fixed, and an error in the logic of the final routine which tries to write a partial line if it cannot write all of it. The original code tried to write using LEN() of the string, but needs to be adjusted to subtract one, since the PRINT will add one extra character (carriage return) at the end. The code has been corrected, and actually tested not. Thanks, Lee!
In Disk BASIC, you can open a file for input and read from it like this:
10 CLEAR 255
20 OPEN "I",#1,FILE.TXT"
30 LINE INPUT #1,A$
40 PRINT A$
50 GOTO 30
That would keep reading lines (data terminated by an ENTER) from the file and printing it to the screen until it reached the end of the file. It would then crash and report an Input Past End Of File error:
?IE ERROR IN LINE 30
Avoiding IE ERROR
If we were using a fixed-format file, such as a configuration file, and we knew exactly what was expected to be in it, this wouldn’t be an issue. Suppose we had a file that just contained a Name, Address, City, State and Zip code. We could read just those entries like this:
0 ' NAME, ADDRESS, CITY, STATE, ZIP
10 PRINT "READING ADDRESS..."
20 OPEN "I",#1,"ADDRESS.TXT"
30 LINE INPUT #1,NM$
40 LINE INPUT #1,AD$
50 LINE INPUT #1,CT$
60 LINE INPUT #1,ST$
70 LINE INPUT #2,ZP$
80 CLOSE #1
For arbitrarily-sized files, like a text document from a word processor, you don’t know how many lines may be in it so that won’t work.
One solution is to write out how many lines are in the file as the first entry. For a file containing seven lines of text, it might look like this:
7
THIS IS THE FIRST LINE.
THIS IS SECOND.
AND THIRD IS HERE.
FOURTH CHECKING IN!
FIFTH FOREVER.
SIXTH GOES HERE.
AND THIS IS THE SEVENTH AND FINAL LINE.
A routine to read this might look like:
10 INPUT "I",#1,"FILE.TXT"
20 INPUT NL
30 FOR I=1 TO NL
40 LINE INPUT #1,A$
50 PRINT A$
60 NEXT
70 CLOSE #1
This can also be used on cassette files just by changing the device number from #1 to #-1.
EOF
Disk BASIC provides the EOF function that will return the status of an open input file. It will return 0 as long as there is more data in the file, or -1 if the end of the file has been reached. That simplifies the reader code to look like this:
10 OPEN "I",#1,"FILE.TXT"
20 IF EOF(1)=-1 THEN 60
30 LINE INPUT #1,A$
40 PRINT A$
50 GOTO 20
60 CLOSE #1
And again, by changing the device from #1 to #-1 and EOF(1) to EOF(-1) it should work on cassettes. (Note to self: Does it? I haven’t actually tried this in almost forty years.)
When it comes to writing data, if you try to write more than the disk can hold, say, in an endless loop like this…
10 OPEN "O",#1,"FILE.TXT
20 PRINT #1,"THIS LINE GOES IN THE FILE"
30 GOTO 20
…you will eventually fill up the disk and get a Disk Full error:
?DF ERROR IN 20
Avoiding DF ERROR
Unfortunately, EOF only works on files opened for input and there is no similar command that tests output. And if there were, how would it work? If it just returned “yes, there is still room” versus “no, the disk is full”, what does “still room” mean? One byte left? What if you wanted to write two bytes?
Let’s pretend there is a Disk Is Full command called DIF:
10 OPEN "I",#1,"FILE.TXT"
15 ' FAKE DIF FUNCTION THAT DOESN'T EXIST
20 IF DIF(1)=-1 THEN 50
30 PRINT #1,"THIS LINE GOES IN THE FILE"
40 GOTO 20
50 CLOSE #1
Above, if the disk still had room (even if it was just one byte), line 20 would return a 0 (“yep, there is still room”) and then we’d crash with a ?DF ERROR in the next line, since we tried to write more than that one byte left.
What we really need is a command that tells us how many more bytes of data we can write to the disk, and we don’t have that.
But we can make something close. Close-ish.
New features for FREE
Disk BASIC has the FREE command which returns the number of free granules on a specified drive. There are 68 granules available on an empty disk. If you know what a granule is, that means something, but all I really knew back then was the largest program I’d ever seen was “13 grans”…
TL:DNR – A granule is 2304 bytes.
A CoCo RS-DOS (what we called Disk BASIC) disk is made up of thirty-five (35) tracks, each holding eighteen (18) 256-byte sectors. That is 161,280 bytes of storage (35 * 18 * 256), which really seemed like a bunch in the early 1980s! Track seventeen (17) is used for the disk directory and file allocation table (FAT), so there is really only 34 tracks you can use for programs or data storage which gives you 156,672 bytes (34 * 18 * 256).
For reasons I do not know, each track was divided up in to two (2) granules. That makes a granule represent nine (9) sectors. Therefore, a granule is 2304 bytes. (9 * 256 = 2304).
When FREE(0) returns 68 for an empty disk, that is 156,672 bytes free. You can multiply the value FREE returns by 2304 to see how many bytes are free.
On first glance, it seems like you could just check for FREE(0) to be 0, and stop writing when it is:
10 OPEN "I",#1,"FILE.TXT"
20 IF FREE(0)=0 THEN 50
30 PRINT #1,"THIS LINE GOES IN THE FILE"
40 GOTO 20
50 CLOSE #1
However, once a new file is created (even if you have written nothing to it yet), the number of free granules goes down by one since there is no longer a full granule available. This can be demonstrated using a program like this:
Running that on an empty disk should print the following:
RUN
68
67
67
Above, initially there were 68 granules available, then we opened/created a new file and then there were 67 granules used. We wrote one byte (“A”) to that new file and there were still 67 unused granules. We are creating a file an allocating that 68th granule for it, whether we write any data to it or not. After writing one byte to that granule, we can not tell how much room is left in it. This means if you had started with only ONE free granule and ran this program, you would see:
1
0
0
The moment the file was opened, a granule was consumed for this new file, leaving zero granules available. Thus, the check for FREE(0) being zero would immediately be satisfied, and even though you had bytes available to use in that granule, the program would skip writing because FREE was returning zero…
This approach simple won’t work if there is only one granule left (a file would be created, then no data would be allowed to be written). And, if there was more than one granule left, it would be wasteful since even writing one byte in to the new granule would count as “full” and the rest of those 2304 bytes would be wasted.
Side Note: Since the smallest amount of allocated space you can have is one granule, a file of one byte consumed 2304 bytes on the disk, just as a file of 2304 bytes would. MS-DOS and other file systems have the same issue with their allocation sizes.
Code compensation
Disk BASIC has no solution for us, by we can easily come up with our own. As long as we control what is being written, we should be able to determine the size of what is being written and do something like this:
0 '
1 ' DF - DISK FREE (BYTES)
2 ' DU - DISK USED (BYTES)
3 '
10 DF=FREE(0)*2304:DU=0
20 OPEN "O",#1,"FREETEST"
30 A$="WRITE THIS TO THE FILE"
40 IF DU<DF THEN GOSUB 1000 ELSE 60
50 GOTO 30
60 CLOSE #1
999 END
1000 ' WRITE A$ TO BUFFER #1
1001 ' COUNT BYTES WE WILL WRITE,
1002 ' ADD ONE FOR THE ENTER
1010 DU=DU+LEN(A$)+1
1020 IF DU<DF THEN PRINT #1,A$
1030 RETURN
The idea behind this code is that a subroutine is used to write whatever is in A$ to the disk file. That subroutine will keep track of how many bytes were written. In line 10, DF is initially set to the number of free bytes by taking FREE(0) and multiplying it by the size of a granule (2304 bytes). DU is how much of that has been used, so it starts out at 0.
In the main loop, as long as DU is less than DF, the subroutine can be called to (attempt to) write A$ to the disk file.
In the subroutine at line 1000, DU is incremented by the length of A$, and 1 is added since PRINT will add an ENTER character to the end of the line. If DU is less than DF, the line is actually written to the file. Otherwise, it is skipped.
This could be made more elegant, but it’s a simple approach that should work just fine.
Code compensation, improved
Some improvements might be to make it not just give up if all the data won’t fit, but to write as much as possibly leaving disk free at zero at the end. This could be done like this:
1000 ' WRITE A$ TO BUFFER #1
1001 ' COUNT BYTES WE WILL WRITE,
1002 ' ADD ONE FOR THE ENTER
1010 LN=LEN(A$)+1
1020 IF DU+LN>DF THEN LN=DF-DU
1030 IF LN>0 THEN PRINT #1,LEFT$(A$,LN-1)
1040 DU=DU+LN
1050 RETURN
…or something like that. The idea is if there is only ten bytes left, and you try to write 15, it will just right the first ten bytes of that string and then RETURN. This, too, could be made more elegant.
How would you improve it? Leave your suggestions in the comments…
Sadly, there’s not much you can do when using a cassette since there is no way to know how much tape is left.
It was almost identical to the description of how to use PRINT for disk access, except PRINT mentioned a semicolon:
I don’t remember if I used PRINT or WRITE back then, but “knowing what I now know” I wondered if this Disk BASIC command would work with other device numbers, such as tape (#-1) or screen (#0). I gave it a try, and found it did work with #0 to print a message to the screen. This is how I learned the difference between WRITE and PRINT:
It appears WRITE will enclose the string in quotes. When I tested with a numeric variable, it looked the same as PRINT output. I also noticed you couldn’t end a WRITE line with a semicolon (thus, maybe, the reference to PRINT being able to use a semicolon in the manual).
If you look above, you will also see WRITE was trying to add a comma, which led me down a rabbit hole trying to figure out what all was going on.
Comma little bit closer
Some quick experiments showed me that WRITE was doing much more than just acting like a PRINT replacement that could not use a semicolon. WRITE ignores TAB positions, which normally happen when you try to PRINT something separated by a comma. For the CoCo’s 32-column screen, the comma uses 16 for the tab position so if you print…
PRINT "THIS","THAT"
…you will get something like this:
12345678901234567890123456789012
+--------------------------------+
|THIS THAT | 32-col screen
| |
If you use PRINT to put messages in a file on tape or disk, I expect they would have the tab spaces inserted in the file as well.
Tangent testing
Obviously I had to test that. I decided to take my hexdump program I shared last time and modify it to dump the bytes in a file I wrote to using PRINT and commas to see what was in it. The program looks like this:
0 'DUMPFILE.BAS
10 ' CREATE TEST FILE
20 OPEN "O",#1,"TEST"
30 PRINT #1,"THIS","THAT","OTHER"
40 CLOSE #1
50 ' DUMP TEST FILE
60 OPEN "D",#1,"TEST",1
70 FIELD #1,1 AS BT$
80 OF=0:C=0
90 FOR R=1 TO LOF(1)
100 IF C=0 THEN PRINT:PRINT USING"#### ";OF;
110 GET #1,R
120 BT=ASC(BT$)
130 IF BT<&H10 THEN PRINT "0";
140 PRINT HEX$(BT);" ";
150 C=C+1:IF C>7 THEN C=0
160 OF=OF+1
170 NEXT
180 CLOSE #1
When I ran that, I did see that it was padding the words with spaces out to the 16 character tab position:
Above, I PRINTed “THIS”, “THAT” and “OTHER” to the file, separated by commas. In the hex dump output, the hex values 54 38 39 53 are “THIS”, followed by hex value 20s (space). You can see it goes all the way to the end of the second line. Each hex dump line represents eight characters, so the comma tabbed out to the 16th character.
At offset 16 are hex values 54 48 41 54 which is “THAT”, followed by spaces out to the next 16th tab position (end of line four).
At offset 32 there is just 4F 54 48 45 52 which is “OTHER” followed by 0D which is CHR$(13) for ENTER.
Checks out! But I digress…
PRINT versus WRITE versus COMMAS
WRITE, on the other hand, will put string items in quotes and output a comma character rather than tab spaces. Here’s an example of WRITE versus PRINT:
With this understood, the difference between WRITE and PRINT now becomes clear.
SIDE NOTE: Just like with PRINT, you don’t need to specify device #0. You can simply do WRITE “HELLO, WORLD!” and you will get a nicely quoted “HELLO, WORLD!” message to the screen.
If I were to modify the test program to use WRITE instead of PRINT, it would look like this:
0 'DUMPFILE2.BAS
10 ' CREATE TEST FILE
20 OPEN "O",#1,"TEST"
30 WRITE #1,"THIS","THAT","OTHER"
40 CLOSE #1
50 ' DUMP TEST FILE
60 OPEN "D",#1,"TEST",1
70 FIELD #1,1 AS BT$
80 OF=0:C=0
90 FOR R=1 TO LOF(1)
100 IF C=0 THEN PRINT:PRINT USING"#### ";OF;
110 GET #1,R
120 BT=ASC(BT$)
130 IF BT<&H10 THEN PRINT "0";
140 PRINT HEX$(BT);" ";
150 C=C+1:IF C>7 THEN C=0
160 OF=OF+1
170 NEXT
180 CLOSE #1
And if I run that, I expect we’d see the addition of the quote character around each word, and a comma character in place of the run of spaces that PRINT added. Let’s try:
It looks like it works as predicated. Hex 22 must be the quote character, then 54 48 49 53 is “THIS”, then a closing quote 22, followed by a 2C which must be the comma, then another quote 22 and “THAT” followed by a quote 22, then another comma 2C, then a quote 22 and “OTHER” with a final quote 22 and ENTER 0D.
That really packs the data much better than using PRINT. I had no idea when I was PRINTing comma separated numbers to a file it was padding them out with all those spaces! Apparently, when you INPUT the data back, it must be checking for the spaces to know where the next value starts or something, or maybe that won’t work at all. I need to test this, sometime, too…
There’s no time like sometime
I wrote a simple program that PRINTs out three strings separated by commas and reads them back.
0 'PRINTREAD.BAS
5 A$="HELLO, WORLD!"
6 B$="DON'T PANIC!"
7 C$="WELL... OKAY, THEN."
10 OPEN "O",#1,"TEST"
20 PRINT #1,A$,B$,C$
30 CLOSE #1
40 '
50 OPEN "I",#1,"TEST"
60 INPUT #1,X$,Y$,Z$
70 PRINT X$:PRINT Y$:PRINT Z$
80 CLOSE #1
Running this gives me results I do not want:
This is because INPUT separates things by a comma, so if the string is:
HELLO, WORLD!
…doing INPUT A$,B$ would put “HELLO,” in A$, and “WORLD!” in B$. But the output above doesn’t quite look like that, and that’s due to there being no commas between the three strings we PRINTed. Instead, it was adding spaces to the next TAB position. Therefore, if we looked at the contents of the file as if it was the screen, the data might look like:
BUT! If you type that in and hit enter, you will then see “?? ” as a prompt, because BASIC is looking for the third parameter. It found an element, then a comma (that was the first so it goes in to A$), then a bunch of stuff and an end of line (that goes in to B$) and still wants to find the C$. If you do:
10 INPUT A$,B$,C$
20 PRINT A$:PRINT B$:PRINT C$
And RUN that, you can type each of those three items on a line by itself and it will work.
RUN
? THIS
?? THAT
?? OTHER
THIS
THAT
OTHER
BUT, if you are doing an INPUT from a file, there is no way to prompt the user that something is missing, so apparently INPUT just skips it and returns what it was able to find.
To make INPUT accept a comma as part of what you type, you can surround it by quotes. That would let you do this:
RUN
? "HELLO, WORLD!"
?? "I THINK, THEREFORE..."
?? "BUT, WAIT!"
HELLO, WORLD!
I THINK, THEREFORE...
BUT, WAIT!
INPUT requires the quotes so it doesn’t split up a string that might contain a comma. Which means you could have typed them all on one line like this:
RUN
? "HELLO, WORLD!","I THINK, THEREFORE...","BUT, WAIT!"
HELLO, WORLD!
I THINK, THEREFORE...
BUT, WAIT!
In order to output a quoted string to a file, you would have had to manually add the quote characters using CHR$(34) like this:
PRINT #1,CHR$(34)"THIS WILL BE QUOTED"CHR$(34)
And, when using output to cassettes, I guess that’s how you had to do it, since there was no WRITE command in Color BASIC or Extended Color BASIC!
And, since the first example didn’t have enough commas to use to separate the string values, you cannot use PRINT like that for strings and expect INPUT to read them back:
0 'NOWORKY.BAS
10 OPEN "O",#1,"TEST"
20 PRINT #1,"THIS","THAT","OTHER"
30 CLOSE #1
40 '
50 OPEN "I",#1,"TEST"
60 INPUT #1,A$,B$,C$
70 PRINT A$:PRINT B$:PRINT C$
80 CLOSE #1
The above program will produce an ?IE ERROR IN 60 because it never found a comma and therefore only sees one long entry that looks like those three words with a bunch of spaces between each of them. The only reason the previous example got as far as it did was due to having a comma in one of the strings. #TheMoreYouKnow
Input Crosses the Line
Extended BASIC added the LINE INPUT command which can only input strings, and drops support for the comma. It treats everything as a literal string (which can contain commas and even quotes). It is a superior INPUT for strings. You can do:
10 LINE INPUT "TYPE:";A$
20 PRINT A$
…and then you can type things that contain a comma and it works just fine:
RUN
TYPE:THIS, MY FRIENDS, IS COOL.
THIS, MY FRIENDS, IS COOL.
It also allows you to have quotes in the string:
RUN
TYPE:"I AM QUOTED!"
"I AM QUOTED!"
While you can use LINE INPUT on tape or disk files (if you have Extended or Disk BASIC), you can no longer separate data with commas. This means only one entry per line in the file.
If you want to use INPUT so you can have multiple items on each line, you either need to make sure they don’t contain commas or, if they do, quote them, and put commas between each quoted string.
PRINT #1,CHR$(34)"THIS"CHR$(34)","CHR$(34)"THAT"CHR$(34)","CHR$(34)"AND, OF COURSE, OTHER"CHR$(34)
Or use WRITE and it takes care of that for you.
WRITE #1,"THIS","THAT","AND, OF COURSE, OTHER"
Changing the example to use WRITE instead of PRINT works nicely:
And that, in a nutshell, is the difference between using WRITE and PRINT, and why you might want to use WRITE/PRINT versus PRINT/LINE INPUT.
Bonus Tip
In a comment to the previous entry, William “Lost Wizard” Astle added:
You can also use “R” for random files. It’s exactly the same as “D”.
– William Astle
So I guess OPEN “R”,#1,”FILE” and OPEN “D”,#1,”FILE” do the same thing. If I ever knew that, I don’t now. Well, except now I do, again or for the first time, thanks to William.
On a tape-based Color Computer (Color BASIC or Extended Color BASIC), you could write data out to a tape file by opening device #-1 for Output (“O”) like this:
0 'TAPEWRIT.BAS
10 OPEN "O",#-1,"MYFILE"
20 PRINT #-1,"THIS IS IN THE FILE"
30 PRINT #-1,"SO IS THIS"
40 PRINT #-1,"AND THIS IS AS WELL"
50 CLOSE #-1
By having a tape inserted in the recorder, and PLAY+RECORD pressed, when that program runs the cassette relay in the computer would click on, starting the tape motor, and the three lines of text would be written to the file. The file would look like this:
THIS IS IN THE FILE(enter)
SO IS THIS(enter)
AND THIS IS AS WELL(enter)
After rewinding the tape and pressing PLAY, running this program would open the same file for Input (“I”) and read and display that data:
0 'TAPEREAD.BAS
10 OPEN "I",#-1,"MYFILE"
20 IF EOF(-1)=-1 THEN 60
30 INPUT #-1, A$
40 PRINT A$
50 GOTO 20
60 CLOSE #-1
NOTE: In line 30, if you have Extended Color BASIC, if reading strings use LINE INPUT instead of INPUT. That will allow lines that have commas, quotes, and other special characters in it, which INPUT will not.
In line 20, the EOF function is used to check if there is more data in the file. If you knew exactly how much data was in the file (like a configuration file that always has the same information), you could just do that many INPUTs. If the amount of data is not known, EOF must be used to avoid an end-of-file error when you try to read past the end of data.
Now you have a simple program that would read and print as many lines as are in the file.
Disks Do More
When a floppy disk controller is added, Disk BASIC comes along for the ride. While cassettes use device #-1, disks can use devices #1 to #15. This allows multiple files to be open at the same time, and on different drives. (Disk BASIC supported four floppy drives simultaneously.)
We can change the above sequential file programs to work on a disk system just by changing device #-1 to be device #1:
0 'DISKWRIT.BAS
10 OPEN "O",#1,"MYFILE.TXT"
20 PRINT #1,"THIS IS IN THE FILE"
30 PRINT #1,"SO IS THIS"
40 PRINT #1,"AND THIS IS AS WELL"
50 CLOSE #1
…and…
0 'DISKREAD.BAS
10 OPEN "I",#1,"MYFILE.TXT"
20 IF EOF(1)=-1 THEN 60
30 LINE INPUT #1, A$
40 PRINT A$
50 GOTO 20
60 CLOSE #1
The only change I made other than the device number was adding an extension to the filename. Since a disk file can have a three-character extension, I used “.TXT”. If you leave off the extension, it will be created as “.DAT” for a data file.
With a sequential file, each entry is expected to have a carriage return at the end. You can write out a single line, like the earlier example:
PRINT #1,"THIS IS AN ENTRY"
…or even write out numeric data, separated by commas:
PRINT #1,A,B,C,D
In the disk file will either be “THIS IS AN ENTRY” with an ENTER at the end, or the three numbers with an ENTER at the end.
5 A=1:B=2:C=3
10 OPEN "O",#1,"NUMBERS"
20 PRINT #1,A,B,C
30 CLOSE #1
40 '
50 OPEN "I",#1,"NUMBERS"
60 INPUT #1,X,Y,Z
70 PRINT X;Y;Z
80 CLOSE #1
As the name implies, data is sequential — one after the next. If you had a file with 1000 entries in it, and wanted to get to the 1000th entry, you would have to read through the 999 entries first.
Direct Access
A far more powerful form of disk access is Direct. This allows you to create a file that is not made of arbitraty strings that end in a carriage return. Instead, the file can be a set of records (of a size you specify). This is done by using the “D”irect access mode and specifying the record size at the end of the OPEN.
With a direct access file, you can specify a record size, and then write or read to any record you want. (This is usually called “random access” these days.)
Here is an example that creates a file with 32-byte records, and writes three entries to it:
0 'DISKWRIT2.BAS
10 OPEN "D",#1,"MYFILE2.TXT",32
20 PRINT #1,"THIS IS IN THE FILE"
25 PUT #1,1
30 PRINT #1,"SO IS THIS"
35 PUT #1,2
40 PRINT #1,"AND THIS IS AS WELL"
45 PUT #1,3
50 CLOSE #1
For a direct access file, using PRINT (or the WRITE command, which seems to do the same thing), the data goes in to a buffer and won’t be written to the disk until PUT is used to tell it which record of the disk file to write it to. The file would look like this:
11111111111222222222233
12345678901234567890123456789012
+--------------------------------+
|THIS IS IN THE FILE | record 1
+--------------------------------+
|SO IS THIS | record 2
+--------------------------------+
|AND THIS IS AS WELL | record 3
+--------------------------------+
(There would also be an ENTER at the end of each line, normally.)
The program to read back and display the records looks like this:
0 'DISKREAD2.BAS
10 OPEN "D",#1,"MYFILE2.TXT",32
20 FOR R=1 TO LOF(1)
30 GET #1,R
40 LINE INPUT #1,A$
50 PRINT A$
60 NEXT
70 CLOSE #1
Above, you see the addition of “,32” to specify 32 byte records, and the use of LOF which is the length of file (number of records). In our example, this should be 3, matching the three records we wrote in the previous example.
To load a record in to the buffer, GET is used, followed by a LINE INPUT to read it in to a string.
Now if 1000 entries had been written in to a direct access file, we could retrieve any record we wanted just by using GET #1,42:LINE INPUT A$ or whatever.
Breaking Records
A record can be treated like a string of a maximum size. When you PRINT or WRITE that record, it must be smaller than the record size, and have the ENTER at the end. The ENTER is needed for INPUT/LINE INPUT to know where the end of that record is.
But, you can also break a record up in to specific entries. For instance, first name, middle initial, and last name. This is done using the FIELD command. You tell it the buffer (device) number and how many bytes to assign to a variable. For example, if you wanted a 32 byte record to look like this:
11111 11111111
12345678901234|1|12345678901234567
+--------------+-+-----------------+
| First Name |I| Last Name |
+--------------+-+-----------------+
…with fourteen (14) characters for the First Name, one (1) character for the Initial, and fifteen (17) characters for the Last Name, and you wanted them in variables F$, I$, L$, you would use:
FIELD #1,14 AS F$,1 AS I$,17 AS L$
(I wanted to use FN$ for first name but FN is a reserved keyword used for the DEF FN function and it cannot be used for a variable.)
If you do that, you no longer use INPUT/LINE INPUT. Instead, when you GET the record, it loads the appropriate bytes in to the variables for you! Nifty!
And, to write it, you reverse the process by loading the variables (using LSET or RSET) and then using PUT. Also nifty! Here is a program that adds three First/Initial/Last records:
0 'DISKWRIT3.BAS
10 OPEN "D",#1,"NAMES.DAT",32
15 FIELD #1,14 AS F$,1 AS I$,17 AS L$
20 LSET F$="ALLEN":LSET I$="C":LSET L$="HUFFMAN"
25 PUT #1,1
30 LSET F$="ARTHUR":LSET I$="P":LSET L$="DENT"
35 PUT #1,2
40 LSET F$="TRICIA":LSET I$="M":LSET L$="MCMILLAN"
45 PUT #1,3
50 CLOSE #1
If you try to just assign the variable and then PUT, it doesn’t work (or at least, did not for me). The example in the Disk BASIC manual show this being done with LSET and RSET to assign those variables to the buffer (left or right justified). After the write, the disk file looks something like this:
11111 11111111
|12345678901234|1|12345678901234567
+--------------+-+-----------------+
|ALLEN |C|HUFFMAN | record 1
+--------------+-+-----------------+
|ARTHUR |P|DENT | record 2
+--------------+-+-----------------+
|TRICIA |M|MCMILLAN | record 3
+--------------+-+-----------------+
Using LSET puts the entry in to the left, and using RSET would right justify it instead. (What is this for, anyone know?) RESET would make the file look like this:
11111 11111111
12345678901234|1|12345678901234567
+--------------+-+-----------------+
| ALLEN|C| HUFFMAN| record 1
+--------------+-+-----------------+
| ARTHUR|P| DENT| record 2
+--------------+-+-----------------+
| TRICIA|M| MCMILLAN| record 3
+--------------+-+-----------------+
…and here is the program that reads them back and displays them:
0 'DISKREAD3.BAS
10 OPEN "D",#1,"NAMES.DAT",32
15 FIELD #1,14 AS F$,1 AS I$,17 AS L$
20 FOR R=1 TO LOF(1)
30 GET #1,R
40 PRINT F$;" ";I$;". ";L$
60 NEXT
70 CLOSE #1
With this in mind, we could make a program that dumps out all the bytes in a file by making the record size one (1) byte, like this:
0 'DIRECT.BAS
10 '
11 ' CREATE A FILE
12 '
20 OPEN "O",#1,"FILE.TXT"
30 PRINT #1,"DON'T PANIC!"
40 CLOSE #1
100 '
101 ' OPEN DIRECT ACCESS
102 '
110 OPEN "D",#1,"FILE.TXT",1
115 FIELD #1,1 AS BT$
120 NR=LOF(1)
130 PRINT "RECORDS: ";NR
140 FOR R=1 TO NR
150 GET #1,R
170 PRINT ASC(BT$);
180 NEXT
190 CLOSE #1
The important part are lines 100-190. You could remove the earlier lines that just make a test file, and modify this to “dump” any file you want. Here’s a simple HEX dump program:
0 'HEXDUMP.BAS
10 LINE INPUT "FILENAME:";F$
20 OPEN "D",#1,F$,1
30 FIELD #1,1 AS BT$
40 OF=0:C=0
50 FOR R=1 TO LOF(1)
60 IF C=0 THEN PRINT:PRINT USING"#### ";OF;
70 GET #1,R
80 BT=ASC(BT$)
90 IF BT<&H10 THEN PRINT "0";
100 PRINT HEX$(BT);" ";
110 C=C+1:IF C>7 THEN C=0
120 OF=OF+1
140 NEXT
150 CLOSE #1
There is more you can do with Disk BASIC, so here are a few references to get you started:
2022-12-30 – Update to Jason’s final version to make it two bytes smaller.
In this final (?) installment, I wanted to share some other approaches that were taken to by members of the CoCo community draw this:
…including one that immediately was smaller than the version I did.
Rick Adams – PDP8/I
Early on, a version was shared by legendary CoCo programmer Rick Adams. His version was not for the CoCo – he chose to do it “in a very primitive BASIC, BASIC8 on a simulated PDP8/I running the TSS8 OS”…
0 'RICK ADAMS
12 FOR B = 1 TO 4
14 GOSUB 2000
20 NEXT B
22 C = 0
24 D = 0
30 FOR I = 1 TO 9
32 READ A, B
34 GOSUB 1000
36 NEXT I
50 FOR B = 4 TO 1 STEP -1
52 GOSUB 2000
58 NEXT B
200 DATA 0, 17, 1, 15, 2, 13, 3, 11, 4, 9, 3, 11, 2, 13, 1, 15, 0, 17
300 STOP
1000 PRINT TAB(A);
1010 FOR J = 1 TO B
1020 PRINT "*";
1030 NEXT J
1040 PRINT TAB(A + B + C);
1050 FOR J = 1 TO D
1060 PRINT "*";
1070 NEXT J
1080 PRINT
1090 RETURN
2000 A = 4
2002 D = B
2010 C = 9 - 2 * B
2020 GOSUB 1000
2030 RETURN
2046 END
I am unfamiliar with the BASIC on this machine, but at least it doesn’t require using “LET“. This version can run on the CoCo as well, and correctly reproduces the pattern.
Jim Gerrie – MC-10/CoCo
Next, take a look a this one by MC-10 BASIC-meister, Jim Gerrie:
Jim Gerrie’s fancier solution
His approach uses DATA statements and then draws the star in an interesting way.
Jason Pittman
In the comments on an earlier installment, Jason shared his attempt. His approach was realizing that the shape was just “four overlapping right triangles.”
This version is just 100 bytes! Due to the CoCo’s 32 column screen being too short, it doesn’t draw the top and end lines of the pattern, so it wouldn’t meet the challenge requirements. To fix that, he needed to add an IF:
1 FORX=32TO416STEP32:L=X/32:T$=STRING$(L,42):PRINT@X-28,T$;:PRINT@(X-19-L),T$;:IF X>32THEN PRINT@544-X+4,T$;:PRINT@557-X-L,T$;
2 NEXT
3 GOTO3
Since the CoC 3 also has a 40×24 and 80×24 screen, the entire pattern could fit on those screens. Version three looked like this:
That one is a mere 88 bytes! And, the GOTO1 at the end is just to make it keep redrawing, else it stops near the top and would print the “OK” in the middle of the pattern.
I’d say the “WIDTH40:” is not required, since you could just say “run this from the 40 column screen.” And, to keep the loop, starting on LINE 0 allows just saying “GOTO” with no line number:
Just when I thought I was out… they pull me back in.
Michael Corleon, Godfather III
Sometimes clever isn’t as good as brute force. In this installment, I’ll present a hybrid approach to the challenge of displaying the Logiker 2022 holiday image.
Instead of writing code to handle each section of the pattern, perhaps taking the simpler approach of just doing Run Length Encoding (sorta) might be smaller. I do not know where I first learned about RLE, but I implemented a simple version in Sub-Etha Software’s graphical “CoCoFEST Simulation” text adventure back in the early 1990s. The images in the graphical adventure were large, and I came up with a simple way to represent repeated data in them. (I do not know if someone gave me this idea, or if I worked it out on my own; in those pre-Internet days, knowledge was alot more organic.)
Basically, RLE looks for repeating sequences and replaces them with a count value and the sequence. The Wikipedia article gives this example:
Consider a screen containing plain black text on a solid white background. There will be many long runs of white pixels in the blank space, and many short runs of black pixels within the text. A hypothetical scan line, with B representing a black pixel and W representing white, might read as follows:
Above, if there are twelve “W” characters in a row, you would encode the value 12 followed by the “W”.
I wrote a simple program that will input a string and then scan it looking for repeating characters. When it finds a new character, it prints the count of the previous characters as well as what character that was.
0 ' RLE.BAS
10 LINE INPUT "STRING:";A$
20 LC=ASC(LEFT$(A$,1)):L=1
30 FOR I=2 TO LEN(A$)
40 C=ASC(MID$(A$,I,1))
50 IF C<>LC THEN PRINT L;LC:L=0:LC=C
60 L=L+1:NEXT
70 PRINT L;LC
I pasted this code in to chat.openai.com and it correctly recognized it as an RLE routine! Here is how it described my program:
This looks like a BASIC program that encodes a string using run-length encoding (RLE). RLE is a simple form of data compression, where runs of repeated characters are replaced by a count and the character. For example, the string “AAABBCCC” would be encoded as “3A2B3C”.
The program starts by prompting the user to input a string, and then assigns the first character of the string to the variable LC (which stands for “last character”). The variable L is set to 1, and then a FOR loop is used to iterate through the remaining characters in the string.
For each character in the string, the program checks if it is different from the last character. If it is, it prints the length of the previous run and the character itself, and then resets the length and last character variables. If the character is the same as the last character, the length is incremented. This continues until the end of the string is reached, at which point the final run is printed.
For example, if the input string is “AAABBCCC”, the output would be:
3 65 2 66 3 67
This indicates that there were three instances of the character “A” (ASCII value 65), two instances of the character “B” (ASCII value 66), and three instances of the character “C” (ASCII value 67).
– chat.openai.com
Wow. Just wow.
Here is an example of the program running:
RLE (Run Length Encoding) in BASIC
Since the Logiker Challenge image only contains spaces and asterisks, we don’t need a full RLE to encode it. I chose to use alternating numbers to indicate how many spaces, then how many asterisks.
Ultimately, I tried to save code space by breaking the image in to sections, and using two main routines to print that section within FOR/NEXT loops.
But … is that overkill? Instead of using multiple PRINT routines, what if I only needed one? By expanding the image data so each line covers the entire width of the CoCo’s 32-column screen, I could do away with the “end of line” markers in the data, and replace them with a larger series of spaces that goes from the end of the data on that line to the start of the data on the next line:
Above, at the end of the first line’s asterisks, there are 12 spaces to the end of that line. For the next line, there are 11 spaces to get to the start of the next asterisks. That means after printing the last asterisks in line 1 we can just print 23 spaces and be at the start of the next line.
Assuming we start with a SPACE then an ASTERISK then a SPACE and do on, the data for the first two lines would look like this:
11 - print11 spaces
1 - print 1 asterisk
7 - print 7 spaces
1 - print 1 asterisk
23 - print 23 spaces (to move to the start of data in the second line)
2 - print 2 asterisks
5 - print 5 spaces
2 - print 2 asterisks
...and so on...
I was going to convert all the PRINT lines of the original version I started with to DATA statements and write a program to count this for me, but that sounded complicated. I just counted, and came up with the following numbers:
DATA 11,1,7,1,23,2,5,2,3,3,23,4,4,4,23,5,16,15,16,17,18,16,5,2,3,3,23,4,4,4,23,5,18,2,5,2,1,1,7,1
But, that takes up alot of room. There is a comma between each number, so for 50 numbers we’d be adding 49 commas, basically doubling the size of the data. Also, two digit numbers like 10 take up two bytes. I thought about using HEX numbers (0-15 turns in to 0-F) but the data has some values that are larger than 15 (the highest value that fits in a single character of a HEX value).
HEX is BASE-16 (0-F to represent 0-15) and what I really need is at least BASE-23 (0-23, the larger number I need). Since there are 26 letters in the alphabet, I could use all of them and get BASE-26 leaving me room to spare!
If A=1, B=2 and so on, the above series of numbers could be turned in to:
K A G A W B E B W C C C W D A D S Q P O R M T K V I V K T M R O P Q S D A D W C C C W B E B W A G A
I could then turn those in to DATA:
DATA K,A,G,A,W,B,E,B,W,C,C,C,W,D,A,D,S,Q,P,O,R,M,T,K,V,I,V,K,T,M,R,O,P,Q,S,D,A,D,W,C,C,C,W,B,E,B,W,A,G,A
…and read them as a string (READ A$) and then convert that string to a number by subtracting 63 (ASCII for A is 64, so if I read an A and get 64, subtracting 63 turns that in to 1):
READ A$
V=ASC(A$)-64
While this saves a byte for every number that was two digits, the extra code to convert from ASCII to a number may be larger than what we saved.
Since we have 49 commas, we could get rid of those and add code to parse a long string. As long as that code is smaller than 49 bytes, we come out ahead.
DATA KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA
Now I could read that as a string and parse it in to numbers:
0 'STRTONUM.BAS
10 READ A$
20 FOR I=1 TO LEN(A$)
30 PRINT ASC(MID$(A$,I,1))-64;
40 NEXT
50 DATA KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA
And, if I want to use that series of numbers in a loop that prints alternating strings of spaces and asterisks, I don’t even need to bother with it being in a DATA statement. I could just embed it directly in the MID$() command and hard code the lengthof the string, like this:
0 'STRTONUM2.BAS
20 FOR I=1 TO 50
30 PRINT ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I,1))-64;
40 NEXT
And if I can do that, the only thing left is to figure out when to print a space and when to print an asterisks.
An easy way to do that is looking at the I variable in the FOR/NEXT loop. As it counts from 1 to 2 to 3 to 4, I can use AND to check bit 1. For odd numbers, that bit is set. For even numbers, it is not.
This means a simple check for “I AND 1” in an IF statement can help me decide which to print. Something like:
IF (I AND 1) THEN PRINT space ELSE PRINT asterisk
That gets me to something like this:
0 ' LOGIKER-ALPHA2.BAS
10 FORI=1TO50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 IF I AND 1 THEN PRINT STRING$(L,32); ELSE PRINT STRING$(L,42);
40 NEXT
Perhaps I can get rid of one of those PRINT STRING$ commands… Since I know a space is ASCII 32 and an asterisk is ASCII 42, I could start with the 32 and add 10 if it’s the asterisk case. To do that, I need to see the result that comes back from AND:
PRINT 1 AND 1
1
PRINT 2 AND 1
0
So if the condition is TRUE (bit 1 is set, meaning the value is odd), I get a 1. If the condition is FALSE (bit 1 is clear, meaning the value is even), I get a 0.
Since I want to print spaces on the odd values, I need to use the 1 (odd) to mean 32, and the 0 (even) to mean 42. I’ll reverse my logic a bit and always start with 42 (asterisks) and multiply it by 10 times the result of (I AND 1). Something like this should work:
0 ' LOGIKER-ALPHA3.BAS
10 FOR I=1 TO 50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 PRINT STRING$(L,42-(I AND 1)*10);
40 NEXT
And that gives me the pattern I want, with far less code. I can remove unneeded spaces and combine everything in to one line and see how big it is.
Unneeded Spaces
A quick thing about unneeded spaces. There are spaces that BASIC itself doesn’t need, but the tokenizer that turns what you type in to the program DO need. For example:
FOR I=100 TO 5000
None of those spaces are needed, because BASIC knows where a keyword ends (FOR) and can tell the variable will be whatever is there before the “=”. The same is true for the numbers, since it can tell where a number ends and know to look for “TO”.
FORI=100TO5000
BUT, if you were using variables in that loop…
FOR I=B TO E
…and you took the spaces out:
FORI=BTOE
…how does BASIC know what your variable is? Is it “B”? Or “BT”? Or maybe “BTOE”? You will get an “?SN ERROR” if you try that because BASIC sees a non-number after the “=” and switches to parsing it as if it were a variable. To get around this, we have to put a space after it like this:
FORI=B TOE
That allows the tokenizer to work fine.
However… If you were manually creating the BASIC program by packing bytes together in a file, you could omit that space and it will run just fine. Utilities such as Carl England’s CRUNCH do this trick to save a byte. BUT, if you were to CRUNCH the program then try to EDIT that line, you’d no longer have code that would run because updating the line requires it to be re-tokenized. #TheMoreYouKnow
Why is that important?
I mention this because in my above program, I wanted to remove spaces from this line:
30 PRINT STRING$(L,42-(I AND 1)*10);
I can remove all but one, since I need a space between “I” and “AND” for the same reason I just mentioned:
30 PRINTSTRING$(L,42-(I AND1)*10);
But… instead of “I AND 1” I could change it to “1 AND I” and get the same result, but no longer need the space because BASIC can tell where a number stops:
30 PRINTSTRING$(L,42-(1ANDI)*10);
And that, my friends, is how you save one more byte.
Would it be possible to also get rid of those parenthesis? Right now, I need to take my asterisk value (42) and subtract either 0 or 10. I need the results of “1 AND I” multiplied by 10, and if I removed the parens…
42-1 AND I*10
…BASIC would do the math first (42-1 and I*10) and if “I” was 3 at the time, I would get this:
42-1 AND 3*10
41 AND 30
…and that’s not at all what we want.
Can it be done? I moved things around but it really looks like the result of “1 AND I” has to be in parens. Can you figure a way to save those two bytes?
With that said, I present this version:
10 FOR I=1 TO 50
20 L=ASC(MID$("KAGAWBEBWCCCWDADSQPORMTKVIVKTMROPQSDADWCCCWBEBWAGA",I))-64
30 PRINT STRING$(L,42-(1ANDI)*10);
40 NEXT
Oh, one thing I should also mention — during last year’s challenge, a comment was made about how ASC() works. If you give it a string, it returns the ASCII value of the first character. So ASC(“A”) returns 64, just like ASC(“ALLEN”) does. They said instead of using MID$(A$,I,1) to get one character, you can leave off that third parameterand MID$ returns the rest of the string:
A$="HELLO"
PRINT MID$(A$,2,1)
C
PRINT MID$(A$,2)
ELLO
If we were trying to print or use just one letter, we need that third parameter. But since I am passing it in to ASC, I could still give it the longer string and it would work fine:
PRINT ASC("E")
69
PRINT ASC("ELLO")
69
Thus, I can leave off that third parameter and save the two bytes that “,1” took up.
The challenge continues. From humble beginnings of using PRINT, to fancier methods of encoding the image as a series of spaces and asterisks, we eventually ended up with an even fancier method that used only 1/4 of the image data to represent the entire symmetrical image.
That approach could work for any image that is symmetrical vertically and horizontally, and typically general purpose routines are not as small as custom routines that know what they will be doing.
Knowing what we now know…
WIth that said, looking at this image, there is another shortcut that I missed:
The entire image is centered over one column… This means the amount of spaces on the left is unimportant — we just need to center the following lines:
I’m not sure what the pattern is as I type this, but I am expecting there is one. Here is a quick program that prints the rows of the shape using FOR/NEXT loops (uncentered):
0 ' LOGIKER13.BAS
10 FOR I=1 TO 4
20 PRINT STRING$(I,"*");STRING$(1+(4-I)*2," ");STRING$(I,"*")
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 PRINT STRING$(I,"*")
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 PRINT STRING$(I,"*")
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 PRINT STRING$(I,"*");STRING$(1+(4-I)*2," ");STRING$(I,"*")
120 NEXT
If each of those lines were centered, we’d have our shape. Let’s try that by creating a string for the row, and then using the LEN() of that string to know how to center it using TAB().
0 ' LOGIKER14.BAS
10 FOR I=1 TO 4
20 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
25 PRINT TAB(16-LEN(A$)/2);A$
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 A$=STRING$(I,"*")
55 PRINT TAB(16-LEN(A$)/2);A$
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 A$=STRING$(I,"*")
85 PRINT TAB(16-LEN(A$)/2);A$
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
115 PRINT TAB(16-LEN(A$)/2);A$
120 NEXT
130 GOTO 130
That produces our desired shape (though it does leave a blank line at the end, which our original version avoided by having a semi-colon on the PRINT and just breaking lines when we went to the next one).
The first thing I see it that the centering code on line 25, 55, 85 and 115 is the same. Subroutine!
0 ' LOGIKER15.BAS
10 FOR I=1 TO 4
20 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
25 GOSUB 150
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 A$=STRING$(I,"*")
55 GOSUB 150
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 A$=STRING$(I,"*")
85 GOSUB 150
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*")
115 GOSUB 150
120 NEXT
130 GOTO 130
150 PRINT TAB(16-LEN(A$)/2);A$:RETURN
Next, we see that the string building code for the top and bottom are the same, so 20 and 110 are the same (it’s the value of I that changes how it prints), and then 50 and 80 are the same. Subroutines!
0 ' LOGIKER16.BAS
10 FOR I=1 TO 4
20 GOSUB 200
25 GOSUB 150
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 GOSUB 250
55 GOSUB 150
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 GOSUB 250
85 GOSUB 150
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 GOSUB 200
115 GOSUB 150
120 NEXT
130 GOTO 130
150 PRINT TAB(16-LEN(A$)/2);A$:RETURN
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):RETURN
250 A$=STRING$(I,"*"):RETURN
Next, I notice the subroutines of 200 and 250 both have the centering PRINT called after them, so maybe we change it up a bit…
0 ' LOGIKER17.BAS
10 FOR I=1 TO 4
20 GOSUB 200
30 NEXT
40 FOR I=17 TO 9 STEP-2
50 GOSUB 250
60 NEXT
70 FOR I=11 TO 17 STEP 2
80 GOSUB 250
90 NEXT
100 FOR I=4 TO 1 STEP-1
110 GOSUB 200
120 NEXT
130 GOTO 130
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN
What else? The FOR/NEXT loops are basically all the same, except for the start and end value and the step value… Maybe we could come up with a way to have only one, and feed it those values using DATA statements?
10 FOR I=1 TO 4
...
40 FOR I=17 TO 9 STEP-2
...
70 FOR I=11 TO 17 STEP 2
...
100 FOR I=4 TO 1 STEP-1
500 DATA 1,4,1
510 DATA 17,9,-2
520 DATA 11,17,2
530 DATA 4,1,-1
If they all went to the same GOSUB routine this would be easy, but they don’t. The go 200, 250, 250, 200. We could add a fourth element in the DATA that tells it which routine to go to and “IF X=1 THEN GOSUB Y ELSE GOSUB Z” or something. That adds more code. Perhaps we don’t need the DATA since we know it alternates? Still, we’d have to track it ourselves with an IF or something. For now, let’s just try this:
0 ' LOGIKER18.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT I
60 NEXT J
70 GOTO 70
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0
And that still produces your original shape. But is it any smaller?
In part 4, we had a version that (using my default XRoar emulator running DISK EXTENDED COLOR BASIC) showed 22499 bytes free after loading. This new version shows 22567 bytes free. So yes, it is smaller! And, we can pack those lines and make it even smaller than that. (And NEXT doesn’t near the variable — in fact, using “NEXT I” is slower than just saying “NEXT” so I’ll remove those here as well.)
0 ' LOGIKER19.BAS
10 FOR J=1 TO 4:READ A,B,C,D:FOR I=A TO B STEP C:IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT:NEXT
70 GOTO 70
200 A$=STRING$(I,"*")+STRING$(1+(4-I)*2," ")+STRING$(I,"*"):GOTO 300
250 A$=STRING$(I,"*")
300 PRINT TAB(16-LEN(A$)/2);A$:RETURN:DATA 1,4,1,0,17,9,-2,1,11,17,2,1,4,1,-1,0
That version shows me 22609 free, which is even smaller — and we could still make this a bit smaller by getting rid of unnecessary spaces in the code.
Side note: I am being lazy and just showing the BASIC “PRINT MEM” values rather than calculating the actual size of the program. On my configuration, 22823 is how much memory is there on startup. So, 22823-22609 shows that this program is 214 bytes. It uses more memory for the strings when running, but I don’t think that matters for this challenge.
What else can we do to save a few bytes? Well, STRING$() takes two parameters. The first is the count of how many times to repeat the second parameter. The second parameter can be a quoted character like “*”, or a number like 42 (the ASCII value of the asterisk). 42 is one by smaller than “*” so we can do that as well as use 32 (the ASCII value for space) instead of ” “:
Another thing we know is that in the shape there are always the same number of spaces before the top and bottom sections, so we really don’t need to center it. We could just hard code a PRINT TAB for that instead of building a string and calling a center subroutine:
The middle section is similar. Since we know the length, we could calculate how many spaces to tab using that number:
250 PRINT TAB(16-I/2);STRING$(I,42)
260 RETURN
And that removes a subroutine, leaving us with this (not line packed yet):
0 ' LOGIKER20.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN GOSUB 200 ELSE GOSUB 250
50 NEXT
60 NEXT
70 GOTO 70
200 PRINT TAB(11);STRING$(I,42);STRING$(1+(4-I)*2,32);STRING$(I,42)
210 RETURN
250 PRINT TAB(16-I/2);STRING$(I,42)
260 RETURN
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0
Two FOR/NEXT loops, a READ, an IF, and two PRINT subroutines.
Maybe we don’t need those subroutines, now that we have an “IF” in line 40 that decides what to do?
0 ' LOGIKER21.BAS
10 FOR J=1 TO 4
20 READ A,B,C,D
30 FOR I=A TO B STEP C
40 IF D=0 THEN PRINT TAB(11);STRING$(I,42);STRING$(1+(4-I)*2,32);STRING$(I,42) ELSE PRINT TAB(16-I/2);STRING$(I,42)
50 NEXT
60 NEXT
70 GOTO 70
500 DATA 1,4,1,0
510 DATA 17,9,-2,1
520 DATA 11,17,2,1
530 DATA 4,1,-1,0
That’s an ugly line 40, but it got rid of two GOSUBs and two RETURNS. Plus every line takes up 5 bytes, so by removing two of those (each subroutine must start on its own line) we saved even more.
Not only is this smaller, it’s faster, too, since it doesn’t have to spend time seeking through the program looking for a subroutine line number for each line it prints.
If we pack this version, removing unnecessary spaces and semicolons and such, and remove the “GOTO” loop (the challenge does not require the screen to be cleared, and says it can return to a prompt after running), we get:
0 ' LOGIKER22.BAS (don't include this line)
1 FORJ=1TO4:READA,B,C,D:FORI=A TOB STEPC:IFD=0THENPRINTTAB(11)STRING$(I,42)STRING$(1+(4-I)*2,32)STRING$(I,42)ELSEPRINTTAB(16-I/2)STRING$(I,42)
2 NEXT:NEXT:DATA1,4,1,0,17,9,-2,1,11,17,2,1,4,1,-1,0
151 bytes of BASIC code!
Is there more that can be done? Comment with your ideas! I think I’m out of them, here.
In the beginning Logiker offered up a programming challenge to produce this simple image:
From its humble beginnings as a bunch of PRINT statements, to a DATA driven output routine, we have now ended up with code that can print the top half of the symmetrical image by only having data to describe one quarter of the image.
When we last left off, I had code that would read this data, print the left half, then print the right half, mirroring the same data. It worked well, but only printed the top half of the image.
I showed that code to OpenAI and asked how it would accomplish the task of reversing the data to print the bottom portion:
To make the program print the rows in reverse order after it has finished printing all of the rows, you will need to make some changes to the code. One way to do this would be to add another loop after the main loop that reads the data from the DATA statements and prints the rows in reverse order. You can use the “REVERSE” statement in BASIC to reverse the order of the rows in the DATA statements, and then use the same code as in the main loop to read the data and print the rows. This will cause the program to print the rows in the original order, and then again in reverse order.
– chat.openai.com
I’ve never heard of the REVERSE keyword, and the CoCo’s BASIC doesn’t have it. But if I did have it, this task would be pretty easy. I informed the A.I. that I had no such command, and it said:
If your BASIC does not have a REVERSE command, you can still print the rows in reverse order by using a different approach. One way to do this would be to store the data from the DATA statements in an array, and then loop through the array in reverse order to print the rows. In BASIC, you can use the “DIM” statement to create an array, and the “READ” statement to read data from the DATA statements into the array. You can then use a loop to iterate through the array in reverse order, using the same code as in the main loop to print the rows. This will cause the program to print the rows in the original order, and then again in reverse order.
– chat.openai.com
That’s the approach I was thinking of taking, so… okay!
Side Note: I did ask the A.I. to modify my program to do this, and it gave me something that would have been a good starting point for the rest of this article. I could have then discussed things with the A.I. a bit more and encouraged it to evolve the program until it met the requirements. (One issue with BASIC is that the A.I. inevitably tries to do things from other dialects of BASIC that Color BASIC does not have, such as the REVERSE command.) I hope to demonstrate one of these co-programming sessions in a future article series, provided access to ChatGPT remains available to me.
I changed my routine so it loaded all the DATA values in to a dimension. Since I knew how many entries there would be (27), I could eliminate the need for having a -1 to mark the end of data.
Then, in the PRINT routine, instead READing A, B and C and looking at them, I’d use the D() dimension variable as D(x), D(x+1) and D(x+2) to get to the same three values.
At the end of each line (three numbers, or two numbers followed by a zero), I would increment my index by three so it starts at the next set of numbers in the array.
I used a new variable, M, to indicate the mode (forward or reverse). When it reaches the end of the data (D goes past the 27 entries), it sets that variable to 1, and other code now runs decrementing the index by 3 each time until it reaches the top (0).
Here is my new code:
0 ' LOGIKER11.BAS
10 CLS:DIM D(26)
11 FOR I=0 TO 26:READ D(I):NEXT:D=0:M=0
15 PRINTTAB(6);
20 PRINT STRING$(D(D),32);
25 IF D(D+2)=0 THEN PRINT STRING$(D(D+1)*2-1,42);STRING$(D(D),32);:GOTO 40
30 PRINT STRING$(D(D+1),42);STRING$(D(D+2)*2-1,32);STRING$(D(D+1),42);
40 IF M=0 THEN D=D+3:IF D<27 THEN PRINT:GOTO 15 ELSE M=1
41 IF D>0 THEN PRINT:D=D-3:GOTO 15
50 DATA 5,1,4
60 DATA 5,2,3
70 DATA 5,3,2
80 DATA 5,4,1
90 DATA 1,9,0
100 DATA 2,8,0
110 DATA 3,7,0
120 DATA 4,6,0
130 DATA 5,5,0
220 GOTO 220
OpenAI describes it as follows:
This program is similar to the previous ones, but it uses a slightly different approach to read the data from the DATA statements and print the rows. The program starts by defining an array D to store the data from the DATA statements, and then reads the data into the array. The program then enters a loop that prints the rows of the pattern. In each iteration of the loop, the program prints a row of the pattern by printing a string of spaces, followed by a string of asterisks, and then another string of spaces. The number of spaces and asterisks in each string is determined by the data in the array.
After the program has printed all of the rows in the original order, it enters another loop that prints the rows in reverse order. In each iteration of the loop, the program prints the same row as in the previous loop, but in reverse order. This causes the program to print the rows of the pattern in the original order, and then again in reverse order. However, without more information about the data in the DATA statements, it is not possible to say exactly what the output of the program would look like.
– chat.openai.com
Nicely done, A.I.
When I run this program, I get the desired output!
It is, of course, slower than the original brute-force PRINT version. But, we are going for size. And speaking of size, lets try to pack this down as much as possible and see where we stand:
0 ' LOGIKER12.BAS
10 CLS:DIM D(26):FOR I=0 TO 26:READ D(I):NEXT:D=0:M=0
15 PRINTTAB(6);STRING$(D(D),32);:IF D(D+2)=0 THEN PRINT STRING$(D(D+1)*2-1,42);STRING$(D(D),32);:GOTO 40
30 PRINT STRING$(D(D+1),42);STRING$(D(D+2)*2-1,32);STRING$(D(D+1),42);
40 IF M=0 THEN D=D+3:IF D<27 THEN PRINT:GOTO 15 ELSE M=1
41 IF D>0 THEN PRINT:D=D-3:GOTO 15
50 GOTO 50:DATA 5,1,4,5,2,3,5,3,2,5,4,1,1,9,0,2,8,0,3,7,0,4,6,0,5,5,0
Loading the first version gives me 22431 bytes free. Loading the second gives me 22499 – about 68 bytes smaller! Compare that to the original brute-force PRINT version (which was 22309 free), we have saved 190 bytes so far.
And, we could remove spaces, get rid of the REM comment at the start, and save even more.
But is that enough for the challenge? One thing the challenge says it you are not required to clear the screen, and you can return to an OK/Ready prompt. That means I could remove the CLS and the GOTO loop at the end, saving even more.
But saving even more is not enough. I think there’s a few more things we can do, especially now that I understand what it takes to draw this shape.
Until next time, take a look at what I have done in LOGIKER11.BAS and see what suggestions you can come up with.
So far, we’ve taken a brute force PRINT program and turned it in to a less-brute force program that did the same thing using DATA statements:
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
All of this in an effort to try to print out this image:
While there are still many BASIC optimizations we could do (removing spaces, combining lines even further, renumbering by 1, etc.), those would apply to any version of the code we create. Instead of doing that, let’s look at some other ways we can represent this data.
When the Atari VCS came out in 1977 (you younguns may only know it as the 2600, but it didn’t get that name until 1982 — five years after its release later), it required clever tricks to make games run in only 1K or 2K of ROM and with just 128 bytes of RAM.
The game Adventure was quite the challenge, since it had multiple screens representing different mazes, castles and areas.
Atari’s Greatest Hits on an old iPad
Each screen was represented by only 21 bytes of ROM! If you follow that link, you can read more about my efforts to understand how this worked. Here is an example of how the castle room was represented:
;Castle Definition
CastleDef:
.byte $F0,$FE,$15 ;XXXXXXXXXXX X X X R R R RRRRRRRRRRR
.byte $30,$03,$1F ;XX XXXXXXX RRRRRRR RR
.byte $30,$03,$FF ;XX XXXXXXXXXXRRRRRRRRRR RR
.byte $30,$00,$FF ;XX XXXXXXXXRRRRRRRR RR
.byte $30,$00,$3F ;XX XXXXXX RRRRRR RR
.byte $30,$00,$00 ;XX RR
.byte $F0,$FF,$0F ;XXXXXXXXXXXXXX RRRRRRRRRRRRRR
There are three bytes to represent each line. Three bytes would only be able to represent 24 pixels (8 bits per byte), and the ASCII art shows the screen width is actually 40. Those three bytes cannot represent the entire row of pixels.
In fact, 4-bits of that isn’t used. Each set of three bytes represents halfa row (20 bits out of the 24 the three bytes represent). Look at the first entry:
.byte $F0,$FE,$15 ;XXXXXXXXXXX X X X R R R RRRRRRRRRRR
If you turn those bytes into binary, you get this pattern:
The Atari drew the first 8-bits from least significant bit to most. the second 8-bits from most significant to least, then the third from least significant to most. That makes it look like this, matching the ASCII art (skipping the unused 4-bits):
000011111111111010101000
XXXXXXXXXXX X X X
To represent a full screen, the Atari had a trick that would mirror or duplicate the other half of the screen. In the case of the castle, the right side was a mirror image. In the case of certain mazes, the data was duplicated.
Looking at our image here, since it is symmetrical, we could certainly use the same trick and only store half of the image.
Since the image is 17×17 (an odd number, so there is a halfway row and column), we’d actually need to just draw to that halfway row/column, then reverse back through the data.
We should be able to take our existing data and crop it down from this, which represents the full image:
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
…to this, which represents the top left quarter-ish of the image:
50 DATA 5,1,4,0 ' X '
60 DATA 5,2,3,0 ' XX '
70 DATA 5,3,2,0 ' XXX '
80 DATA 5,4,1,0 ' XXXX '
90 DATA 1,9,0 ' XXXXXXXXX'
100 DATA 2,8,0 ' XXXXXXXX'
110 DATA 3,7,0 ' XXXXXXX'
120 DATA 4,6,0 ' XXXXXX'
130 DATA 5,5,0 ' XXXXX'
That represents all the data up to the center row/column, and that seems to be a considerable savings in code space (removing eight lines).
But how do we draw that forward, then in reverse? There is no way to back up when using the READ command, so we’d have to remember what we just did. For a general purpose “compress 1-bit image” routine it would be more complex, but since we know the image we are going to produce, we can make an assumption:
The image never has more than three transitions (space, asterisk, space) in a line.
No line entry has more than 4 numbers total.
Knowing that, we could simply save up to three numbers in variables, so we would print them out A B C and then C B A. We won’t even need the zeros now, since we can read A,B,C and act on them (stopping if C is 0).
Neat!
A quick bit of trial and error gave me this code that will print the top half of the image:
0 ' LOGIKER10.BAS
10 CLS
15 CH=32:PRINTTAB(6);
20 READ A:IF A=-1 THEN 220
21 PRINT STRING$(A,32);
22 READ B,C
25 IF C=0 THEN PRINT STRING$(B*2-1,42);STRING$(A,32):GOTO 15
30 PRINT STRING$(B,42);STRING$(C*2-1,32);STRING$(B,42)
40 GOTO 15
50 DATA 5,1,4
60 DATA 5,2,3
70 DATA 5,3,2
80 DATA 5,4,1
90 DATA 1,9,0
100 DATA 2,8,0
110 DATA 3,7,0
120 DATA 4,6,0
130 DATA 5,5,0
215 DATA -1
220 GOTO 220
It creates this:
I can now say “we are halfway there!”
But now I have another issue to solve. How do I back up? There is no way to READ data in reverse. It looks like I’m going to need to load all those numbers in to memory so I can reverse back through them.