Hac-Man is a clone and a recreation of a game I originally wrote on my high school’s minicomputer 30 years ago. I’m delighted that I was able to get back into retrocomputing, considering I had to throw out almost all of my old computer and electronics history.
Though I’m very much future-focused (and sometimes depressed for it), I always loved reading old technical books and magazines and I love it more so when I can run old software and old operating systems like DEC RSTS/E. I am not done with my retro explorations—I have a whole file share on my home server with hundreds of megabytes of nothing but emulators and system images, most of which I haven’t even had time to yet touch. (It was this system that hosted the RSTS/E instance from which I developed the game, since I could work on it at my desk and at my laptop during lunch breaks.)
Hac-Man the game is completely finished now; I’ve thought about making it available to download as a complete system, but it’s hard to do as there are very few terminal emulators that will work for Hac-Man, other than TeraTerm. Kermit 95 is another possibility for a terminal emulator—it has recently been open-sourced—but from reading about the code, it sounds like quite a fixer-upper that I know I won’t have the time for.
I’d like to recreate my game in QuickBasic. QB64 seems to be a very ingenious, well-developed reinvention of this classic BASIC interpreter and compiler. I could create a very nice looking, if retro, Hac-Man, and this is what I’ll probably do if I ever have time.
The final tally: Hac-Man is contained in two RSTS/E BASIC-PLUS programs, each of them weighing in at 20K and 400 lines each, and I spent roughly 50 hours in July working on the project.
Here’s the final, final video:
This’ll be my last retro entry for a little while, as I am forced back to the present by SATV and Microsoft.
I’m a bit too exhausted to write the long post-mortem I had planned for the project. I got enough of it done to show, but not enough to be a complete game; the real world won’t let me take any more time on this project.
But it’s been a lot of fun. RSTS/E is as linked to BASIC as Unix is to C. Considering the limitations of BASIC, it’s amazing that so many of the system utilities were written in that language. RSTS will never be a good host for arcade games, but we made it do in 1981.
I’d like to give a hat tip to my old classmates in the Salem High computer lab: Randy Moisan, Chris Haight, John Orlowski, Dave Irish, Peter Georgelas, and our department head Thomas Risoldi. Thanks for the start in this crazy business.
There are just a few details left to explain. When I first thought of recreating my old BASIC game, I’d planned to have some bells and whistles like a high-score screen and an attract mode like in a real arcade game.
Very late in development, I ran out of time and memory to include all this in one executable, and I wasn’t sure if I would have a complete game for the contest.
Fortunately, fate went in the other direction; Hac-Man will have a front end with animation, a help screen and high scores. It will also have the “halftime show” that was in the original game—and in my original clone.
RSTS/E BASIC, like most BASICS, has a mechanism for starting other programs from within a program: The CHAIN statement.
23050 IF NOT DEBUG% THEN CHAIN "HACMAN.BAC" 9000
This code, if I’m not debugging will call HACMAN.BAC and start it at line 9000. The .BAC extension indicates a compiled program; CHAIN will fall back to the uncompiled program if the compiled program isn’t found, so it’s recommended practice to always specify .BAC.
(The DEBUG% variable is not part of RSTS/E BASIC, but is my own convention I’ve used for debugging in all my code.)
OK, but how do we pass information from one program to another. This line of code is run in the Hac-Man game engine when the player loses the game. The program should then pass the score back to the front end so it can be included in the high score list if possible.
RSTS/E has a feature it calls “core common”. This is a 128-byte area that the operating system keeps for each job, and it is shared amongst all the processes in each job. The RSTS/E command processor uses core common to pass parameters to system programs from the command line.
23010 GM$="HACMAN"+CVTF$(SCORE)+SPACE$(12-LEN(CVTF$(0)))+CVT%$(LEV%)+CVT%$(PLAY%) 23020 XX$=SYS(CHR$(8%))
We construct a string to pass back to our front end. The first characters of the string are “HACMAN”, our magic signature that the other program uses to verify that it’s valid.
Next, the function CVTF$(SCORE) adds the score as a binary string. Some explanation: In RSTS/E, BASIC can use either 2-word floating point or 4-word floating point values, determined at system generation. I generated my own RSTS/E system but I don’t remember which option I selected. (And in the era of emulation, there’s no saying I could not generate a system with 4-word FP.)
The string fragment SPACES$(12-LEN(CVTF$(0 ))) accounts for this possibility; it pads the string with spaces for the next two values.
CVT%$(LEV%) and CVT%(PLAY%) convert these integers to binary; integers are always 2 bytes so this is straightforward.
Here is the corresponding code on the front end:
8500 GM$=SYS(CHR$(7%)) ! Get string from core common 8509 !Validate string 8510 IF LEFT(GM$,6)<>"HACMAN" GOTO 1000 ! If no score passed, resume attract loop 8520 SCORE=CVT$F(MID(GM$,7,LEN(CVTF$(0)))) 8530 LEV%=CVT$%(MID(GM$,12,2))
This code is called from the Hac-Man game engine at game over. Line 8500 retrieves the string from core common. Line 8510 checks for a “HACMAN” signature that indicates a valid code. Line 8520 retrieves the score, and 8530 retrieves the ending level.
One last aside, this on memory: I had forgotten how little memory I had on RSTS. The memory in the BASIC interpreter, and the memory in my TECO screen editor more or less tracked together. The biggest program I could get in both the editor, and BASIC was about 19,000 bytes, just less than 20K.
That included white space. It isn’t free here, and that will surprise a lot of modern developers.
Several times, I had to back out of programming changes and refactor my code. Hac-Man likely represents the most complex programming in RSTS/E. In 1981, I was complimented on my work by a DEC field service engineer (“Wow, is that Pac-Man!?”) who told us that was the most ingenious program he had ever seen.
It is as good as I can possibly get it.
Next: Closing words, and a video.
After a month at recreating this old game, I have the AI kinda-sorta working. Here’s how it’s supposed to work.
First, I must explain that the original game had no AI. All we learned in those days about AI was in two lines of BASIC that were in virtually every book on BASIC, including the seminal work 101 BASIC Computer Games. There were many such books up through the mid-80s.
10 Z=INT(RND(0)*10000) 20 D=SQR((X2-X1)*2+(Y2-Y1)*2)
Line 10 computes a pseudo-random number (which will be the same number every time it runs unless I include a RANDOMIZE statement). Line 20 is a very familiar Euclidean distance formula.
That was it. That was our AI in almost all of the computer games I and my classmates wrote in those days, and indeed in all of the innumerable collections of BASIC games published in print.
I wanted to do better this time around. My strategy was inspired by a discussion of the AI of a certain popular game. I couldn’t copy it as is, but I did take to heart its important lesson: One can assemble simple behaviors and simple code to make our ghosts have reasonable smarts.
All of the ghosts share AI code; each ghost has target coordinates that it prefers to go to. It will take a direction to these coordinates first, if it can. If not, it will continue in the same direction it’s heading, in hopes of finding a cross-corridor.
If the ghost finds itself blocked, it will then try moving to its left, and then to its right. Here’s a diagram of how it works for Ink, the “fast” ghost that is most like the red ghost in the original game.
The other ghosts have different strategies: Blink tries to move in front of the player. Pink tries to block the player from behind, and, Sal, the fourth ghost, prefers to hide in a corner.
When the player eats a power pill, the ghosts only have one strategy—to get away from the player as fast as possible.
In testing, I found the ghosts to move very fast in relation to the player, so I added a counter to slow the ghosts down. Currently, the ghosts move one cycle for every five cycles through the game loop.
Here’s the annotated code:
12198 !Ghost movement routine 12200 FOR I%=GHOST% TO MAXGH% 12201 !Apply a delay count before moving ghost; skip moving if delay timer not expired 12210 SV%(I%)=SV%(I%)-1\IF SV%(I%)>0 THEN 13480 ELSE SV%(I%)=VEL% 12220 IF NOT FNGHB%(SPX%(I%),SPY%(I%)) THEN 12300
Line 12200 starts a FOR loop to iterate through the four ghosts. Line 12210 applies the delay counter. Line 12220 calls a function, FNGHB%, that detects if the ghost is in its box.
12229 !If in ghost box, move ghost up through door 12230 IF SSTAT%(I%) THEN 13400 !If in box and eatable, do not move! 12240 SDR%(I%)=2\GOTO 13350 !End ghost box exit AI
If the ghost is in its box, it does one of two things: If it is eatable, the ghost stays exactly where it is and does not try to leave the box. If it’s normal, it will move up through the exit door (which I’ve made wide to simplify the AI). SDR% is an array with direction data for all the moving sprites in the game, including ghosts.
12300 IF NOT SSTAT%(I%) THEN 12330 !Skip if not eatable 12301 !Same strategy for eatable mode for all ghosts 12302 !Preferred: Move away from Hac-Man 12310 GP%=REV%(FNDIR%(I%,SPX%(0),SPY%(0))) 12320 GOTO 13200
If the ghost is roaming the maze and it is eatable, its only strategy is to get away. Line 12310 gets the reverse of the current direction of Hac-Man.
12323 !Per-ghost target calculations 12330 ON (I%-GHOST%)+1 GOSUB 12400,12500,12600,12900 12340 IF DEBUG% THEN XX$=FNSP$(SPDEAD%,PX%,PY%) !Indicate target square in debug 12350 GP%=FNDIR%(I%,PX%,PY%)
Otherwise, the target square for each ghost is calculated. Line 12340 is some debugging code to display the target square on the playfield. Line 12350 calculates the direction to the target square.
12398 !Ink: Head directly for Hac-Man 12400 PX%=SPX%(0)\PY%=SPY%(0%)\RETURN 12498 !Blink: Head in front of Hac-Man 12500 PX%=SPX%(0)+DX%(SDR%(0))*2\PY%=SPY%(0)+DY%(SDR%(0))*2\RETURN 12598 !Pink: Goes behind Hac-Man 12600 PX%=DX%(SDR%(0))+DX%(SDR%(7))\PY%=DY%(SDR%(0))+DY%(SDR%(7))\RETURN 12895 !Sal: Go for Hac-Man if far, hide when close 12900 SD=FNDIST(I%,0)\IF SD>8 THEN PX%=SPX%(0)\PY%=SPY%(0)\RETURN 12920 PX%=70%\PY%=5\RETURN !Sal's corner is in upper right
Each ghost’s target square is calculated here. This code was much shorter than I thought it would be. Line 12400 handles Ink’s AI. Blink is handled at line 12500. Pink is at line 12600. Sal is slightly more complicated: If he’s far away (greater than 8 characters) from Hac-Man, he’ll head towards him, but when he gets closer, he’ll run and hide to the upper right corner.
13195 !Select direction: Priority is Preferred (calculated), straight-ahead, sprite left, sprite right 13200 DIM G%(3) 13210 G%(0)=GP%\G%(1)=SDR%(I%)\G%(2)=FNRLD%(SDR%(I%),-1)\G%(3)=FNRLD%(SDR%(I%),1) 13220 IX%=0 13230 GD%=FNOBJ%(G%(IX%),SPX%(I%),SPY%(I%)) !Check each direction if clear 13240 IF GD%=4 OR GD%=5 THEN XX$=FNTUN$(I%,GD%)\GOTO 13460 13250 IF GD%<=2 THEN SDR%(I%)=G%(IX%)\GOTO 13350 13260 IF IX%<=2 THEN IX%=IX%+1\GOTO 13230 13270 IF DEBUG% THEN PRINT FNNC$;"NO DIRECTION FOUND-G,GD,IX,I";\MAT PRINT G%\PRINT IX%;I%\stop 13350 XX$=FNRD$(SPX%(I%),SPY%(I%)) 13440 SPX%(I%)=SPX%(I%)+DX%(SDR%(I%))\SPY%(I%)=SPY%(I%)+DY%(SDR%(I%)) 13450 IF NOT SSTAT%(I%) THEN IS%=I% ELSE IS%=9 ! Draw ghost sprite or eatable sprite 13460 XX$=FNSP$(IS%,SPX%(I%),SPY%(I%)) 13480 NEXT I% !End AI routine
To make it easier on me, I used another array, G%, to hold the possible directions a ghost would take. Again, the ghost moves in this order: 1) preferred direction, 2) current direction or straight ahead, 3) left turn, 4) right turn.
Lines 13220 through 13260 form the direction determining loop. Line 13230 call the background object detection function FNOBJ%. Line 13240 has special-case code and calls another special function FNTUN$ for the tunnel, which the ghosts will take. If FNOBJ% returns 0 (empty space), 1 (dot) or 2 (pill), the direction is good and the ghost will take that, in line 13250.
Otherwise, the code tries the next direction from G%. Line 13270 is debugging code that triggers if the four choices in G% are exhausted. (There are no blind alleys in the maze. There should not be.)
Once the ghost has a direction the rest is straightforward. Line 13350 erases the ghost in its old location. Line 13440 updates the ghost’s coordinates. Line 13450 accounts for the ghost’s normal sprite or eatable sprite and line 13460 draws the sprite. Finally, line 13480 ends the main FOR loop for the ghost AI.
With that, the major work behind Hac-Man is finished.
I only have a few more posts left before the end. I’ll cover the last technical details in the next post. My last post will have a video, my closing thoughts, and a dedication to close out the July Retrochallenge.
For the past month I’ve been working on a Pac-Man clone that I originally wrote back in 1981. I lost the source, so I’ve had to recreate everything as I’ve gone. But one technique hasn’t changed: When I had the idea for the game, I already learned how to get our terminals to move the cursor about the screen and draw characters on them, but one problem stumped me for a long time in 1981: How do I get my game code to know what’s on the screen?
How can I have a game with spaceships, enemy saucers, and ghosts on the screen, recognizing each other’s existence and interacting with the player? From the time I first saw Pac-Man in an arcade, I wanted to replicate it. But how?
Back then (and even to this day, for the VT-100), there was no command, no escape sequence, that would take in row/column coordinates and return the ASCII value of the character painted on the screen on that location. Nothing at all.
I’m not sure how I came up with the solution, but I thought of something that my peers considered elegant at the time, a technique that’s just the way I remember it back then, the one thing in my program that still dates to 1981.
Look at this code snippet:
2895 ! *** Gamefield string definition *** 2950 DIM GFL$(22%) ! GFL$ = Gamefield drawn on screen 2960 DIM GFL%(80%,24%) ! GFL%(x,y) = Gamefield object detection 3000 GFL$(1) = "lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqk" 3010 GFL$(2) = "x O........ .............. ............. ............ ..... ...... .......O x" 3020 GFL$(3) = "x x" 3030 GFL$(4) = "x .lqqqqqu. tqqqqqqqqqqqu. tqqqqqqqqqqu. tqqqqqqqqqqu. tqqqu. tqqu. tqqqqqk. x" 3040 GFL$(5) = "x .x ...... .............. ........ .... .... ........ ...... ..... ..... x. x" 3050 GFL$(6) = "x .x x. x" ...
You’ve seen the GFL$ string array before; it contains the playfield. Note the GFL% 80×24 integer array. More code:
10115 !Initialize background maze collision detect array 10120 MAXDOT%=0% \ FOR IY%=2% TO 23% \ FOR IX%=1% TO 79% 10130 GFL%(IX%,IY%)=ASCII(MID(GFL$(IY%-1%),IX%,1%)) 10140 IF (GFL%(IX%,IY%)=DOT% OR GFL%(IX%,IY%)=PILL%) THEN MAXDOT%=MAXDOT%+1 10141 !Count up total dots including 4 power pills 10150 NEXT IX%\NEXT IY% 10200 !Draw maze and score display 10210 PRINT FNCL$+FNLD$ ! Clear screen and alt character set 10220 GOSUB 19010\GOSUB 19050 ! Print score and level 10250 PRINT FNHM$;! Home cursor 10260 PRINT\FOR I%=1% TO 22%\PRINT GFL$(I%)\NEXT I%
At this point, we go through the playfield string array one line at a time, and we fill the GFL% integer array with the ASCII value of each character making up the playfield. Along the way, we count the dots and the pills to get a maximum count that the game uses to determine when the level is complete. (That makes it easier to change the maze during development.)
LInes 10210 through 10260 actually draw the maze on the screen. But the real logic is in the GFL% array—this array has the information that Hac-Man and the ghosts will use to navigate as they twist, turn and eat their way through the game.
This was the most important idea I had in 1981, and the one that made the game possible.
In this era’s revision of my program, I put a lot more thought and a lot more logic into a great many tables and data structures that govern how the actors move in the maze. (That’s how last Friday’s blog update turned into this Friday’s…)
I wanted to make the logic as general as I could, which in BASIC is something of an oxymoron, but I think I did much better than I did in 1981, Here’s how my object detection routines work:
There are four, and only four directions for any sprite to take, starting from leftward motion—direction 1—going clockwise, up (2), right (3) and down (4), and they are numbered and indexed in various tables and arrays, beginning at the input detection logic and continuing all the way to the ghost AI routines.
(On the screen, you may notice a debugging message at the top saying “X,Y,Dir”. The coordinates are right but the direction is wrong: I changed the direction numbering scheme after I took the screenshot—the old scheme I first used would have made the ghost AI routines nearly impossible to write.)
The master list of sprite coordinates—SPX%, SPY%, SDR%—contain location and current direction for the player and the ghosts (and the inanimate, unmoving fruits.) There are also arrays for starting location and direction.
There is a object detection array CX% and CY%, illustrated by the green highlights. The function FNOBJ% will return a value given the current position of a sprite, and the direction it is moving in. This is used by the player sprite to detect dots and pills.
And there is a movement array DX% and DY% which returns the proper direction XY vector given the numerical direction. There’s even a reverse direction array that the program uses in certain circumstances, such as when a pill is eaten and the ghosts reverse direction.
Wall detection logic is simple: FNOBJ% checks the two coordinates in the direction the sprite is moving. If either of the two coordinates is not a space, a dot or a pill, it is considered a wall. This made it possible for me to make the maze look nicer with VT-100 drawing characters that I didn’t have in the original.
There is special-case code for the tunnel that connects the left and right sides of the maze.
The background object detection array is altered in one circumstance: When Hac-Man eats dots, or one of the pills, the corresponding object is erased from the array and is replaced by the ASCII code for space, 32, which the game treats as empty, movable space. So, the array saves the game state during play until the level is complete.
The background object array also has one important purpose I forgot to mention in my first draft: Ghosts ignore dots and pills, but as they move through the maze, those dots and pills are the background that has to be preserved and redrawn. (In 1981, multiplane sprite graphics were something I had only heard of. They were also in machines I could never afford.)
Checking for collisions between sprites, between Hac-Man and a ghost, or a fruit, is much simpler; the original game, and my remake, uses a Euclidean distance formula of the sort taught in basic high school geometry. If Hac-Man and a ghost meet, their mutual distance will be zero and the ghost had better be edible. Either way, the code to handle that is under the sprite collision section.
Next up, the ghost AI. The most important part of the game. The part that makes Hac-Man, Hac-Man. We’re nearly there.
This is a delayed update, but I finally have first motion in my game. I’ve been recreating a Pac-Man clone I first wrote as a high-school student for a text-mode terminal on our school’s PDP-11/60 running RSTS/E.
I’ll provide another update with more technical details, but this is great progress. The only major parts of the game I need to finish are the ghost AI routines and the sprite collision routines; the latter routines are almost trivial, so that leaves the AI. This will either be doable—or not—and I am thinking it’s doable, so I expect that when the Retrochallenge ends for July, I will have a working game!
In my game, presently, I can move the Hac-Man character. It eats dots and pills, and the ghosts, while not moving, do change appearance, and change back. Hac-Man can use the tunnel. Points add up and the game logic correctly recognizes each level’s completeness. I only need to add a fruit—my original game had two different fruits. The levels top out at 255, in a homage to the original game (though it does not crash as the original game did.)
I deviated from history just a bit in the design of the maze: The original game worked on a VT-52, and an ADDS terminal (the model number of which I do not remember and which is not on Bitsavers either). Neither of them had line-drawing characters, only blocks (of a sort) that, as one commenter put it, made the game look more like the old Unix text classic Dungeon, rather than the casual maze of Pac-Man, and my clone.
Salem High just missed the VT-100 by that much, having gotten our system circa 1976; the VT-100 was already available by the time I wrote my game in 1980. I think its new appearance is worth any historical infidelity it may cause, since my program logic hasn’t changed any. If there is still a PDP-11 running RSTS/E somewhere, it can run my game as long as it talks to a VT-100 or compatible.
Technical details will follow in my next post, and expect more frequent updates as I have more time to work on the game and explain even more of the details.
Wakka wakka wakka!
An important part—one of the important parts of a video game is the input processing. Whether the player controls his or her persona, character or avatar with a mouse, joystick, keyboard or game controller, there has to be a way for our game code to take feedback from the outside world and use it to affect the world in the game. It also helps if there’s some way to stop the game if the player’s about to miss the bus, or take a refreshment break.
RSTS/E is normally configured for line-based, line-delimited, input, such as one would see in a command shell like Bash, Windows CMD or Powershell. It’s not normally able to read characters on the fly.
Many early microcomputer implementations of BASIC did eventually include some means of character-based inputs; some of you may be familiar with the INKEY$ variable that was part of most versions of Tandy BASIC across their product line.
RSTS/E turns out to have such a capability, but it’s not real easy to use. RSTS/E needed to have character-based input to support certain system software, notably ODT, an all-purpose machine-language debugger and binary editor.
To access this capability from within a BASIC program, one must open the local keyboard as a file, specifying some special modes. I’ve written a short test program that reads input as required by the game, which I’ll explain presently, line by line:
100 EXTEND 490 ES$ = CHR$(27) ! Escape character 500 LA$ = ES$+"[D" ! VT100 Left arrow 502 RA$ = ES$+"[C" ! VT100 Right arrow 504 UA$ = ES$+"[A" ! VT100 Up arrow 506 DA$ = ES$+"[B" ! VT100 Down arrow 508 ! Quit, Help and Restart keys 518 ! Combine arrow keys in one search string. & ! Note the filler characters at the start of the string & ! All arrow key sequences are three characters long, including the escape char. 520 ARKY$ = CHR$(0)+CHR$(0)+CHR$(0)+LA$+RA$+DA$+UA$ 528 ! Game command keys are also combined in one search string. 530 CMKY$ = "QHR "
First, the preliminaries; we have to define the characters we are checking for. The player is on a VT-100 terminal, so we must define its arrow key sequences in lines 500-506. For brevity, we reserve a string variable ES$ for the escape character.
There are also other keys we need to define. “Q” is obvious, it quits the game. “R” restarts. The space bar is important. In a real game with a joystick, the player can freeze his or her ship or character on the screen by releasing the controller; with a keyboard, it’s a bit different—the player hits an arrow key to move, and the player sprite keeps moving, until he or she hits the space bar.
There is another key “H” for a high score display but that’s not relevant at the moment.
There are two character search strings, which I will explain as we go along.
Next, we open the keyboard for input:
540 ! Open the keyboard for input 1000 OPEN "KB:" FOR INPUT AS FILE #1 1010 X% = SPEC%(7,0,1,2%) ! Disable type-ahead 1020 X% = SPEC%(0,0,1,2%) ! Cancel Ctrl-O 1030 X% = SPEC%(3,0,1,2%) ! Disable echo 1050 FIELD #1%, 128% AS B$ 1060 LSET B$ = "" ! Flush buffer 1100 ON ERROR GOTO 2000 1110 PRINT "Waiting for input..."
With this idiomatic sequence of code, we open the keyboard for input on channel #1. Microsoft BASIC virtually copied much of the syntax and semantics of file access from DEC BASIC, so if you’ve used a Tandy, you might have seen some code like this.
Lines 1010 through 1030 set up some special conditions we need to take input. We disable type-ahead, the well known phenomenon we see in online web forms, Facebook and Twitter where people type whole messages to a blank form box, only to see their message pop up already sent with misspellings, usually including phrases like “StopQuitABORTControl-CDammit$$$$!%” When we start the game we want no stray input. Line 1030 is self-explanatory; it keeps the system from echoing arrow keys and other input, which would destroy the display. Line 1020 disables a certain control character that could also disrupt the game.
RSTS/E requires us to process our input with record-based input in lines 1050 and 1060. To make a long story short, our text is going to go into the buffer B$. Line 1060 flushes the buffer before we begin. Line 1100 sets up error handling for the next section of the code.
1150 ! Start character input loop 1155 X%=SPEC%(4,0,1,2%) ! Set ODT Mode 1160 GET #1%, RECORD 8192%
This is where we actually start reading characters. Line 1155 sets the special character input mode. Line 1160 gets characters from the terminal. RECORD 8192% tells RSTS/E not to wait for input. If there is no input, an error is thrown, which we handle later on.
1170 I$ = LEFT(B$,RECOUNT) 1175 IF I$<>ES$ THEN GOTO 1400 ! If it's not an escape sequence, we are done getting chars. 1180 X% = SPEC%(4%,0%,1%,2%) ! Set ODT mode to get remaining escape sequence 1185 GET #1%, RECORD 8192% ! And get the rest of the sequence 1190 I$ = I$+LEFT(B$,RECOUNT) 1200 I2% = INSTR(1%,ARKY$,I$)/3 1210 PRINT "I2%=";I2%;", I$=";I$;", Count=";RECOUNT
We have our input, but we’re not done. Some of our characters are escape sequences, and others are normal characters. Line 1170 retrieves the character from the buffer. If it’s an escape sequence, we need to get the rest of the characters in the sequence. This is done in lines 1185-1190. Line 1200 detects the arrow key characters and returns a value in I2%. RECOUNT refers to the number of characters left in the buffer.
1220 IF I2%<>0% THEN ON I2% GOSUB 1320, 1330, 1340, 1350 1250 LSET B$ = "" 1290 GOTO 1150 1320 PRINT "** Left arrow" \ RETURN 1330 PRINT "** Right arrow" \ RETURN 1340 PRINT "** Up arrow" \ RETURN 1350 PRINT "** Down arrow" \ RETURN
In line 1220, if I2% is not zero, meaning we have valid input, we do an ON…GOSUB on that index. This familiar BASIC construct is somewhat like a CASE statement, though it really should be called an indexed function dispatch. Each of the lines in 1320-1350 print a message at the terminal indicating the arrow key that was pressed. In a game, this will be replaced by specific handling code. Line 1250 clears the buffer and line 1290 goes back to the beginning of the input loop.
Just a few more lines left:
1399 ! 1 char codes 1400 I1% = INSTR(1%,CMKY$,CVT$$(I$,32%)) ! Convert input to uppercase before checking 1405 PRINT "I1%=";I1% 1410 IF I1%<>0% THEN ON I1% GOSUB 1430,1440,1450,1460 1415 LSET B$ = "" 1420 GOTO 1150 1430 PRINT "** Quit key" \ RETURN 1440 PRINT "** High Score" \ RETURN 1450 PRINT "** Restart" \ RETURN 1460 PRINT "** (space)" \ RETURN 1470 RETURN 2000 IF ERR=13 AND ERL=1160 THEN RESUME 1150 2010 PRINT "ERR=",ERR,"ERL",ERL 2030 X% = SPEC%(2%,0%,1%,2%) ! Reenable echo 2040 STOP 9999 END
Lines 1400 through 1470 perform the same processing for one-character non-escape inputs. The CVT$$ function in line 1400 converts the incoming characters to upper case, so that a player’s input to quit the game will work whether he or she enters “q” or “Q”. Note line 2000. The GET #1, RECORD 8192% statement that performs the input will error out if there is no input, which will certainly happen through the course of the game as the game loop is (or should be) faster than the player. Line 2000 checks for the specific error (error 13, equivalent to a end-of-file error on most platforms) at the specific line. If it comes from a normal no input condition, it resumes at the top of the input loop. Otherwise, it prints an error message, re-enables echo on the terminal and ends the program.
This is all we need for input. Next post will cover more graphics, including how I handled object detection. Soon after, I can actually start moving sprites on the screen—I have no more excuses, no more exposition!
As mentioned in my last post, there are a lot of old, interesting and scary idioms that RSTS/E programmers had to employ if they were to write any kind of game on the system. Of course, the term “graphics” has to be stretched quite a lot with a text-mode VT100, but when you’re in 1981 and can’t afford an Atari, one does what one must.
The screenshot above shows some of the graphic handler routines in Hac-Man. You’ll note this looks somewhat like BASIC, only with some extensions. (This describes virtually every dialect of BASIC with “Extended” in the title…) RSTS/E BASIC has what is known as EXTEND mode. This allows for longer variable names and multiline statements and comments.
I’m using the DEF FN functionality of BASIC to wrap some code that displays VT100 escape sequences to the user’s terminal. FNAD$ is a function that performs direct cursor addressing. It is not obvious from my snippet of code, but RSTS/E is a 7-bit ASCII system and one had to do some tricks to get the escape code to pass to the terminal without the system intercepting it. Earlier on, there is a line in my code that defines the escape character (which would normally be decimal 27).
910 ESCH$=CHR$(155) ! Default escape character
Code 155 is simply code 27 with the high bit set. RSTS ignores it and it is echoed back to the terminal.
To make this game work, we also need to turn on the alternate character set. That’s in the function FNLD$. Here’s what the program listing looks like normally:
The GFL$ array holds our playfield, which I will describe in a later post. Doesn’t look like anything.
Here is the listing in special character mode:
The playfield is being murdered by line wrap, but you can see blocks and dots there.
An aside on editing: RSTS/E had an older revision of TECO, but with screen editor capabilities via the TECO program VTEDIT. (EDT also had a screen editor, but back in the day I much preferred VTEDIT; also, today, EDT’s screen mode doesn’t seem to work in Tera Term.) Such an editor was a virtual necessity when developing in any language on the 11/60. This playfield was first composed in TECO as a plain text file, and then copied, and edited again to add code around the text.
The next few posts will explain more about my graphics and my input logic. Then, I’ll be almost ready to move Hac-Man and his ghosts around the screen!
I have been accepted into the July retrochallenge!
As mentioned previously, I am writing, or more accurately, recreating a program I wrote in high-school. It’s a clone of a very popular game involving a yellow wedge that eats pellets and avoids ghosts. Because I don’t want to hear from Namco, this game will be called “Hac-Man”. Yes, that’s the right spelling. There are a number of other games with “hack” in the title that I wanted to avoid confusion with.
I’m not writing this from scratch in a month; I had worked on this a few years ago when I got a RSTS/E emulation running, but put it aside. I got far enough to do much of the software design—such as it is. (I’ll explain shortly.)
Our PDP-11/60 at Salem High School looked something like this system except the lower facepanel was blue rather than brown. We ran RSTS/E V6C to, if I recall, five video terminals (a mix of ADDS terminals, VT50s and VT52s) and two hardcopy terminals (DECWriter II’s), not counting the system console. We had one line printer and one card reader (plus a Univac card-punch terminal), two RK06 disk drives (14 MB each) and no tape.
I configured my PDP-11/60 in SIMH in much the same configuration, except I gave my system RL drives, which store a bit more information, and a magtape to take advantage of the many PDP-11 repositories with tape images. I couldn’t find RSTS V6C, but V7 is available and matches the old system well enough for my purposes. (It, by the way, is patched for Y2K.)
The terminal of choice is a VT-100, DEC’s most popular model and one that is very widely emulated over the net even today; my emulation is through Tera Term and a telnet connection to an instance of SIMH that runs on my server.
RSTS/E was built around BASIC. Even for system programming—this is no joke! Hac-Man will have to be in BASIC too. For one that knows contemporary dev practices, this is really something else. Even Visual Basic 6.0 does not expect you to use line numbers anymore! (I had thought about writing Hac-Man in assembler, as RSTS/E has full API support for MACRO-11, the PDP-11 assembly code, but time does not permit this at all.)
My next post will have to go into the various idioms and conventions I had to use in my day to perform tasks in game development, display and input that were already supported as a matter of course in the microcomputers of the time, such as the Tandy CoCo I would use a few years later.
I’d written before about the PDP-11/60 I used in high school and the programs I wrote for it. I was a video gamer all the way through, even then. I am best remembered amongst my classmates for the “port” of Pac-Man, which was then and still now a very popular game; I was inordinately proud of the way I somehow got sprites and collision detection on a 80×24 text terminal over a 4800 baud serial line.
I lost the source code, but a few summers ago, I got RSTS/E running on an instance of SIMH and managed to replicate the original experience of high school computer life as much as possible. I even started to design a new version of Pac-Man (“Hac-Man”, for legal reasons) but SATV work picked back up again and I had to put it aside.
It would be a good item for a contest. Stay tuned.