Forth: Bouncing some ideas around (#3)
In this short tutorial we'll start with something really simple: We'll get a bouncing ball moving around on the screen.
As we develop our words, we'll test them as we go, rather than run the whole program in one go only to find that it doesn't work and wonder where the bugs could possibly be.
When we've got the ball moving around, we'll add a bat that we can use to hit the ball. Think old style breakout.
We're going to restrict ourself to characters in 32 column mode. We'll look at sprites in a future lesson; I'll just say this: Sprites are in some respects simpler than characters when it comes to moving them around, because you don't need to erase them, so there's great difficulty coming down the line regarding sprites. If your curiosity cannot be contained however, have a look at the sprite tutorial here:
So, here's a program that bounces a ball (a zero character, actually, at the moment I'm into a rather odd "less is more" period when it comes to graphics, and am particularly fascinated with ASCII graphics; check out some of those old ZX81 games and you'll see what I mean) around the screen, inside a frame, just to keep things tidy.
First in TI BASIC so that we have something as a reference:
10 CALL CLEAR 20 REM DRAW FRAME AROUND SCREEN 30 CALL HCHAR(1,2,ASC("-"),30) 40 CALL HCHAR(24,2,ASC("-"),30) 50 CALL VCHAR(2,1,ASC("|"),22) 60 CALL VCHAR(2,32,ASC("|"),22) 70 CALL HCHAR(1,1,ASC("+")) 80 CALL HCHAR(1,32,ASC("+")) 90 CALL HCHAR(24,1,ASC("+")) 100 CALL HCHAR(24,32,ASC("+")) 110 REM DEFINE BALL VARIABLES 120 BALL_COL=2+INT(RND*29) 130 BALL_ROW=2+INT(RND*21) 140 XDIR=1 150 YDIR=1 160 REM ERASE BALL 170 CALL HCHAR(BALL_ROW,BALL_COL,32) 180 REM CALCULATE NEW BALL POSITION 190 BALL_COL=BALL_COL+XDIR 200 BALL_ROW=BALL_ROW+YDIR 210 CALL HCHAR(BALL_ROW,BALL_COL,ASC("0")) 220 REM CHECK FOR EDGE OF SCREEN 230 IF (BALL_COL<3)+(BALL_COL>30)THEN 260 240 IF (BALL_ROW<3)+(BALL_ROW>22)THEN 290 250 GOTO 170 260 REM REVERSE X DIRECTION 270 XDIR=-XDIR 280 GOTO 170 290 REM REVERSE Y DIRECTION 300 YDIR=-YDIR 310 GOTO 170
We'll now look at how we could re-create this program in Forth.
Note how I have separated the code above into distinct sections (using blank lines here for clarity). We'll divide (or "factor") the Forth version into pretty much the same sections, testing them as we go.
Drawing the Frame Around The Screen
Okay, in the program above, we need to clear the screen and then draw some lines and characters. Note that I've used the ASC function to make the code a little more "self-describing" who wants to waste time looking up ASCII codes, right?
In Forth, there's an extra step, as we first need to tell the system to go into 32 column mode (TurboForth and fbForth default to 40 or 80 column text modes).
The (TurboForth) command to change graphics modes is GMODE.
- Mode 0=40 column text mode
- 1=32 column graphics mode
- 2=80 column text mode
So, lets create a word called FRAME which sets up the screen and draws the frame.
: FRAME ( -- ) \ set up screen and draw frame 1 GMODE \ 32 column text mode 0 1 ASCII - 30 HCHAR 23 1 ASCII - 30 HCHAR 1 0 ASCII | 22 VCHAR 1 31 ASCII | 22 VCHAR 0 0 ASCII + 1 HCHAR 0 31 ASCII + 1 HCHAR 23 0 ASCII + 1 HCHAR 23 31 ASCII + 1 HCHAR ;
Things to note in the above code:
- : FRAME - this part says "hey, here's a new word called FRAME;
- ( -- ) this is comment that tells us that word has no effect on the stack - it takes nothing from the stack and puts nothing on the stack;
- The \ (backslash) is a comment. When TurboForth see this is treats eveything following it as a comment;
- Screen coordinates are zero-based, whereas in BASIC they are one-based
- Read the code from left to right, top to bottom. You can have multiple instructions on the same line;
- The "arguments" to the function/word (in this case HCHAR and VCHAR) come BEFORE the word itself. Why? Because the words/functions take them from the stack, so they need to be on the stack BEFORE the word itself executes;
- The command ASCII looks at the character in front of it and pushes the appropriate ASCII value to the stack. So ASCII * is like ASC("*") in TI BASIC;
- Spaces are ESSENTIAL in Forth. One or more spaces MUST separate words and numbers from each other. Forth cannot recognise 99STARS if you really mean 99 STARS;
- The semi colon at the end says "Okay, I'm done defining my FRAME word, thank you very much, add it to the dictionary" (where all the Forth words and their code are stored).
At this point, you've added a new word to the language called FRAME. Now that it exists, any other word that you create can use it.
So how do we use it? We "name" it. I.e. we type its name. Forth will do the rest.
- If we type FRAME on the command line (i.e. when the cursor is sitting there blinking at you) then it will immediately execute it. This is the same as typing something in TI BASIC without a line number in front of it. TI BASIC will just execute it (or try to);
- If we type FRAME inside another definition, then it will be compiled for execution later. This is the same as putting a line number in front of something in TI BASIC. TI BASIC will store it and run it later.
So, assuming you have typed the above in (the best way is to use Classic99 and just copy the text above and paste it into TurboForth) you can just type FRAME (or frame - TurboForth is not case sensitive by default (you can turn it off)) and press enter and the code will run.
So, hopefully, you have a neat frame around the screen and TurboForth is saying OK at you and furiously flashing its cursor at you.
We can now move on to the next bit. Note that TurboForth is still in 32 column mode. You can leave it in 32 column mode if you like, but you might be more comfortable in 40 column mode on a real TI, or, if you have an F18A or you are using classic99, 80 column mode.
Type either 0 GMODE and press enter, or 2 GMODE and press enter. The screen will clear and the mode will be changed.
Let's move on. We've tested FRAME, it does just one thing and has no variant behaviour, so it either works or it doesn't.
The Value of VALUEs
The next step of the program sets up the initial ball and row and column variables, let's remind ourselves:
110 REM DEFINE BALL VARIABLES 120 BALL_COL=2+INT(RND*29) 130 BALL_ROW=2+INT(RND*21) 140 XDIR=1 150 YDIR=1
In TI BASIC, you can go ahead and just use a variable name. Forth is not like that. In Forth everything must be created first and stored in the dictionary, then you can reference it. So, we first need to declare our variables. However, in this tutorial, as it's very early days, I'm going to suggest that we use VALUES as they work more like variables in BASIC (i.e. they work with values; whereas variables in Forth work with addresses - that's for another day!)
1 VALUE BALL_COL 1 VALUE BALL_ROW 1 VALUE XDIR 1 VALUE YDIR
So, this bit of code creates some VALUEs called BALL_COL, BALL_ROW, XDIR and YDIR respectively. The number is the initial value. Here's how 0 VALUE BALL_COL is evaluated:
- 0 is pushed onto the stack;
- The word VALUE (a built-in TurboForth word) executes. VALUE is programmed to *read ahead* of itself and use the word it finds there as the *name* of the value to create, so in this case, it creates a VALUE in the dictionary called BALL_COL.
- The initial value (0) that is on the stack is removed, and is used to initialise BALL_COL. If we had typed 99 VALUE BALL_COL then it would be initialised with 99.
- Declaration of VALUES/VARIABLES should NOT be done inside a definition. They should be by themselves. Some words are special in Forth and can only be used on the command line, just like (for example) NEW in TI BASIC, which can only be used on the command line.
So, that's the declaration of the VALUEs done. You can test them by simply typing their name and pressing enter:
Note that TurboForth responds with OK:1 meaning that 1 number is on the stack. What happened? You just "executed" a VALUE. Sounds strange doesn't it? I mean, you can't execute, say, variables in BASIC. What does it mean to execute a VALUE or a variable?
Well, in the case of VALUEs in Forth, they are pre-programmed (once you have created them) to push their current value to the stack when you execute them. It's as simple as that. If you're used to OOP, think of VALUE as a class, and BALL_COL as an instance of class VALUE which took 0 as the constructor.
Now, type YDIR <enter>
TurboForth says OK:2 meaning that 2 numbers are on the stack.
Let's look at them. There are two ways:
- The word .S will display what's on the stack, and leave them there for us (non-destructive)
- The word . (a dot/period) will take what's on the top of the stack and display it, removing it from the stack as it does so.
Type .S <enter>
TurboForth should display:
1 1 <--TOP OK:2
So, it's told you that 1 (YDIR) is on the top of the stack, and 1 (BALL_COL) is underneath it.
Now, type XDIR .S <enter>
TurboForth says 1 1 1 <--TOP OK:3
As an experiment, let's change the value of XDIR. Type:
-1 TO XDIR
See? That wasn't so hard was it? Pretty simple. Let's display the stack again:
1 1 1 <--TOP OK:3
Hang on. We changed XDIR to -1. Why does it still show 1 on the top of the stack? Surely it should display -1, right? No. The stack is a separate entity all by itself. It simply stores what you push on it. If you then change the value of something, good for you. You won't see it until you push it again.
So, type XDIR again. TurboForth says OK:4
Now type .S and we get 1 1 1 -1 <--TOP OK:4
Okay, let's continue, but before we do, how do we clear those four numbers off the stack? There's a number of ways:
- type DROP DROP DROP DROP to drop (discard) them;
- type . . . . (four dots, separated by spaces) to display them (they get removed as they are displayed);
- Or, my favourite: type some random gibberish which causes TurboForth to empty its stack and display an error message:
ERROR: NOT FOUND OK:0
See? 0 items on the stack!
Right, now we need to initialise random starting row and columns for BALL_ROW and BALL_COL. So, let's make a word called SET_RC ("set row and column") that does just that:
Type in or paste in the following:
: SET_RC ( -- ) \ set row and column 30 RND 1+ TO BALL_COL 22 RND 1+ TO BALL_ROW ;
That's it. How does this work? Well, this is a new word called SET_RC so when it is executed it will:
- Push 30 to the stack;
- Call RND which takes the 30 off the stack and uses it to generate a random number between 0 and 29. We need a random number between 1 and 30, so we call 1+ ("one plus") which simply adds 1 to the number on top of the stack. The phrase "TO BALL_COL" removes whatever is on the top of the stack and stores it in our VALUE which is called BALL_COL. The exact same technique applies for BALL_ROW, but using a different random number range.
Let's test it. Make sure the stack is clear by typing some gibberish. Now type:
TurboForth says OK:0 - nothing was pushed to the stack. Let's see what was stored in BALL_COL and BALL_ROW:
Type BALL_COL . BALL_ROW . <enter> (note the spaces between the dots)
TurboForth will respond will respond with something like 13 9 OK:0, depending on what random numbers it chose.
Let's look again, but this time using .S to show us the stack:
BALL_COL BALL_ROW .S
13 9 <--TOP OK:2
This time, we executed the values directly, so they were pushed to the stack. Note how it's possible to put multiple commands on the same line separated by spaces. It's not necessary to enter them one at a time on separate lines, like this:
But you could if you wanted to. Placing commands/words together on the same line is a bit like using :: in Extended Basic to separate statements, only in Forth we just use spaces because there is hardly any syntax in Forth, you're in total control.
Next, we need a word to erase the ball:
Erasing The Ball
: ERASE_BALL ( -- ) BALL_ROW BALL_COL 32 1 HCHAR ;
There's not a lot of code there - it's hardly worth making a word just for this, *however* the advantage is that it means we can test it separately from the rest of our code.
Let's test it:
First, fill the screen with a character, using HCHAR like this:
PAGE 0 0 ASCII * 960 HCHAR ERASE_BALL
PAGE clears the screen (which resets the cursor position to the top of the screen) then we fill the screen (assuming you're in 40 column mode) with asterisks using HCHAR. You should see a hole somewhere where ERASE_BALL erased an asterisk. Good. So, how does it work?
Well, during execution of ERASE_BALL, BALL_COL will push its value to the stack, BALL_ROW will push its value to the stack, we then push 32 to the stack (the ASCII code for a space character), and then we push 1 to the stack (the number of repeats - this is an optional parameter in HCHAR/VCHAR in BASIC, but NOT so in Forth). HCHAR then gobbles all those values up and uses them to draw a space at the correct place on the screen.
Calculate New Ball Position
Next, we need to re-create the calculation of the new ball position, and display of the ball. We're going to re-create this TI BASIC code:
180 REM CALCULATE NEW BALL POSITION 190 BALL_COL=BALL_COL+XDIR 200 BALL_ROW=BALL_ROW+YDIR 210 CALL HCHAR(BALL_ROW,BALL_COL,ASC("0"))
: MOVE_BALL ( -- ) XDIR +TO BALL_COL YDIR +TO BALL_ROW BALL_ROW BALL_COL ASCII 0 1 HCHAR ;
And voila. We have a new word, MOVE_BALL. Let's test it. We'll first make sure that the row, columns etc. are set to realistic values:
10 TO BALL_COL 10 TO BALL_ROW 1 TO XDIR 1 TO YDIR
Note how I typed all that on one line. I could have typed:
10 TO BALL_COL 10 TO BALL_ROW 1 TO XDIR 1 TO YDIR
But I'm lazy and impatient.
Okay, so type in PAGE MOVE_BALL <enter>
The ball should be displayed. Now type MOVE_BALL again.
Now type MOVE_BALL MOVE_BALL MOVE_BALL MOVE_BALL <enter>.
It should leave a trail of balls (ooer!).
So how does it work? Here's the breakdown:
- XDIR pushes its value to the stack;
- The word +TO removes it and *adds* it to whatever is stored in BALL_COL;
- YDIR pushes its value to the stack;
- The word +TO removes it and *adds* it to whatever is stored in BALL_ROW;
- We then push the values of BALL_COL, BALL_ROW, the ASCII code for 0, and the number 1 (the number of repeats) to the stack;
- HCHAR removes them and does it's thing.
Okay, we're nearly there! Next, we need to check if the ball is on a screen edge, and if it is then we reverse the direction of the ball in either the X or the Y direction.
TI BASIC is rather terrible at this, because IF can only target a line number, so you're forced to separate the IF from the code that should run when IF is true. Just awful. In Forth we can do much better, however, the syntax may hurt your head a little bit. Not to worry, I'll break it all down.
This is the TI BASIC code that we want to re-create:
220 REM CHECK FOR EDGE OF SCREEN 230 IF (BALL_COL<3)+(BALL_COL>30)THEN 260 240 IF (BALL_ROW<3)+(BALL_ROW>22)THEN 290 250 GOTO 170 260 REM REVERSE X DIRECTION 270 XDIR=-XDIR 280 GOTO 170 290 REM REVERSE Y DIRECTION 300 YDIR=-YDIR 310 GOTO 170
I'm going to create four new words:
- HIT_NS? (Hit north or south?)
- HIT_EW? (Hit east or west?)
- REV_XDIR (Reverse X direction)
- REV_YDIR (Reverse Y direction)
Now, to be clear, we could write all of the above as one word. In fact, we could write the entire program as one word, but you'd have a terrible job trying to debug it! That's the advantage of breaking our code down ("factoring it") into small chunks. We can test them, and then just string them together at the end.
So, here we go: HIT_NS? first:
: HIT_NS? ( -- flag ) \ check hit on top or bottom of screen BALL_ROW 2 < BALL_ROW 21 > OR ;
Whoa! That is some WEIRD looking code!! What on earth does it mean?
Let me break it down step by step. There's only seven instructions, so it's not difficult to understand. It just LOOKS weird (most Forth looks weird, to be honest!)
First, you need to be aware of the stack signature of this word:
( -- flag )
That means that this word takes nothing from the stack, but it does *leave* something on the stack. It leaves a flag (something that is either true or false). It's also very important to realise that the stack signature is a COMMENT. It's not a function parameter declaration like in C or Java. It's a comment that tells us *humans* what this word expects and leaves on the stack. Forth itself doesn't actually *know* what the word expects or leaves on the stack. It just dumbly tears through the code, obeying what it sees, and the results are the results. If the results are NOT what you expected, well, then YOU made a mistake somewhere!
So, here we go:
- BALL_ROW pushes its value to the stack;
- We push the value 2 to the stack
- We execute the word < which means "is less than?"
Is Less Than
The word < or "is less than?" takes two values off the stack and compares them. If the first value is less than the second value, it pushes a -1 (true). If the first value is NOT less than the second value it pushes a 0 (false). It's as simple as that.
So... at run time, BALL_ROW will be compared to 2, and if BALL_ROW *is* less than 2, the word "<" will push a -1 to the stack, otherwise it'll push a 0.
We then do the same thing but using the word ">" is "is greater than?". Here, we compare BALL_ROW to 29, and if it *is* greater than 29, ">" will give us a -1, otherwise it'll give us a 0 on the stack.
So, after the execution of these two lines of code, we'll end up with *two* values on the stack. The result of the < comparison, and the result of the > comparison.
Next, we execute the word OR. OR takes two values off the stack and if the first value, or the second value, or both values are true, it pushes a -1 (true) to the stack. If both values are 0, it pushes a 0 (false) to the stack. So OR pushes the flag to the stack that we refer to in the stack signature for our word.
We can prove that this will work at the command line, using numbers:
-1 0 OR . (result is -1 (true) because one of the inputs to OR was true
0 0 OR . (result is 0 (false) because both inputs to OR were false).
Let's have a quick look at the stack comments for these words:
- The word < has the stack comment ( a b -- flag ) which means flag will be true if a < b. A and b are removed from the stack.
- The word > has the stack comment ( a b -- flag ) which means flag will be true if a > b. A and b are removed from the stack.
- The word OR has the stack comment ( a b -- flag ) which means flag will be true if a or b are true. A and b are removed from the stack.
All quite simple and logical.
Okay, I'll rattle through the next one, it uses the exact same principle. It's just check East and West (left and right) screen edges.
: HIT_EW? \ Hit east or west? BALL_COL 2 < BALL_COL 29 > OR ;
I'll refrain from breaking this down as the principle is identical. I will however make a VERY brief detour and discuss paragraphs:
When we write in C or Java or assembly language we're used to leaving blank lines between lines of code. These are paragraphs, and they separate code up into logical blobs of code. Because Forth coded horizontally, we often use multiple spaces (two or three) on a line of code to break our code up into paragraphs. Consider HIT_EW? written in a horizontal style:
: HIT_EW? \ Hit east or west? BALL_COL 2 < BALL_COL 29 > OR ;
It reads okay (to someone that is used to Forth) but a better way to write it is like this:
: HIT_EW? \ Hit east or west? BALL_COL 2 < BALL_COL 29 > OR ;
That is probably a lot more readable to you. It certainly is to me. It's now much clearer that "BALL_COL 2 <" is a separate blob of code from "BALL_COL 29 >" because we separated them using paragraphs. It also takes up less screen space, and block space if you are using blocks.
REV_XDIR (Reverse X direction) and REV_YDIR (Reverse Y direction)
Okay, we're nearly finished. If you're having trouble reading all this stuff, spare a thought for the guy that had to write it!
Let's finish up with REV_XDIR and REV_YDIR which will reverse the direction of the ball in the horizontal and vertical directions:
: REV_XDIR ( -- ) \ reverse x direction XDIR NEGATE TO XDIR ; : REV_YDIR ( -- ) \ reverse y direction YDIR NEGATE TO YDIR ;
You can probably see what these do. For XDIR, XDIR goes to the stack, NEGATE then negates whatever is on the stack (1 becomes -1 and -1 becomes 1 etc.) and then TO writes it back into XDIR. Same principle for YDIR.
Let's test them:
-1 TO XDIR REV_XDIR XDIR .
TurboForth should display 1. I'll leave you to test REV_YDIR.
Now we're going to write a word to roll these four words up. We'll call it CHECK_DIR for Check Direction.
: CHECK_DIR ( -- ) HIT_EW? IF REV_XDIR THEN HIT_NS? IF REV_YDIR THEN ;
I reckon right about now your head just exploded. I hope you're not sat on a bus as you read this. Just what in the name of Satan's Holy Trousers is that THEN doing at the END of a line? This doesn't make sense at all!
Or does it?
Well, actually it does. Here's a quick detour into how IF...THEN works in Forth. First, some BASIC to compare it to:
10 INPUT A 20 IF A < 10 THEN 50 ELSE 70 30 PRINT "FINISHED!" 40 END 50 PRINT "LESS THAN 10" 60 GOTO 30 70 PRINT "NOT LESS THAN 10" 80 GOTO 30
This absolutely vile, abominable code which forces you to go searching down the code for the appropriate line numbers (what if there was 100 lines of other code between them? Just vile) can be beautifully expressed in Forth thus:
: CHECK ( n -- ) 10 < IF ." LESS THAN 10" ELSE ." NOT LESS THAN 10" THEN CR ." FINISHED" CR ;
Go ahead and type that in. Note the stack signature. It needs a value passed into it from the stack:
You understand how this works now: We put 9 on the stack, then call CHECK which uses the 9 we just we put there, and so on.
Let's break this CHECK word down:
It puts 10 on the stack. Then "less than?" executes which will compare whatever we put on the stack to 10 and leave a true or false on the stack. IF then consumes whatever "less than?" left on the stack. If it was a TRUE then the code after the IF will execute, ELSE the code after the ELSE will execute, THEN normal execution will continue to the end of the word.
The following should illustrate how this works, and it's important to realise that THIS IS VALID FORTH CODE (assuming the following words existed):
SUNNY? IF GET-SHADES ELSE GET-JACKET THEN GO-OUTSIDE
If you read that out loud, it reads like English. And well crafted Forth code, if factored nicely (which takes experience) will often read very close to English. It's clear from the above that the THEN denotes the continuation of the rest of the code. It's an "ENDIF" in other languages.
Alright, so what do the other words do? Well, the word ." just prints a string. It needs a space between it and the string, and a closing " to indicate the end of the string. The word CR means "carriage return" and moves the cursor/current print position to the next line, scrolling the screen upwards if necessary.
Putting It All Together
Let's review all the code we have so far:
: FRAME ( -- ) \ set up screen and draw frame 1 GMODE \ 32 column text mode 0 1 ASCII - 30 HCHAR 23 1 ASCII - 30 HCHAR 1 0 ASCII | 22 VCHAR 1 31 ASCII | 22 VCHAR 0 0 ASCII + 1 HCHAR 0 31 ASCII + 1 HCHAR 23 0 ASCII + 1 HCHAR 23 31 ASCII + 1 HCHAR ; 1 VALUE BALL_COL 1 VALUE BALL_ROW 1 VALUE XDIR 1 VALUE YDIR : SET_RC ( -- ) \ set row and column 30 RND 1+ TO BALL_COL 22 RND 1+ TO BALL_ROW ; : ERASE_BALL ( -- ) BALL_ROW BALL_COL 32 1 HCHAR ; : MOVE_BALL ( -- ) XDIR +TO BALL_COL YDIR +TO BALL_ROW BALL_ROW BALL_COL ASCII 0 1 HCHAR ; : HIT_NS? ( -- flag ) \ check hit on top or bottom of screen BALL_ROW 2 < BALL_ROW 21 > OR ; : HIT_EW? \ Hit east or west? BALL_COL 2 < BALL_COL 29 > OR ; : REV_XDIR ( -- ) \ reverse x direction XDIR NEGATE TO XDIR ; : REV_YDIR ( -- ) \ reverse y direction YDIR NEGATE TO YDIR ; : CHECK_DIR ( -- ) HIT_EW? IF REV_XDIR THEN HIT_NS? IF REV_YDIR THEN ;
So far, you can probably see that we don't yet have a "program" as such. We just have a collection of words that each do something, but we need to glue them together. So, let's break out the glue:
: BOUNCE ( -- ) FRAME SET_RC BEGIN ERASE_BALL MOVE_BALL CHECK_DIR AGAIN ;
So, BOUNCE calls FRAME which draws the screen, and then SET_RC which sets our row and column values. Then, we BEGIN a loop. The word BEGIN marks the start of the loop.
Then, we call ERASE_BALL, MOVE_BALL and CHECK_DIR. Notice how at this high level the code is quite generic. It's almost like English. It's just words strung together in a sentence:
"erase ball, move ball, check direction"
Then, we execute AGAIN which runs everything again from the word BEGIN (in reality, it jumps back to ERASE_BALL; BEGIN is just a marker to show where it jumps back to).
So we have this in our main loop: "erase ball, move ball, check direction, do it again".
It's English. Well factored code at the high-level will read like English. Sure, it's not so nice at the low level with all the stack management and stuff going on, but (and this is a big one) you tested all those words "on the way up". You know they work. No need to re-visit them. Your higher level words use the lower level words, and you can keep building your code up in this way. Words stand on the shoulders of other words. It's a LOT more sophisticated than line numbers, so it takes more practice, but once you've got it you won't want to do line numbers again.
If you type the complete program in as shown and type BOUNCE you will see the program run. However, there are two problems:
- It's too fast. You can't really see anything;
- There's no way to exit.
Let's fix that:
: DELAY ( n -- ) 0 DO LOOP ; : BOUNCE ( -- ) \ top-level code FRAME SET_RC BEGIN ERASE_BALL MOVE_BALL CHECK_DIR 100 DELAY 0 JOYST 1 = UNTIL ;
So, we've introduced a delay word which uses an empty loop just to spin the wheels for a while (more on DO...LOOP in a further article - hopefully someone else will write it! - it's generic; not specific to TurboForth) and we've changed BOUNCE as follows:
- We now read the first joystick (unit number 0).
- JOYST pushes a value on the stack according to what the joystick is doing. The only value we're currently interested in is 1, which means the fire button has been pressed.
- We push 0 onto the stack. JOYST uses it to read joystick 0 and pushes the result;
- We push the number 1 onto the stack;
- The word = ("equal?") tests the value that JOYST pushed against the 1 that we pushed. If they are equal then "=" will push a true else it will push a false;
- UNTIL consumes the number that "=" pushed. If it is TRUE then execution is allowed to continue past the UNTIL word, otherwise it loops back to begin. So, our code will loop back to the associated BEGIN word UNTIL the fire button is pressed.
- There's no code after the UNTIL so everything just stops.
You can see that the program is not really a program until we get to the word BOUNCE. That's where a bunch of related, but unconnected words come together to make a program, yet BOUNCE is just another word that we've added to the system.
This is how programs are grown in Forth. Of course, it's possible to be more sophisticated (where words leave values on the stack for other words to consume). We haven't done that much here. There is a bit of that going on in CHECK_DIR though.
Well, this turned out to be a LOT longer than I was planning. If you stuck with me to the end then I'm grateful. The whole program, in it's finished form which you can cut and paste into Classic99:
: FRAME ( -- ) \ set up screen and draw frame 1 GMODE \ 32 column text mode 0 1 ASCII - 30 HCHAR 23 1 ASCII - 30 HCHAR 1 0 ASCII | 22 VCHAR 1 31 ASCII | 22 VCHAR 0 0 ASCII + 1 HCHAR 0 31 ASCII + 1 HCHAR 23 0 ASCII + 1 HCHAR 23 31 ASCII + 1 HCHAR ; 1 VALUE BALL_COL 1 VALUE BALL_ROW 1 VALUE XDIR 1 VALUE YDIR : SET_RC ( -- ) \ set row and column 30 RND 1+ TO BALL_COL 22 RND 1+ TO BALL_ROW ; : ERASE_BALL ( -- ) \ erase ball from screen BALL_ROW BALL_COL 32 1 HCHAR ; : MOVE_BALL ( -- ) \ update ball position and draw it XDIR +TO BALL_COL YDIR +TO BALL_ROW BALL_ROW BALL_COL ASCII 0 1 HCHAR ; : HIT_NS? ( -- flag ) \ hit top or bottom of screen? BALL_ROW 2 < BALL_ROW 21 > OR ; : HIT_EW? \ Hit east or west? BALL_COL 2 < BALL_COL 29 > OR ; : REV_XDIR ( -- ) \ reverse x direction XDIR NEGATE TO XDIR ; : REV_YDIR ( -- ) \ reverse y direction YDIR NEGATE TO YDIR ; : CHECK_DIR ( -- ) \ reverse direction if hit screen edge HIT_EW? IF REV_XDIR THEN HIT_NS? IF REV_YDIR THEN ; : DELAY ( n -- ) 0 DO LOOP ; : BOUNCE ( -- ) \ top-level word FRAME SET_RC BEGIN ERASE_BALL MOVE_BALL CHECK_DIR 100 DELAY 0 JOYST 1 = UNTIL ;
- HCHAR - http://turboforth.ne...word.asp?ID=220
- VCHAR - http://turboforth.ne...word.asp?id=232
- < - http://turboforth.ne..._word.asp?ID=50
- > - http://turboforth.ne..._word.asp?ID=54
- = - http://turboforth.ne..._word.asp?ID=53
- BEGIN - http://turboforth.ne..._word.asp?ID=72
- AGAIN - http://turboforth.ne..._word.asp?ID=71
- UNTIL - http://turboforth.ne..._word.asp?ID=89
- OR - http://turboforth.ne..._word.asp?ID=95
- ASCII - http://turboforth.ne...word.asp?ID=210
- VALUE - http://turboforth.ne...word.asp?ID=168
- TO - http://turboforth.ne...word.asp?id=167
- +TO - http://turboforth.ne...word.asp?id=152
I hope you enjoyed learning some Forth.
Edited by Willsy, Mon May 15, 2017 3:53 PM.