Before proceeding, I suggest you read the original article to have a better understanding of the program logic, as below I will only explain the parts that require changes to work in Toddy Forth-79. And if you have little or no familiarity with Forth, then it's best to read the tutorial from the beginning.
The game code
The original code is written in lowercase, but will be transcribed here using uppercase, as required by TF79.
Variables and contants
The code starts with the creation of two variables that will store the snake's coordinates, SNAKE-X-HEAD and SNAKE-Y-HEAD. These are two one-dimensional arrays, each with 500 memory cells. These variables were created with the word VARIABLE, but the TF79 (as well as most Forths) has another word more suitable for this: CREATE . CREATE creates an empty definition, that is, without anything in your CFA/PFA (if you don't understand what this is, read section 11.2 of the manual). For example, CREATE FOO adds the word FOO to the vocabulary and when we invoke FOO all it does is leave the address of its CFA/PFA on the stack. The word CELLS is not part of the TF79 vocabulary, but it can be easily defined. In Forths that run on 8-bit computers, each cell is equivalent to 2 bytes, so our code would look like this:
Code: Select all
: CELLS ( n -- ) 2* ;
CREATE SNAKE-X-HEAD 500 CELLS ALLOT
CREATE SNAKE-Y-HEAD 500 CELLS ALLOT
Code: Select all
[CREATE SNAKE-X-HEAD 500 ALLOT
CREATE SNAKE-Y-HEAD 500 ALLOT
After that, we define the words that will draw the snake:
Code: Select all
: DRAW-WHITE ( x y -- ) SWAP AT BL EMIT ;
: DRAW-BLACK ( x y -- ) SWAP AT 128 EMIT ;
So the code for this first part looks like this:
Code: Select all
CREATE SNAKE-X-HEAD 500 ALLOT
CREATE SNAKE-Y-HEAD 500 ALLOT
VARIABLE APPLE-X
VARIABLE APPLE-Y
0 CONSTANT LEFT
1 CONSTANT UP
2 CONSTANT RIGHT
3 CONSTANT DOWN
24 CONSTANT WIDTH
24 CONSTANT HEIGHT
VARIABLE DIRECTION
VARIABLE LENGTH
: SNAKE-X SNAKE-X-HEAD + ;
: SNAKE-Y SNAKE-Y-HEAD + ;
: DRAW-WHITE SWAP AT BL EMIT ;
: DRAW-BLACK SWAP AT 128 EMIT ;
Here we have just two small changes, the first is in the definition of INITIALIZE-SNAKE where we replace the ! by C! (we will manipulate only 1 byte of the array); The second change is in the definition of INITIALIZE, where the loops are replaced by the word PAGE, to erase the screen (not necessary, but more efficient).
This part then looks like this:
Code: Select all
: DRAW-WALLS
WIDTH 0 DO
I 0 DRAW-BLACK
I HEIGHT 1 - DRAW-BLACK
LOOP
HEIGHT 0 DO
0 I DRAW-BLACK
WIDTH 1 - I DRAW-BLACK
LOOP ;
: INITIALIZE-SNAKE
4 LENGTH !
LENGTH @ 1 + 0 DO
12 I - I SNAKE-X C!
12 I SNAKE-Y C!
LOOP
RIGHT DIRECTION ! ;
: SET-APPLE-POSITION
APPLE-X ! APPLE-Y ! ;
: INITIALIZE-APPLE
4 4 SET-APPLE-POSITION ;
: INITIALIZE
PAGE
DRAW-WALLS
INITIALIZE-SNAKE
INITIALIZE-APPLE ;
Moving the Snake
Here the words MOVE-* and MOVE-SNAKE-TAIL are changed so that they can handle only 1 byte, in accordance with the new definitions of SNAKE-X-HEAD and SNAKE-Y-HEAD. MOVE-SNAKE-HEAD does not change. But another not so obvious adjustment is still needed in MOVE-SNAKE-TAIL.
With the Forth-83 standard, the DO/LOOP structure had its behavior slightly modified in relation to Forth-79. I won't go into detail about these differences, a quick Google search will bring up this information. But one difference in particular applies to the DO/+LOOP present in MOVE-SNAKE-TAIL: when there is a negative step, in the most recent Forths the loop performs once more than in the Forth-79. For example, 0 3 DO I . -1 +LOOP will print the sequence 3 2 1 on Forth-79 and 3 2 1 0 post Forth-83. The solution is simple, subtract 1 from the loop limit, so the equivalent loop in Forth-79 will be -1 3 DO I . -1 +LOOP.
So the code for this part looks like this:
Code: Select all
: MOVE-UP SNAKE-Y-HEAD C@
1- SNAKE-Y-HEAD C! ;
: MOVE-LEFT SNAKE-X-HEAD C@
1- SNAKE-X-HEAD C! ;
: MOVE-DOWN SNAKE-Y-HEAD C@
1+ SNAKE-Y-HEAD C! ;
: MOVE-RIGHT SNAKE-X-HEAD C@
1+ SNAKE-X-HEAD C! ;
: MOVE-SNAKE-HEAD DIRECTION @
LEFT OVER = IF MOVE-LEFT ELSE
UP OVER = IF MOVE-UP ELSE
RIGHT OVER = IF MOVE-RIGHT
ELSE DOWN OVER = IF MOVE-DOWN
THEN THEN THEN THEN DROP ;
: MOVE-SNAKE-TAIL
-1 LENGTH @ DO
I SNAKE-X C@ I 1+ SNAKE-X C!
I SNAKE-Y C@ I 1+ SNAKE-Y C!
-1 +LOOP ;
Keyboard Input
Here just the use of the word INKEY in CHECK-INPUT and the replacement of the key codes with ASCII key in CHANGE-DIRECTION, with control of the snake being assigned to the IJKL keys.
Here is the code for that part:
Code: Select all
: IS-HORIZONTAL DIRECTION @ DUP
LEFT = SWAP
RIGHT = OR ;
: IS-VERTICAL DIRECTION @ DUP
UP = SWAP
DOWN = OR ;
: TURN-UP IS-HORIZONTAL
IF UP DIRECTION ! THEN ;
: TURN-LEFT IS-VERTICAL
IF LEFT DIRECTION ! THEN ;
: TURN-DOWN IS-HORIZONTAL
IF DOWN DIRECTION ! THEN ;
: TURN-RIGHT IS-VERTICAL
IF RIGHT DIRECTION ! THEN ;
: CHANGE-DIRECTION
ASCII J OVER =
IF TURN-LEFT ELSE
ASCII I OVER =
IF TURN-UP ELSE
ASCII L OVER =
IF TURN-RIGHT ELSE
ASCII K OVER =
IF TURN-DOWN
THEN THEN THEN THEN DROP ;
: CHECK-INPUT
INKEY CHANGE-DIRECTION ;
The Apple
Here only the replacement of RANDOM by RND, the usual exchange of @ for C@ associated with SNAKE-X_HEAD and SNAKE-Y-HEAD and, finally, the inclusion of the word BELL in CHECK-APPLE to produce a sound signal when the snake take an apple:
Code: Select all
: RANDOM-POSITION
WIDTH 4 - RND 2 + ;
: MOVE-APPLE
APPLE-X @ APPLE-Y @ DRAW-WHITE
RANDOM-POSITION
RANDOM-POSITION
SET-APPLE-POSITION ;
: GROW-SNAKE 1 LENGTH +! ;
: CHECK-APPLE
SNAKE-X-HEAD C@ APPLE-X @ =
SNAKE-Y-HEAD C@ APPLE-Y @ =
AND IF BELL
MOVE-APPLE
GROW-SNAKE
THEN ;
Collision Detection
Coordinates are inverted to comply with AT requirement; address 16398 contains the address within the DFILE of the coordinate defined by the AT.
Code: Select all
: CHECK-COLLISION
SNAKE-Y-HEAD C@
SNAKE-X-HEAD C@ AT
16398 @ C@ 128 = ;
Drawing the Snake and Apple
The usual exchanges of @ for C@ associated with SNAKE-X and SNAKE-Y and the adoption of the asterisk to represent the apple:
Code: Select all
: DRAW-SNAKE
LENGTH @ 0
DO
I SNAKE-X C@ I SNAKE-Y C@
DRAW-BLACK
LOOP
LENGTH @ SNAKE-X C@
LENGTH @ SNAKE-Y C@
DRAW-WHITE ;
: DRAW-APPLE
APPLE-Y @
APPLE-X @
AT ASCII * EMIT ;
The Game Loop
The "GAME OVER" message was moved to START, a message informing the length of the snake was included and also an end-of-game sound signal was added:
Code: Select all
: GAME-LOOP
BEGIN
DRAW-SNAKE
DRAW-APPLE
CHECK-INPUT
MOVE-SNAKE-TAIL
MOVE-SNAKE-HEAD
CHECK-APPLE
CHECK-COLLISION
UNTIL ;
: START INITIALIZE GAME-LOOP
7 26 AT ." GAME" 8 26 AT
." OVER" 11 25 AT ." SNAKE"
12 25 AT ." LENGTH:"
14 27 AT LENGTH @ 3 .R
3 0 DO BELL 2 WAIT LOOP
BEGIN INKEY UNTIL PAGE ;
The word SLEEP has an equivalent in the TF79, WAIT, which pauses processing for multiples of 20ms. The 100 SLEEP of the original program could then be replaced by 5 WAIT ( 5x20ms ), but this was not necessary here. At start the snake is very fast, but it becomes slower as it grows, making the game boring after a certain size (see the video below).
Below is the complete list of the game. Copy and paste the text into the SNAKE.FTH file and then convert it to a block file:
blk2fth.pl SNAKE.FTH
Then transfer the created file SNAKE.BLK to the ZXpand SD card and load it into the Forth-79.
Code: Select all
\ -= SNAKE GAME =- 1/6
CREATE SNAKE-X-HEAD 500 ALLOT
CREATE SNAKE-Y-HEAD 500 ALLOT
VARIABLE APPLE-X
VARIABLE APPLE-Y
0 CONSTANT LEFT
1 CONSTANT UP
2 CONSTANT RIGHT
3 CONSTANT DOWN
24 CONSTANT WIDTH
24 CONSTANT HEIGHT
VARIABLE DIRECTION
VARIABLE LENGTH
: SNAKE-X ( n -- addr)
SNAKE-X-HEAD + ;
: SNAKE-Y ( n -- addr)
SNAKE-Y-HEAD + ;
: DRAW-WHITE SWAP AT BL EMIT ;
: DRAW-BLACK SWAP AT 128 EMIT ;
: DRAW-WALLS
WIDTH 0 DO
I 0 DRAW-BLACK
I HEIGHT 1 - DRAW-BLACK
LOOP
HEIGHT 0 DO -->
\ -= SNAKE GAME =- 2/6
0 I DRAW-BLACK
WIDTH 1 - I DRAW-BLACK
LOOP ;
: INITIALIZE-SNAKE
4 LENGTH !
LENGTH @ 1 + 0 DO
12 I - I SNAKE-X C!
12 I SNAKE-Y C!
LOOP
RIGHT DIRECTION ! ;
: SET-APPLE-POSITION
APPLE-X ! APPLE-Y ! ;
: INITIALIZE-APPLE
4 4 SET-APPLE-POSITION ;
: INITIALIZE
PAGE
DRAW-WALLS
INITIALIZE-SNAKE
INITIALIZE-APPLE ;
: MOVE-UP SNAKE-Y-HEAD C@
1- SNAKE-Y-HEAD C! ;
: MOVE-LEFT SNAKE-X-HEAD C@
1- SNAKE-X-HEAD C! ;
: MOVE-DOWN SNAKE-Y-HEAD C@
1+ SNAKE-Y-HEAD C! ;
-->
\ -= SNAKE GAME =- 3/6
: MOVE-RIGHT SNAKE-X-HEAD C@
1+ SNAKE-X-HEAD C! ;
: MOVE-SNAKE-HEAD DIRECTION @
LEFT OVER = IF MOVE-LEFT ELSE
UP OVER = IF MOVE-UP ELSE
RIGHT OVER = IF MOVE-RIGHT
ELSE DOWN OVER = IF MOVE-DOWN
THEN THEN THEN THEN DROP ;
: MOVE-SNAKE-TAIL
-1 LENGTH @ DO
I SNAKE-X C@ I 1+ SNAKE-X C!
I SNAKE-Y C@ I 1+ SNAKE-Y C!
-1 +LOOP ;
: IS-HORIZONTAL DIRECTION @ DUP
LEFT = SWAP
RIGHT = OR ;
: IS-VERTICAL DIRECTION @ DUP
UP = SWAP
DOWN = OR ;
: TURN-UP IS-HORIZONTAL
IF UP DIRECTION ! THEN ;
: TURN-LEFT IS-VERTICAL
IF LEFT DIRECTION ! THEN ;
: TURN-DOWN IS-HORIZONTAL
IF DOWN DIRECTION ! THEN ;
-->
\ -= SNAKE GAME =- 4/6
: TURN-RIGHT IS-VERTICAL
IF RIGHT DIRECTION ! THEN ;
: CHANGE-DIRECTION
ASCII J OVER =
IF TURN-LEFT ELSE
ASCII I OVER =
IF TURN-UP ELSE
ASCII L OVER =
IF TURN-RIGHT ELSE
ASCII K OVER =
IF TURN-DOWN
THEN THEN THEN THEN DROP ;
: CHECK-INPUT
INKEY CHANGE-DIRECTION ;
: RANDOM-POSITION
WIDTH 4 - RND 2 + ;
: MOVE-APPLE
APPLE-X @ APPLE-Y @ DRAW-WHITE
RANDOM-POSITION
RANDOM-POSITION
SET-APPLE-POSITION ;
: GROW-SNAKE 1 LENGTH +! ;
-->
\ -= SNAKE GAME =- 5/6
: CHECK-APPLE
SNAKE-X-HEAD C@ APPLE-X @ =
SNAKE-Y-HEAD C@ APPLE-Y @ =
AND IF BELL
MOVE-APPLE
GROW-SNAKE
THEN ;
: CHECK-COLLISION
SNAKE-Y-HEAD C@
SNAKE-X-HEAD C@ AT
16398 @ C@ 128 = ;
: DRAW-SNAKE
LENGTH @ 0
DO
I SNAKE-X C@ I SNAKE-Y C@
DRAW-BLACK
LOOP
LENGTH @ SNAKE-X C@
LENGTH @ SNAKE-Y C@
DRAW-WHITE ;
: DRAW-APPLE
APPLE-Y @
APPLE-X @
AT ASCII * EMIT ;
-->
\ -= SNAKE GAME =- 6/6
: GAME-LOOP
BEGIN
DRAW-SNAKE
DRAW-APPLE
CHECK-INPUT
MOVE-SNAKE-TAIL
MOVE-SNAKE-HEAD
CHECK-APPLE
CHECK-COLLISION
UNTIL ;
: START INITIALIZE GAME-LOOP
7 26 AT ." GAME" 8 26 AT
." OVER" 11 25 AT ." SNAKE"
12 25 AT ." LENGTH:"
14 27 AT LENGTH @ 3 .R
3 0 DO BELL 2 WAIT LOOP
BEGIN INKEY UNTIL PAGE ;
In the next article we will see how we can optimize the game to maintain a satisfactory speed.