Sunday, August 3, 2014

Getting REAL with Retrochallenge

23:59 GMT is staring me in the face, daring me to get my Fortran program done in time. But alas, it wasn't meant to be.

I'm throwing in the towel, with two of nine programs done. As explained in my last post, I'm actually really happy with the time I spent. It taught me a lot and I intend to keep going with programming these platforms. The effort has, though, reminded me of Murphy's Law...and to never enumerate your juvenile poultry until the proper process of incubation has thoroughly materialized.

I was so, so close on the Fortran program. Ironically, the roadblock is a problem I thought I'd solved early on - random numbers.

My PDP-11 is a lot of fun. But it has a shortcoming that really got in my way for Retrochallenge - no Floating Point Processor. The 11/23+ CPU card (KDF11-BA) can have floating point, or not. On mine, it's not. Therefore, to use Fortran, I have to ensure I never, ever use floating point in the program, and I have to use a special library in the LINK phase to even get the programs to run. Fortran really likes floating point, and is somewhat loathe to run without it. I guess that makes sense for a language derived from "Formula Translating System".

The pseudo-random number generators out there for Fortran, including the one that comes with the DEC compiler, also typically want floating point. I ain't got none. So there you have it.

I thought I'd found a good INTEGER based algorithm, but it turned out to be so darned complicated to use that there's no way I'd implement it in time to save the day.

Workflows


So instead, here is my promised post about modern=>retro workflows.

I've learned so much during this RC that it's tough to write it all down. However, here are some workflow highlights from my programming efforts.

I wanted to write my source code on a modern PC because I'm lazy. There, I said it. I like (no, love) modern programmer's text editors. I've always tried to be the first on my block with cool, syntax-highlighting, macro-enabled editors. Brief was one of my favorites of all time.

For this excursion, I chose Jedit. I'm not really a fan of Java client-side applications, but Jedit is an exception. It's clean, feature-filled, and supports Forth and Fortran natively. I used it for both the Atari 800/X-Forth and PDP-11/Fortran projects.

But, how to get the source code to the target platforms? Easy! Well, heh, I probably spent more time figuring this stuff out than it would have taken me to edit on the local platforms. But hey, take time to save time, I always say. Actually, maybe that's the first time I've said that. But I'll always say it from now on.

For Atari 800/X-Forth:

  • I used aspeqt (an open-source Atari peripheral emulator) to simulate disk drives for the Atari, connected via an SIO2PC adapter. In aspeqt, I mounted a Windows folder as a virtual drive. To the Atari, the folder looked like a disk drive, so I could transparently load things from it.
  • Alas, the Atari uses ATASCII, and Jedit naturally edits in ASCII. So when I wanted to make the Forth source file available to the Atari, I had to convert it. For this, I used a cute little utility called Dratex. It's simple, but fast, and works easily.
  • Once the source file was in ATASCII format, I loaded it from the virtual folder (that the Atari saw as drive D2), and voila!

 For PDP-11/Fortran 77:

  • E11 is a most excellent PDP-11 emulator, and is free for personal/hobbyist use. I admit that during most of the Fortran development, I used the emulator. (Keeping a real PDP-11 with dual RL02 drives running for extended periods makes the electric company happy, but makes my pocketbook sad.)
  • E11 can map the main "SLU" console (and, actually, many of the emulated terminal interfaces) to a Telnet session. From there, I connected to my emulated PDP-11 with the Tera Term terminal emulator program.
  • Once code in Jedit was ready for a test, I would open a Tera Term session to E11, and simply paste the code into the terminal window. Tera Term has a configurable delay in between lines when pasting, and this helps the host machine to keep up. Once the source code was pasted, I would save it, compile it, and test!

Thanks as always to the folks running the Retrochallenge for a fun outing. I just love it. I hope to participate every time, till smoking capacitors do us part.

See you next time,

- Earl

Friday, August 1, 2014

Craps program for the Epson PX-8 in BASIC

Without any ado, here's the Retrochallenge 2014 SC craps program that I wrote for the Epson PX-8 in BASIC.

First, a video demonstration:



Now, the source code:

1000 REM Remove menu bar and set text screen mode
1010 SCREEN 0,0,0
1020 CLS
1030 PRINT "Welcome to RC-CRAPS! What is your name: ";
1040 LINE INPUT N$
1050 REM Get random seed from 614.4 KHz clock at Z80 I/O reg 0 and 1
1060 R=INP(1)*256+INP(0)
1070 R=R-32768!
1080 RANDOMIZE R
1090 PRINT "Hello ";N$;"! Would you like instructions? (Y/N): ";
1100 YN$=INPUT$(1)
1110 IF YN$="y" OR YN$="Y" THEN GOSUB 1630
1120 BR%=1000
1130 CLS
1140 PRINT "You have ";
1150 PRINT USING "$#####";BR%;
1160 PRINT " in the bank."
1170 PRINT "Your bet? (1-50 dollars, or Q to quit: ";
1180 LINE INPUT BT$
1190 IF LEFT$(BT$,1) = "q" OR LEFT$(BT$,1) = "Q" THEN 1870
1200 BT% = VAL(BT$)
1210 IF BT%<1 OR BT%>50 THEN 1170
1220 PRINT "(P)ASS/win or (D)ON'T PASS/lose?: ";
1230 PD$=INPUT$(1)
1240 PRINT
1250 IF INSTR("PpDd",PD$)=0 THEN 1220
1260 PRINT
1270 GOSUB 1800
1280 IF (P%=7) OR (P%=11) THEN 1320
1290 IF (P%=2) OR (P%=3) THEN 1370
1300 IF (P%=12) THEN 1420
1310 GOTO 1450
1320 REM win
1330 PRINT "You win!"
1340 IF PD$="p" OR PD$="P" THEN PRINT "You bet you would win. Good job!":BR%=BR%+BT%
1350 IF PD$="d" OR PD$="D" THEN PRINT "You bet against yourself. OOPS!":BR%=BR%-BT%
1360 GOTO 1140
1370 REM lose
1380 PRINT "You lose!"
1390 IF PD$="p" OR PD$="P" THEN PRINT "You bet you would win. OOPS!":BR%=BR%-BT%
1400 IF PD$="d" OR PD$="D" THEN PRINT "You bet against yourself. Good job!":BR%=BR%+BT%
1410 GOTO 1140
1420 REM push
1430 PRINT "You rolled boxcars (12). No win or loss."
1440 GOTO 1140
1450 REM continue point roll
1460 PT%=P%
1470 PRINT "Your point is";PT%
1480 GOSUB 1800
1490 IF P% = 7 THEN 1530
1500 IF PT% = P% THEN 1570
1510 PRINT "Keep playing!"
1520 GOTO 1470
1530 PRINT "You lose"
1540 IF PD$="p" OR PD$="P" THEN PRINT "You bet you would win. OOPS!":BR%=BR%-BT%
1550 IF PD$="d" OR PD$="D" THEN PRINT "You bet against yourself. Good job!":BR%=BR%+BT%
1560 GOTO 1140
1570 PRINT "You win"
1580 IF PD$="p" OR PD$="P" THEN PRINT "You bet you would win. Good job!":BR%=BR%+BT%
1590 IF PD$="d" OR PD$="D" THEN PRINT "You bet against yourself. OOPS!":BR%=BR%-BT%
1600 GOTO 1140
1610 GOTO 1140
1620 END
1630 CLS
1640 PRINT "RC-CRAPS rules:"
1650 PRINT "1. The bank gives you an initial bankroll. Use it wisely!"
1660 PRINT "2. Make a bet on PASS (win) or DON'T PASS (loss). Max bet is $50."
1670 PRINT "3. Make an initial roll of the dice:"
1680 PRINT "   7 or 11 wins. 2 or 3 loses. 12 is a 'do over' (no win or loss)"
1690 PRINT
1700 PRINT "Press any key for next page...";
1710 NT$=INPUT$(1)
1720 CLS
1730 PRINT "4. Any other roll sets your 'point'. Rolling this again is your goal."
1740 PRINT "5. Roll again until you roll your point for a win, or 7 for a loss."
1750 PRINT "6. Good luck! Remember, what happens in RC-CRAPS, stays in RC-CRAPS!"
1760 PRINT
1770 PRINT "Press any key to continue...";
1780 NT$=INPUT$(1)
1790 RETURN
1800 REM Two dice at random
1810 PRINT "Press any key to roll the dice!"
1820 NT$=INPUT$(1):PRINT
1830 D1%=INT(RND(1)*6)+1:D2%=INT(RND(1)*6)+1
1840 P%=D1%+D2%
1850 PRINT "You rolled a";D1%;"and a";D2%;"for a";P%;"!"
1860 RETURN
1870 CLS
1880 PRINT "Thanks for playing RC-CRAPS!"
1890 PRINT "You left with $";BR%;"in the bank!"
1900 END

Travails and Triumphs

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.

TL;DR Version



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:

10 PRINT
12 PRINT "Oops, I left this line out"
15 PRINT "And this one, too!"
20 GOTO 12

and makes it look like this:


10 PRINT
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."
1010 RETURN
9999 END

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.

The EXPECT 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.

The TYPE 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
        DROP 1
    ELSE
        DUP C@ EMIT 1 + 0 THEN
    UNTIL
    ;


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:

        PROGRAM CLSCR
        BYTE CLS(5)
        BYTE HOME(4)
        DATA CLS/27,'[','2','J',128/
        DATA HOME/27,'[','H',128/
        CALL PRINT(CLS)
        CALL PRINT(HOME)
        END


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...