Phil Hagelberg wrote several implementations of the robots(1) game found on *BSDs in different lisp dialects to get a feeling on how they compare. This has been an interesting read for me and I wanted to take it as an opportunity to try an old aquaintance again: Forth.
My robots version has the following rules:
- The player dies when a robot moves into his position on the next move.
- The robot dies when it falls into a hole.
- Robots are allowed to step on each other and move stacked on top onwards. This is a mayor violation of the classic rule but I didn't want to bother with it at the time.
- Teleports are limited per level.
- Each level increases the number of robots by 1.
- The player cannot run into holes.
So, without further ado, here is my forth robots game:
\ Robots.fth - An implementation of the BSD robots game
\
\ You ("@") are placed into a wide space covered with holes ("o") and
\ robots ("R"). Try to lure the robots into the holes to win. If a
\ robot catches you, you lose. You are able to teleport yourself to a
\ random location 3 times.
require random.fs
\ statistics
variable #moves 0 #moves !
variable #teleports 3 #teleports !
\ the board
20 constant board-rows
78 constant board-cols
: random-xy ( -- x y ) \ returns random x y coordinates
board-cols random board-rows random ;
board-rows board-cols * constant board-dimension
create board board-dimension allot
: xy->board ( x y -- i ) \ converts xy coordinates to the board buffer index
board-cols * + ;
: board@ ( x y -- c ) \ fetches a tile at coordinates x y
xy->board board + c@ ;
: board! ( c x y -- ) \ stores tile char c at coordinates x y
xy->board board + c! ;
\ drawing words
char @ constant player-sym
char R constant robot-sym
char o constant hole-sym
32 constant floor-sym \ a space character
char | constant wall-sym
: border ( -- ) \ draw a border like '+----+' for the board
[char] + emit
board-cols 0 do [char] - emit loop
[char] + emit ;
: wall ( -- ) \ print a wall symbol
wall-sym emit ;
: board-reset ( -- ) \ empties a board by placing floor tiles in it
board-dimension 0 do \ for the whole board
floor-sym board i + ! \ store a floor tile
loop ;
: board-print ( -- ) \ prints the whole board
\ clears screen, prints the board surrounded
\ by walls and a border on top and bottom
page border cr wall
board-dimension 0 do
i board-cols mod 0= i 0> and if \ special case for first position at (0,0)
wall cr wall then \ otherwise draw a wall at the end of each line
board i + @ emit \ and draw the (next) tile
loop
wall cr border cr ;
\ player
create (player) 2 cells allot
: player@ ( -- x y ) \ returns current player x y position
(player) 2@ ;
: player! ( x y -- ) \ sets current player position to x y
(player) 2! ;
: new-position ( x1 y1 dx dy -- x2 y2 ) \ adds an offset to the current position
rot + \ calculate y2 ( x1 dx y2 )
>r \ store y2
+ r> ; \ add x1 dx and push y2
: valid-position? ( x y -- f ) \ checks whether the player can be placed here
2dup 2>r \ save coordinates for later
0 board-rows within swap \ y is within board boundaries
( b x ) 0 board-cols within and \ x is also within boundaries?
2r> ( x y ) board@ floor-sym = and ; \ is it a free space? This prevents falling into holes
: up ( -- dx dy ) 0 -1 ;
: down ( -- dx dy ) 0 1 ;
: left ( -- dx dy ) -1 0 ;
: right ( -- dx dy ) 1 0 ;
: teleport-location ( -- ) \ ensure that the new location is legal
random-xy \ get random coordinates
2dup valid-position? invert \ use a copy for testing, is valid?
if 2drop \ if it is not, clean stack
recurse then ; \ and try again
: update-player ( x y -- )
\ move the player tile on the board
\ and update the player position
floor-sym player@ board! \ set the floor tile at old pos
player-sym rot rot \ ( player-sym x y )
2dup player! \ set player position, leave x y
( player-sym x y ) board! ; \ set player tile at new position
: move ( dx dy -- )
\ high level movement word, taking a direction
\ and moving the player if the new position is valid
player@ 2swap new-position \ get new x y coordinates
2dup valid-position? \ if these are valid
if update-player \ move to it
else 2drop then ; \ or restore the stack
\ hole words
10 constant #holes
create holes #holes 2* cells allot
: hole@ ( i -- x y ) 2* cells holes + 2@ ;
: hole! ( x y i -- ) 2* cells holes + 2! ;
\ robot words
\ we do have a maximum of 20 robots
20 constant #max-robots
\ we start with 2 robots
2 value #robots
\ robots occupy 3 cells: x y alive?
create robots #max-robots 3 * cells allot
: robot@ ( i -- x y ) \ return the x y position of the ith robot
3 * cells robots + 2@ ;
: robot! ( x y i -- ) \ set the ith robot's postion to x y
3 * cells robots + 2! ;
: robot-alive! ( t i -- ) \ set the alive flag to f for the ith robot
3 * cells 2 cells + robots + ! ;
: robot-alive? ( i -- t ) \ return the alive flag for the ith robot
3 * cells 2 cells + robots + @ ;
: #robots-alive ( -- n ) \ returns the numer of robots that are alive
0 \ always return something sensible
#robots 0 ?do \ we iterate over the current number of robots
i robot-alive? \ it it is alive
if 1+ then \ count it
loop ;
: update-robot ( x y i -- ) \ sets robot position and board tile
>r \ save index for later usage
floor-sym r@ robot@ board! \ replace the old place with floor tile
r@ robot! \ update the robot
robot-sym r@ robot@ board! \ robot tie on new pos
r> drop ; \ clean stack
: distance ( x1 y1 x2 y2 -- x2-x1 y2-y1) \ returns the distance between two positions
>r \ save y2
rot - \ x2-x1
r> rot - \ y2-y1
swap ; \ -> x' y'
: sign ( n1 -- n2 ) \ calculate the sign of a number
dup 0=
if
drop 0 \ 0 -> 0
else
dup abs / \ n1 / (abs n1) -> -1 / 1
then ;
: direction ( d1 d2 -- dx dy ) \ extract the direction from a given position
sign swap sign ;
: towards-player ( x y -- x' y' ) \ return new position that moves one step toward the player's current pos
2dup player@ distance direction new-position ;
: collision? ( x1 y1 x2 y2 -- f ) \ are the two positions the same?
xy->board \ get the index for the second position
rot rot \ move first position to TOS
xy->board = ; \ convert to index and compare
: is-in-hole? ( x y -- f ) \ checks whether a given position is on a hole
xy->board \ convert to index
false \ initialise with false
#holes 0 do
over \ get a copy of the index
i hole@ xy->board \ is it on this hole?
= or \ yes? if so or it
loop swap drop ; \ ( i f ) -> ( -- f )
: move-robot ( i -- ) \ move the i-th robot towards the player
dup >r robot@ towards-player \ get the new coords
2dup is-in-hole? \ fell into a hole
if false r@ robot-alive! \ yes, this is a dead robot
floor-sym r> robot@ board! \ replace the old robot position with an empty tile
2drop \ remove the wrong position again
else
r> update-robot \ if alive update the robot's position and the board
then ;
: move-robots ( -- ) \ highlevel word to move all robots towards player
#robots 0 ?do
i robot-alive? \ if robot i is alive
if i move-robot then loop ; \ move it towards player
: any-collision? ( -- f ) \ returns true if any robot caught the player
false \ we assume that the player is well
#robots 0 \ for all robots
?do i robot-alive? \ is this robot alive
if i robot@ player@ collision? \ has this robot caught the player?
or \ set flag to true if so
then loop ;
\ game routines init, loop
: init-robots ( -- ) \ place the current number of robots on the board and activate them
#robots 0 ?do
robot-sym random-xy 2dup i robot! board! \ place them on board and initialise position
true i robot-alive! loop ; \ switch them on
: init-holes ( -- ) \ randomly scatter holes on the board
#holes 0 do hole-sym random-xy 2dup i hole! board! loop ;
: init-player ( -- ) \ place player on the board
teleport-location \ find a spot where the player can live
2dup player! update-player \ update board tiles and player pos
0 #moves ! \ reset move counter
3 #teleports ! ; \ give the player 3 teleports
: reset-game ( -- ) \ sets up a new game with the current number of robots
board-reset init-player init-robots init-holes ;
: status-line ( -- ) \ prints a status line on screen
." moves: " #moves @ . ." teleports: " #teleports @ . ." robots: " #robots-alive . ;
: help ( -- ) \ prints a key legend on screen
." h: left, j: down, k: up, l: right, t: teleport, q: quit, any other key waits." cr ;
: user-input ( -- ) \ waits for one key, then handles player movement
key
case
[char] h of left move endof
[char] j of down move endof
[char] k of up move endof
[char] l of right move endof
[char] q of ." Thanks for playing! " quit endof
[char] t of #teleports @ 0>
if #teleports @ 1- #teleports !
teleport-location 2dup
update-player player! then endof
endcase
1 #moves +! ;
: run ( -- ) \ main loop and entry point
!csp
2 to #robots \ start with 2 robots
reset-game
begin
board-print status-line cr help \ print the board
#robots-alive 0= \ are robots left?
if ." You win!" cr \ no, next round with more robots
#max-robots #robots 1+ min to #robots \ unless there are already #max-robots
reset-game then
any-collision? \ did they catch the player?
if ." You died! " cr quit \ yes, player dies
else user-input then \ handle user input
move-robots \ robots move last
?csp \ watch out for an unclean stack
again ;
here seed ! \ initialise PRNG
run \ start the game
You can also grab the source and try it for yourself. Note that the random word is a nonstandard forth word, I have used GNU Forth for this and you may need to adjust this even for your GNU forth installation. On OpenBSD I need to pass the full path to the file (include /usr/local/share/gforth/random.fs). On my linux system it works as is.
My observations:
- It is about as long as a scheme version I'd have written (or estimated) modulo comments and whitespace.
- Working with the stack is not hard at all
- Debugging Forth words has been easier than anticipated. I did not use the gforth debugging facilities though.
- A bad choice of data structures is punished more heavily in forth due to unnecessary stack juggling involved than in other programming languages.
- The forth community is as helpful as the scheme community. As a plus despite the split into hundreds of different forth systems you can reach all the active people via the newsgroup comp.lang.forth.
- It is *fun*! Finding a way to ever write a terser word has been a nice brain teaser and kept me going.
I have sent my first attempt to comp.lang.forth for comments. It looked a bit smaller due to missing comments and features. The nice people on c.l.f helped improve the program and my understanding of forth a lot. It seems I did it mostly right on the first try :)
So how does this game compare to scheme? All in all, I have to say it is not too much different. The interactivity in the forth interpreter is as nice as a scheme repl, the lack of parenthesis is made up by the unusual stack juggling, which got easier by the minute.
As with scheme forth provides a small core language and extensions differ from implementation to implementation. The community is aware of this and tries to stick to the core language for portability but breaks it when necessary.
A nice thing is that you can immediately fiddle with internals if you need it, extend the compiler and even write macros! Although it seemed overkill for this little game.
So will I abandon scheme for this forth? Probably not, but I am looking forward to using it more and more, especially on embedded systems.