Serpentino

Description of the page content

Text-based snake game written Forth for Gforth.

Active project. Started on 2017-11-22.

Tags:

Serpentino

A text-based snake game under development written in Forth for Gforth (Gforth 0.7.9 required).

Credit: Forked on 2017-11-22 from the initial commit of Robert Pfeiffer’s forthsnake.

The interface will be multilingual: in English, Esperanto, Interlingue and Spanish.

There's little to show. The development can be followed in the GitHub repository.

Screenshots

SerpentinoSerpentino

Source code

#! /usr/bin/env gforth

\ Serpentino

: version s" 0.30.0+201711270118" ;
\ See change log at the end of the file.

\ Description:
\
\ A text-based snake game under development
\ written in Forth (http://forth-standard.org)
\ for Gforth (http://gnu.org/software/gforth).

\ Author: Marcos Cruz (programandala.net)
\ http://programandala.net
\ http://github.com/programandala-net/serpentino

\ =============================================================
\ License

\ You may do whatever you want with this work, so long as you
\ retain all copyright, credit and authorship notices, and this
\ license.  There is no warranty.

\ =============================================================
\ Credit

\ Forked on 2017-11-22 from the initial commit of Robert
\ Pfeiffer's forthsnake
\ (https://github.com/robertpfeiffer/forthsnake), 2009.

\ ==============================================================
\ Requirements

\ From Gforth

require colorize.fs

\ From Galope

require galope/between.fs                  \ `between`
require galope/e-key-to.fs                 \ `ekey>`
require galope/minus-keys.fs               \ `-keys`
require galope/question-one-minus-store.fs \ `?1-!`
require galope/random-between.fs           \ `random-between`
require galope/unhome.fs                   \ `unhome`

\ ==============================================================

variable colorize colorize on
  \ Flag.

<a black >bg red   >fg a> value apple-attr
<a black >bg           a> value arena-attr
<a black >bg red   >fg a> value crush-attr
<a black >bg green >fg a> value snake-attr
<a black >bg           a> value status-attr
<a black >bg green >fg a> value text-attr
<a black >bg white >fg a> value wall-attr
  \ Color attributes.

: ?attr! ( x -- ) colorize @ if attr! else drop then ;
  \ If `colorize` is non-zero, set color attribute _x_.

variable score
variable record record off
  \ Counters.

variable delay
  \ Crawl delay in ms.

192 constant initial-delay
  \ Initial crawl delay in ms.

4 constant acceleration
  \ Delay decrement.

cols 2 - constant arena-cols
rows 4 - constant arena-rows
  \ Size of the arena.

cols arena-cols - 2/ constant arena-x
rows arena-rows - 2/ constant arena-y
  \ Coordinates of the top-left corner of the arena.

arena-x arena-cols + 1- constant arena-max-x
arena-y arena-rows + 1- constant arena-max-y
  \ Coordinates of the bottom-right corner of the arena.

arena-x 1- constant wall-x
arena-y 1- constant wall-y
  \ Coordinates of the top-left corner of the wall.

arena-max-x 1+ constant wall-max-x
arena-max-y 1+ constant wall-max-y
  \ Coordinates of the bottom-right corner of the wall.

wall-max-x wall-x - 1+ constant wall-cols
wall-max-y wall-y - 1+ constant wall-rows
  \ Size of the wall.

4 constant initial-length
  \ Initial length of the snake.

512 constant max-max-length
  \ Limit of the calculated maximum length of the snake, no
  \ matter the size of the screen. This limit makes sure the
  \ buffer is big enough, no matter if the size of the screen
  \ is increased.

: (max-length) ( -- n )
  arena-rows arena-cols * 5 / max-max-length min ;
  \ Return maximum length _n_ of the snake, calculated after
  \ the size of the arena.

(max-length) value max-length
  \ Maximum length of the snake.

2 cells constant /segment
  \ Size of each snake's segment.

create snake  max-max-length /segment * allot
  \ Snake's segments. Each segment contains its coordinates.

2variable apple
  \ Coordinates of the apple.

variable head>
  \ Number of the head segment.

2variable previous-head
  \ Coordinates of the head before crawling.

variable length
  \ Snake's current length.

2variable direction
  \ Snake's current direction (coordinate increments).

: segment ( n -- a )
  head> @ + max-length mod /segment * snake + ;
  \ Convert segment number _n_ to its address _a_.

: head ( -- a ) 0 segment ;
  \ Return address _a_ of the snake's head segment.

: neck ( -- a ) 1 segment ;
  \ Return address _a_ of the snake's "neck" segment, ie.
  \ its second segment.

: tail ( -- a ) length @ segment ;
  \ Return address _a_ of the snake's tail segment.

: clash? ( a1 a2 -- f ) 2@ rot 2@ d= ;
  \ Are the coordinates contained in _a1_ equal to the
  \ coordinates contained in _a2_?

: cross? ( a -- f ) head clash? ;
  \ Does the head cross segment _a_?

: random-xy ( -- col row ) arena-x arena-max-x random-between
                           arena-y arena-max-y random-between ;

: segment? ( col row -- f )
  length @ 0 ?do     2dup  i segment 2@ d=
                  if 2drop true  unloop exit then
             loop    2drop false ;
  \ Is there a snake's segment at _col row_?

: apple-random-xy ( -- col row )
  begin random-xy 2dup segment? while 2drop repeat ;

: new-apple ( -- ) apple-random-xy apple 2! ;
  \ Locate a new apple.

wall-x             constant status-x
wall-max-y 1+      constant status-y
wall-max-x         constant status-max-x
  \ Coordinates of the status bar.

status-max-x status-x - 1+ constant status-cols
  \ Columns of the status bar.

: score$ ( -- ca len ) s" Score: " ;
  \ Return the score label _ca len_.

status-x 1+ constant score-label-x
status-y    constant score-label-y
  \ Coordinates of the score label.

: .score$ ( -- )
  text-attr ?attr!
  score-label-x score-label-y at-xy score$ type ;
  \ Display the score label.

4 constant digits

: .(score) ( n -- )
  text-attr ?attr! s>d <# digits 0 ?do # loop #> type ;

: score-xy ( -- col row )
  score-label-x score$ nip + score-label-y ;
  \ Return the coordinates _col row_ of the score.

: .score ( -- ) score-xy at-xy score @ .(score) ;
  \ Display the score.

: record$ ( -- ca len ) s" Record: " ;
  \ Return the record label _ca len_.

wall-max-x 1- digits - record$ nip - constant record-label-x
status-y                             constant record-label-y
  \ Coordinates of the record label.

: .record$ ( -- )
  text-attr ?attr!
  record-label-x record-label-y at-xy record$ type ;
  \ Display the record label.

: record-xy ( -- col row )
  record-label-x record$ nip + record-label-y ;
  \ Return the coordinates _col row_ of the record.

: .record ( -- ) record-xy at-xy record @ .(score) ;
  \ Display the record.

: grow ( -- ) length @ 1+ max-length min length ! ;
  \ Grow the snake.

: .(apple) ( -- ) ." Q";
  \ Just display the apple, without changing the current
  \ colors, at the current cursor position.

: .apple ( -- ) apple-attr ?attr! apple 2@ at-xy .(apple) ;
  \ Display the apple.

: coords+ ( n1 n2 col1 row1 -- col2 row2 ) rot + -rot + swap ;
  \ Update coordinates _col1 row1_ with direction _n1 n2_,
  \ resulting coordinates _col2 row2_.

: move-head ( -- ) head> @ 1- max-length mod head> ! ;

: (crawl) ( n1 n2 -- )
  head 2@ 2dup previous-head 2! move-head coords+ head 2! ;
  \ Make the snake crawl in direction _n1 n2_.

: crawl ( -- ) direction 2@ (crawl) ;
  \ Make the snake crawl in the current direction.

-1  0 2constant left
  \ Left direction (coordinate increments).

 1  0 2constant right
  \ Right direction (coordinate increments).

 0  1 2constant down
  \ Down direction (coordinate increments).

 0 -1 2constant up
  \ Up direction (coordinate increments).

: at-arena? ( col row -- f ) arena-y arena-max-y between swap
                             arena-x arena-max-x between and ;
  \ Are coordinates _col row_ at the arena?

: wall? ( -- f ) head 2@ at-arena? 0= ;
  \ Has the snake hit the wall?

: crush ( -- ) previous-head 2@ at-xy crush-attr ?attr! ." X" ;

: crossing? ( -- f )
  length @ 1 ?do  i segment cross?  if unloop true exit then
             loop false ;
  \ Is the snake crossing itself?

: apple? ( -- f ) head apple clash? ;
  \ Has the snake found the apple?

: crush? ( -- f ) wall? crossing? or ;

: .horizontal-wall ( -- ) wall-max-x 1+ wall-x ?do ." +" loop ;

: .wall ( -- )
  wall-attr ?attr!
  wall-x wall-y at-xy .horizontal-wall
  arena-max-y 1+ arena-y ?do wall-x     i at-xy ." +"
                             wall-max-x i at-xy ." +" loop
  wall-x wall-max-y at-xy .horizontal-wall ;
  \ Display the wall.

: .head ( -- ) head 2@ at-xy snake-attr ?attr! ." O" ;
  \ Display the head of the snake.

variable swallow swallow off
  \ Flag: has the snake just swallowed an apple?

: .neck ( -- ) neck 2@ at-xy snake-attr ?attr!
               swallow @ if   .(apple) swallow off
                         else ." o" then ;
  \ Display the "neck" of the snake, ie. its second segment.

: -tail ( -- ) tail 2@ at-xy space ;
  \ Delete the tail of the snake.

: .snake+ ( -- ) .head .neck -tail unhome ;
  \ Display the snake updated, ie. only the parts that change
  \ during the crawling.

: .snake ( -- )
  .head length @ 1 ?do i segment 2@ at-xy ." o" loop ;
  \ Display the whole snake.

: -status ( -- ) status-attr ?attr!
                 status-x status-y at-xy status-cols spaces ;
  \ Clear the status bar.

: .status ( -- ) -status .score$ .score .record$ .record ;
  \ Display the status bar.

: init-arena ( -- ) arena-attr ?attr! page
                    .wall .status .apple .snake ;
  \ Init the arena.

: init-max-length ( -- ) (max-length) to max-length ;
  \ Init the maximum length of the snake, depending on the
  \ current size of the screen.

: new-snake ( -- )
  init-max-length  initial-length length !
  head> off  cols 2/ rows 2/ head 2!
  up direction 2!  length @ 0 ?do crawl loop ;
  \ Create a new snake with default values (length, position
  \ and direction).

: init-delay ( -- ) initial-delay delay ! ;
  \ Init the delay.

: init ( -- )
  score off init-delay new-snake new-apple init-arena ;
  \ Init the game.

: dodge ( -- ) score ?1-! .score ;
  \ Decrement the score because of the dodge.

: dodge? ( n1 n2 -- ) direction 2@ rot + -rot + or 0<> ;
  \ Do new direction _n1 n2_ causes a dodge, ie. does it
  \ change the current direction to the left or to the right?

: ?dodge ( n1 n2 -- ) dodge? if dodge then ;
  \ Manage a possible dodge caused by new direction _n1 n2_.

: new-direction ( n1 n2 -- ) 2dup ?dodge direction 2! ;
  \ Set new direction _n1 n2_.

k-down  value down-key
k-left  value left-key
k-right value right-key
k-up    value up-key
bl      value pause-key
  \ Control keys.

: pause ( -- ) begin
                 begin ekey?           until false
                 begin drop ekey ekey> until
                 pause-key =
               until ;
  \ Stop the game until the pause key is pressed again.

: manage-key ( x -- )
  case
    down-key  of down  new-direction endof
    left-key  of left  new-direction endof
    right-key of right new-direction endof
    up-key    of up    new-direction endof
    pause-key of pause               endof
   endcase ;
   \ If _x_ is a supported key, manage it.  _x_ can be
   \ a keypress, a character or an extended character.

: (rudder) ( x -- ) ekey> if manage-key else drop then ;
  \ If keyboard event _x_ i a valid one (a keypress, a
  \ character or an extended character), manage it.

: rudder ( -- ) ekey? if ekey (rudder) then ;
  \ If a keyboard event is available, manage it.

: lazy ( -- ) delay @ ms ;
  \ Wait the current delay.

: faster ( -- ) delay @ acceleration - 0 max delay ! ;
  \ Decrement the delay.

: eaten ( -- ) swallow on 10 score +! .score ;
  \ Increase and display the score.

: eat ( -- ) eaten grow faster new-apple .apple ;
  \ Eat the apple.

: ?eat ( -- ) apple? if eat then ;
  \ If the apple is found, eat it.

: center-y ( -- row ) rows 2/ ;
  \ Calculate Y coordinate _row_ of the center of the
  \ screen.

: center-x ( len -- col ) cols swap - 2/ ;
  \ Calculate X coordinate _col_ of the center of the
  \ screen, for a string lenght _len_.

: center-xy ( len -- col row ) center-x center-y ;
  \ Calculate coordinates _col row_ of the center of the
  \ screen, for a string lenght _len_.

: at-center-xy ( len -- ) center-xy at-xy ;
  \ Set cursor at the center of the screen, for a string lenght
  \ _len_.

: type-center ( ca len -- ) dup at-center-xy type ;
  \ Display string _ca len_ at the center of the screen.

: +type-center ( ca len n -- )
  >r dup center-xy r> + at-xy type ;
  \ Display string _ca len_ at the center of the screen,
  \ but adding row offset _n_.

: update-record ( -- ) score @ record @ max record ! .record ;

: game-over ( -- )
  update-record
  s" **** GAME OVER **** " type-center unhome
  500 ms -keys key drop ;

: (game) ( -- ) .snake+ lazy rudder crawl ?eat ;
  \ Game cycle.

: game ( -- ) begin (game) crush? until crush game-over ;
  \ Game loop.

: version$ ( -- ca len ) s" Version " version s+ ;

: title$ ( -- ca len ) s" ooO SERPENTINO Ooo" ;

: author$ ( -- ca len ) s" programandala.net" ;

: splash-screen ( -- ) text-attr ?attr!
                       page title$ -2 +type-center
                            version$   type-center
                            author$ 2 +type-center unhome
                       2000 ms ;
  \ Display the splash screen.

: run ( -- ) begin splash-screen init game again ;
  \ Main, endless loop.

cr cr .( Type RUN to start) cr cr \ XXX TMP --

run

\ =============================================================
\ Debugging tools

false [if]

: test-on-snake ( -- )
  init
  begin key? if key bl = if exit then then
        random-xy 2dup at-xy segment? if   ." X" key drop
                                      else ." ." then
  again ;

[then]

\ =============================================================
\ Change log

\ 2017-11-22: Fork from Robert Pfeiffer's forthsnake
\ (https://github.com/robertpfeiffer/forthsnake). Change source
\ style.  Rename words. Factor. Use constants and variables.
\ Use full screen. Draw the head apart. Document. Simplify
\ handling of directions. Remove flickering. Accelerate the
\ drawing of the snake. Add an actual scoring.
\
\ 2017-11-23: Add record. Improve the scoring calculation.
\ Improve the keyboard handling to support non-movement keys.
\ Add a pause key.