by Rosen & Maguire
Last issue we talked about why we like Forth, and why you might (or might not) enjoy it also. The hardest single task when first using Forth is just getting oriented. Questions like, "How do I start to attack a problem?"; "How is memory organized, and should I care?" and "How do I interface to the joysticks?" are common for novice programmers in any language. Moreover, since many new Forth users have first learned Basic, their "instincts" about how things are done can make learning Forth harder for them than for someone who has never programmed. In this issue, then, we'll talk not only about the code itself, but about the thoughts behind it.
Have you ever drawn pictures on the Atari Memo Pad using the control characters? Memo Pad appears when you turn on the Atari with no Basic cartridge.
Try it. Randomly type several lines of characters using only CTRL-F and CTRL-G; throw in some occasional CTRL-T's and spaces. What you get looks like a maze made of diagonal paths (the CTRL-F's and G's) surrounding some balls (the CTRL-T's) and some open spaces. This has possibilities. Bring up your version of Forth and we'll automate our doodling.
There are several ways to get a character, say "*," onto the screen. First, we can send it by saying ." *" which sends an asterisk to the next cursor position. This is similar to a Basic Print statement. Or we can type 42 EMIT, which is the ASCII code for an asterisk. Both of these methods go through the Atari ROM operating system, which on the 400/800 is rather slow. So instead we'll POKE directly into video memory to bypass the Operating System. But what do we POKE, and where?
In normal graphics modes the memory location 88 contains the address of the upper left corner of video memory. We'll send all the characters in the ROM set to the screen in code order and pick out the ones we want by hand. Define a word, call it Showme, as follows:
DO I 88 @ I + C!
Now position the cursor roughly halfway down the screen so it won't interfere with the display. Type Showme and the characters appear. Showme is a Do Loop that starts at 0 and ends at 255. For each numeral between 0 and 255 the program stores the Loop index I into the next video memory location. Showme computes the next memory location by adding I to the address at 88, and the characters are displayed starting at the upper left of the display.
For our maze we want the blank character, the two slanted lines, and the ball. You'll find the blank in the upper left corner of the display. Its screen code is zero. The two slants are toward the right end of the second line. Their codes are 70 and 71. The ball is 84. We'll put these codes in a reference table.
We can construct a single-purpose lookup table for just these codes, but instead we'll define a more general word called Byte-Lookup, which you can use for making byte lookup tables for any purpose. Since a Byte-Lookup employs Forth's Complex Builds Does command, we'll skip the explanation here. (If you'd like such an article, please write the editor!)
<BUILDS DOES> + C@;
0 C, 70 C, 71 C, 84 C,
0 C, 0 C, 0 C, 0 C,
70 C, 70 C, 70 C, 70 C,
71 C, 71 C, 71 C, 71 C,
In the above, we define Byte-Lookup and then use it to define our table called Board-Figs. "Board Figures has 16 one-byte entries. The first four entries are the necessary screen codes. We'll add four blanks, and four extra of each of the slants as well.
Board-Figs, or any other word defined using Byte-Lookup, will take one stack argument and return the value of the corresponding element in its table. You can put any number of elements into the table as long as you use the letter C, just as we have here.
Next, let's write a word that will select an element from Board-Figs at random. This word will substitute for the random typing we did earlier in Memo Pad.
53770 C@ 15 AND BOARD-FIGS
Pretty simple. Pick-BF, for "pick a board figure," delivers a random byte to the stack from the location 53770, which is a hardware random-byte generator. Pick-BF then ANDs this number with 15 to pick off the low four bits, creating a number from 0 to 15. The resulting number is used as a random index to the Board-Figs table which returns one of our characters. Now, we're ready to make our first game board.
88 @ CONSTANT TOPLEFT
TOPLEFT 960 + BOTRIGHT
DO PICK-BF I C!
We define the constant Topleft because it's faster and takes less space than repeatedly saying 88@. Botright is defined for similar reasons. (Botright actually points one byte past the end of video memory. This is more convenient.) Make-Board starts at the top left corner of memory and fills the entire screen with our four characters. The characters appear in proportion to their presence in the Board Figs table. You can test the screen by typing Make-Board. The "OK" prompt that appears in the middle of the display wouldn't be there if you were running the program.
Your board should look interesting. Roads go every which way, and balls or dots are tucked in here and there.
The Wumpus Game
Let's make the screen into a kind of solitaire maze-capture game, in which a joystick-controlled Wumpus runs along the lines and collects balls. We need some rules. Let's say that if Wumpus is over a diagonal, then he can only move in the two directions pointed to by the diagonal. But if Wumpus is on a blank it can move up, down, left, right, but not diagonally. If you land Wumpus on a ball, you score a point. The ball will disappear, and then, since Wumpus is sitting on a blank, he can move again up, down, left or right.
What about edges? How about connecting the edges so that if Wumpus goes off the left or right, he reappears on the other side of the screen? Let's do the same with the top and bottom.
Let's put a one-minute time-limit on a round of play. After that the score will be displayed and the player will be asked if he or she wants to play again.
We've tested the game on Valforth, APX Forth, and QS Forth. You Fig-Forth owners should be able to edit it onto screens, load it, and run it as well. To run the game, enter the name of the last word, DOT-MAZE.
Since most 400/800 Forths don't allow you to interface with a joystick we'll examine that first. You can use a number of approaches to the Stick construction. Stick should take one argument, which is the number of the stick you want to read, and return two arguments, x and y:
X should be second on the stack or the "side-to-side" displacement being read from the stick. Leave -1 for left, 0 for center and 1 for right. At the top of stack should be the y, or "up-and-down" displacement, with -1 for up, 0 for center and 1 for down. The reason that "up" is negative is that moving an image upward on the screen involves working from higher memory into lower memory.
The 400/800 joysticks are read into locations 632, 633, 634 and 635. Only the low four bits of each location have meaning, one for each direction. A bit is 0 if its corresponding switch is closed, and 1 if it is open. With some testing on a real stick, we find that the word Stick works properly when defined as on screen 13 in the listing.
Now, about Wumpus. Let's keep him simple. His location in the maze will be tracked using inverse video. To invert the video set the high bit of whatever square he happens to be over. That way, you can still see which way the map under Wumpus is pointing. We'll hold Wumpus's position, its address in video memory, in a variable called WUMPUS.
How do we move this inverse video image through video memory without letting it run rampant through the rest of the computer? Look at screen 14, at the definition of MOVE-WUMPUS.
The variable DY contains the up/down value read from the joystick by another word, GET-STICK. DX contains the left/right value. Since there are 40 characters to a line, we multiply the DY value by 40, and then add the DX value to find the displacement. Because display memory is continuous, we can get away with this: As we go off the right side of the screen, we'll reappear on the left side on the next row down; if we go off the left edge, we come back a row higher on the right side.
But what if we go off the top or bottom? Then we'd be in forbidden memory. So, after we add 40 times DY and DX to WUMPUS, we check to see if we've gotten to Botright or higher. If so, we back up 960 characters. This puts us somewhere near the top left. Then, we check to see if we've gone lower in memory than TOPLEFT, and, if so, we move forward the 960 bytes instead, placing us near the bottom of the screen.
Finally, we want to move the screen image. When the program reaches the Endif on screen 14, line 9, the new address of Wumpus is on the stack. We ignore it for the moment, while WUMPUS @ 128 TOGGLE inverses the image of the old position by toggling its high bit. This returns the address to normal video. Line 10, DUP 128 TOGGLE, makes an extra copy of that new Wumpus address, inversing the video image there. Lastly, we store the remaining copy of the new Wumpus address into WUMPUS, moving him on screen.
Next issue, we'll talk more about the code. Until then, try your hand at the maze. Duration changes the game's length and the value of Stick-Delay changes the stick response by Stick-Delay. By changing any two of the zeros in the Board-Figs table to a 71 and a 72, you can make the maze much more difficult to thread.
Screen Listings For WUMPUS In Forth
Screen: 10 0 ( CONSTANTS AND VARIABLES ) 1 2 88 @ CONSTANT TOPLEFT 3 TOPLEFT 960 + CONSTANT BOTRIGHT 4 5 15 VARIABLE DURATION 6 25 VARIABLE STICK-DELAY 7 8 0 VARIABLE STICK-COUNT 9 0 VARIABLE HIGH-SCORE 10 0 VARIABLE POINTS 11 0 VARIABLE DX 12 0 VARIABLE DY 13 0 VARIABLE WUMPUS 14 : 2DUP OVER OVER ; 15 : NOT 0= ; --> Screen: 11 0 ( BYTE-LOOKUP BOARD-FIGS ) 1 2 : BYTE-LOOKUP 3 (BUILDS DOES) + C@ ; 4 5 BYTE-LOOKUP BOARD-FIGS 6 00 C, 70 C, 71 C, 84 C, 7 00 C, 00 C, 00 C, 00 C, 8 70 C, 70 C, 70 C, 70 C, 9 71 C, 71 C, 71 C, 71 C, 10 11 12 13 14 15 --> Screen: 12 0 ( PICK-BF MAKE-BOARD ) 1 2 : PICK-BF 3 53770 C@ 15 AND BOARD-FIGS 4 5 : MAKE-BOARD 6 BOTRIGHT TOPLEFT 7 DO PICK-BF I C! 8 LOOP ; 9 10 11 12 13 14 15 --> Screen: 13 0 ( STICK ) 1 2 : STICK ( N -- L/R U/D ) 3 632 + C@ )R R 8 AND 0= 4 IF 1 ( RIGHT ) 5 ELSE R 4 AND 0= 6 IF -1 ( LEFT ) 7 ELSE 0 a ENDIF 9 ENDIF R 1 AND 0= 10 IF -1 ( UP ) 11 ELSE R 2 AND 0= 12 IF 1 ( DOWN ) 13 ELSE 0 14 ENDIF 15 ENDIF R) DROP ; --> Screen: 14 0 ( MOVE-WUMPUS DOT-CHECK ) 1 2 MOVE-WUMPUS ( -- ) 3 DY @ 40 * DX @ + 4 WUMPuS @ + DUP 5 BOTRIGHT U( NOT 6 IF 960 - 7 ENDIF DUP TOPLEFT U( a IF 960 + 9 ENDIF WUMPUS @ 128 TOGGLE 10 DUP 128 TOGGLE WUMPUS ! ; 11 12 : DOT-CHECK ( -- ) 13 WUMPUS @ C@ 212 = ( 84 + 128 ) 14 IF 128 WUMPUS @ C! 1 POINTS +1! 15 ENDIF ; --> Screen: 15 0 ( MOVE-OK? ) 1 2 : MOVE-OK? ( --F ) 3 DX @ DY @ AND 4 IF DX @ DY @ = 5 WUMPUS @ C@ 199 = OVER AND 6 WUMPUS @ C@ 198 = ROT NOT AND 7 OR 8 ELSE WUMPUS @ C@ 128 = 9 ENDIF ; 10 11 12 13 14 15 --> Screen: 16 0 ( GET-STICK ) 1 2 : GET-STICK ( -- ) 3 0 STICK 2DUP DY ! DX ! 4 2DUP OR ROT ROT AND NOT AND 5 WUMPUS @ C@ 128 = AND 6 IF STICK-COUNT @ 0= NOT 7 IF -1 STICK-COUNT +! 8 0 DX ! 0 DY ! 9 ELSE STICK-DELAY @ 10 STICK-COUNT ! 11 ENDIF 12 ENDIF ; 13 14 15 -> Screen: 17 0 ( PICK-ONE INITIALIZE ) 1 2 : PICK-ONE ( N -- N ) 3 53770 C@ 256 * 4 53770 C@ + 5 U* SWAP DROP ; 6 7 : INITIALIZE ( -- ) 8 0 STICK-COUNT ! 9 0 POINTS ! 10 960 PICK-ONE 11 TOPLEFT + DUP 12 WUMPUS ! 128 TOGGLE ! 13 14 --> 15 Screen: 18 0 ( SHOW-SCORES FINISHED? ) 1 2 : SHOW-SCORES ( -- ) 3 HIGH-SCORE @ POINTS @ 4 MAX HIGH-SCORE ! 5 125 EMIT CR CR CR CR 6 3 SPACES 7 . " YOUR SCORE: " 8 POINTS @ 4 . R 9 CR CR 3 SPACES 10 . " HIGH SCORE: " 11 H-SCORE @ 4 . R CR CR ; 12 13 : FINISHED? ( -- F ) 14 3 SPACES ." TRY AGAIN? (Y/N) " 15 KEY 223 AND 89 = NOT ; --> Screen: 19 0 ( DOT-MAZE ) 1 2 : DOT-MAZE ( -- ) 3 0 HIGH-SCORE ! 4 BEGIN MAKE-BOARD INITIALIZE 5 DURATION @ 0 6 DO 1000 0 7 DO DOT-CHECK 8 GET-STICK MOVE-OK? 9 IF MOVE-WUMPUS ENDIF 10 764 C@ 255 = NOT 11 IF LEAVE ENDIF 12 LOOP 13 LOOP SHOW-SCORES 14 FINISHED? 15 UNTIL 125 EMIT ;
Evan Rosen & Steve Maquire are the co-creators of Valforth and will be writing monthly for Hi-Res. Their cohort, Dr. Quatro, will field any Forth questions.