I started this year's summer Retrochallenge with a simple goal - develop three games (craps, roulette, and 21) for three different computer/language combos:
- PDP-11 with Fortran 77
- Epson PX-8 with BASIC
- Atari 800 with Forth
The logic of the games is not tough, especially if you simplify it a bit for computer play. However, I bumped into enough issues that in the end, I will likely only have 1 game (craps) for each of the platforms.
On the face of it, this might seem like a disappointment, but I'm actually pretty pleased. This Retrochallenge got me programming again, revived my love for an old friend (Fortran), and taught me more things about Forth, a language and programming paradigm which I'm really coming to respect.
We've been given a couple of extra days to finish our RC entries, so I'll probably have the Fortran game ready by then. For now, I wanted to share some things I've learned (or re-learned) along the way about writing code for my target platforms.
I'm covering quite a bit of ground below. If you're tight on time, here is the summary:
- PX-8: It's tough in BASIC to manage a big program, mostly due to line numbers and the lack of named subroutines. Also, an 8-line display on your target platform makes this even more challenging.
- Atari/X-Forth: Lack of complete documentation and mediocre text I/O meant I had to do some "roll your own" development.
- PDP-11/Fortran 77: Printing control characters for terminal control (e.g., clearing the screen) turned out to be tough to figure out. Solved this by creating a BYTE array, putting the control codes into the array with a DATA statement, then calling an OS subroutine for raw output.
Below - the gory details...
If it's BASIC, why is it hard
First up, BASIC on the PX-8.
Programming in BASIC is often a shoot from the hip exercise. The interpretive, interactive environment lends itself to this. But the freedom isn't without cost.
For instance - line numbers are, quite frankly, a drag. I'm pretty sure the PX-8 BASIC is a Microsoft derivative (it has that style to it), so it's a serviceable language, thankfully with a RENUMBER
statement. You missed some code and need to insert it? No problem, go for it (assuming you left space - didn't you?), and then use RENUMBER to smooth things out again. For the uninitiated, the RENUMBER command takes a BASIC program with erratic line numbers like this:
12 PRINT "Oops, I left this line out"
15 PRINT "And this one, too!"
20 GOTO 12
and makes it look like this:
20 PRINT "Oops, I left this line out"
30 PRINT "And this one, too!"
40 GOTO 20
Notice that along with renumbering the program to even spaces of 10, it also fixes things like the GOTO statement, putting the new correct line number into the code.
This is all well and good, but it can wreak havoc on your code if you've set aside chunks of line numbers for subroutines. For example:
10 PRINT "I'll call a subroutine at line 1000".
20 GOSUB 1000
30 PRINT "I'm back from the subroutine."
40 GOTO 9999 : REM END
1000 PRINT "I'm in the subroutine at line 1000."
If I RENUMBER this program, the subroutine's nice line number separation from the rest of the program will be lost.
If I could call a subroutine by name or label instead of by line number, then everything would be awesome! But I can't. So as a BASIC program gets bigger, it's harder and harder to manage.
To make things even more spicy, there is only an 8 line screen on the PX-8. You can't see many line numbers all at once, making it more difficult to get the big picture of your program.
Getting past these issues can be tough. Here are some possible solutions:
- Plan out your subroutines in advance. Set aside line number chunks for them. Make sure you have plenty of spacing so you don't wander into your subroutines from your main code. Don't RENUMBER until the very end, or at least, until you're tired of knowing where your subroutines are :-)
- Use ranges in the LIST command to help see small chunks of the code at one time without it scrolling up the screen.
- Keep a notebook beside you (or a Notepad window) to scribble some notes on where things are in your program, what variables you use for what purpose, etc.)
Bringing Forth The Text
The Atari 800 has several varieties of the Forth
environment. For Retrochallenge, after some experimentation, I chose X-Forth
, for three reasons:
- X-Forth uses standard ASCII (or in this case, ATASCII) files for source code, rather than relying on the more primitive and frustrating "screen" concept of older Forths. This makes it possible to edit your Forth words in a nice text editor, then load them into the Forth environment.
- Also, X-Forth is sort of a hybrid between the older figForth and the more modern ANSI Forth. While not all of the newer standard is supported, there is enough there to make life more pleasant.
- It's GPL licensed.
Right off the bat, the first challenge with X-Forth was documentation. The web page notes "more detailed tutorial to come!", but for now, there's an amount of hunt and peck required to figure things out.
This hit me first when trying to figure out acquiring and processing text (e.g., the user's name). Older Forth systems seem a bit lacking when it comes to text I/O and character string manipulation. Compared to BASIC's INPUT, LINE INPUT, MID$, CHR$, etc., the offerings in Forth are pretty pedestrian. That's the bad news. The good news is, much of Forth is pretty close to the metal, and the location of things in memory is quite visible to the programmer. Don't have a word you need? Just write it! And that's what I did.
word in Forth prompts the user for input. You provide EXPECT with a maximum input length and memory location, and EXPECT puts the user input into that memory area, followed by one or more null (0) bytes. Let's say you type HELLO [return]. In memory, at the location you specified, will go 6 bytes - the ASCII for H E L L O, and a zero byte. In X-Forth's EXPECT word, there is no way to figure out how many characters the user typed. This turns out to be a problem, as we'll see.
word in Forth prints text of a specified length from a specified memory address. If EXPECT is like INPUT, then TYPE is sort of like PRINT. However, TYPE doesn't know about the null byte at the end - it just outputs the length of text you give it. This means if you set aside 25 characters for the user's name, and the user only typed 7 characters in the EXPECT statement (above), then TYPE will output not just the name, but also the rest of the 25 characters, most of which will be garbage.
Note that EXPECT guarantees a zero byte at the end of the input. So, I wrote a PRINT word in Forth, which simply starts at the provided memory address and prints the bytes one at a time until it encounters the zero terminator. Here's the source code:
: PRINT ( addr -- )
( Prints chars starting at )
( addr until reaches null )
( Better than TYPE, which )
( outputs the nulls and )
( other junk in the string )
( area. )
BEGIN DUP C@ 0= IF
DUP C@ EMIT 1 + 0 THEN
If in the future I want to use Forth for some more advanced programming, it would benefit me to write some words (perhaps in assembly) to do more robust I/O, conversion, and substring processing.
Also, Forth heavily uses the "stack", a LIFO
area of memory for storing intermediate values as your program executes. To be true to the language, I used the stack as much as possible, rather than punting and using defined variables (which Forth supports). To make sure the "stack effect" of my words was as intended, I would open a Notepad and run through the code, updating a pretend stack as I went. Doing this bench test exercise revealed subtle bugs in my code and was quite enlightening.
CLEARing up Fortran
For the PDP-11 version of craps, I'm assuming the user has a VT100/ANSI-compatible terminal. (Oh no! Don't make your code platform-dependent!) So, I wanted to print out some control codes to clear the screen, home the cursor, etc. This, too, was a challenge.
First, Fortran 77 has a function to convert an integer to a character: CHAR(int). However, that function isn't supported in PDP-11 Fortran 77. Bummer! How do I get a non-printable value into a character string so I can print it?
Answer: I don't. The easiest way was to populate a BYTE array with the correct codes, then call a SYSLIB function from Fortran to write the values. Here's some sample source code:
Note the use of the DATA statement - this takes values and crams them into variables. It gave me a way to get non-printable values (like ASCII 27 for ESC) into a BYTE array.
Also, note the CALL PRINT statements. PRINT is a function in the system library that takes an address as an argument, then prints characters from that address forward until it reaches a 0 or 128 byte. A zero termination will result in a CR/LF being printed following the characters, where a 128 will result in no line termination (this is what I wanted).
Interestingly, the PRINT subroutine is very similar to the operation of the PRINT word I created for X-Forth!
That's all that I'll bore you with for now. Suffice it to say, I'm having a blast. It's very likely I'll keep going with these projects once the Retrochallenge is over - it will be fun to leverage the knowledge I've accumulated.
I have two more blog posts to come - one on the workflows I set up to program on a PC and move the code to the target platforms, and finally, a blog post (hopefully) on the final craps game in Fortran. Stay tuned...