Lets play a game

Lets play a game on the PicoCalc. Here is my version of the classic Snake game.

Here is the code if anyone wants to have a look, or use it as a guide to translate it to something else.


/*
 * snake:
 *      My version of this classic game.
 *      Translated from my RTB Basic version...
 *      Gordon Henderson, 2026
 *********************************************************************************
 */

GET "libhdr"
GET "vdu"
GET "sys"
GET "rubyKeys"

GLOBAL
{
  snakeX: ug
  snakeY
  xSpeed ; ySpeed
  foodX ; foodY
  snakeDia ; snakeRad ; snakeRad3
  numSegs
  gWidth ; gHeight
  tWidth ; tHeight
  maxFood
  quickTick ; slowTick
  score 
  done
}

MANIFEST
{

// How many snake segments can fit on the screen?

  segsWide = 25
  segsHigh = 25

// Ticker times
//      The quick timer is for more food, the slow timer makes us shorter...

  quickTickTime  = 4000 // mS
  slowTickTime   = 9000
  movesPerSecond = 10
  snakeSpeed     =  1000/movesPerSecond
}

/*
 * center:
 *      Display centered text
 *********************************************************************************
 */

LET center (s) BE
{
  FOR i = 1 TO tWidth / 2 - s%0 / 2
    sawrch (' ')
  sawrites (s)
}


/*
 * getSpace:
 *      Wait for space to be typed
 *********************************************************************************
 */

AND getSpace () BE
{
  vduInk (colWhite)
  center ("Press the SPACE bar when ready ")
  {} REPEATUNTIL sardch () = ' '
}


AND snakeDie (message) BE
{
  vduXY (4,0)
  vduInk (colRed)
  sawrites (message)
  vduXY (0,tHeight-1)
  getSpace ()
  done := TRUE
}


/*
 * updateSnake:
 *      Move it along to the next location in our playing field
 ********************************************************************************
 */

AND updateSnake () BE
{
  LET x0 = snakeX!0 + xSpeed
  LET y0 = snakeY!0 + ySpeed

  FOR i = numSegs - 1 TO 1 BY -1 DO
  {
    snakeX!i := snakeX!(i-1)
    snakeY!i := snakeY!(i-1)
  }
  snakeX!0 := x0
  snakeY!0 := y0
}


/*
 * longer: shorter:
 *      Add or remove length to the snake
 *********************************************************************************
 */

AND longer (segments) BE
{
  LET new = numSegs + segments
  FOR i = numSegs TO new DO
  {
    snakeX!i := snakeX!(numSegs-1)
    snakeY!i := snakeY!(numSegs-1)
  }
  numSegs := new
}

AND shorter (segments) BE
{
  numSegs := numSegs - segments

  IF numSegs > 1 THEN
    RETURN

  snakeDie ("Your snake has starved to death!")
}


/*
 * checkFood:
 *      Eat food (if we land on it!)
 *********************************************************************************
 */

AND checkFood () BE
{
  LET x = snakeX!0
  LET y = snakeY!0
  FOR i = 1 TO maxFood DO
  {
    IF (foodX!i = x) & (foodY!i = y) THEN
    {
      longer (3)
      foodX!i := 0 // burp
      BREAK
    }
  }
}


/*
 * moreFood:
 *      See if we can put more food on the table
 *********************************************************************************
 */

AND moreFood () BE
{
  LET store = 0
  LET hit = ?
  LET fx, fy = ?, ?

  FOR i = 1 TO maxFood DO
    IF foodX!i = 0 THEN
    {
      store := i
      BREAK
    }

  IF store = 0 THEN
    RETURN      // Too much food on the table

// find a free slot
//     ie. don't land food on the snake or a rock

  FOR i = 1 TO 5 DO     // Only try 5 times, then give up
  {
    fx  := (randno (segsWide - 2) + 1) * snakeDia
    fy  := (randno (segsHigh - 2) + 1) * snakeDia
    hit := FALSE
    FOR j = 0 TO numSegs - 1 DO
      IF (snakeX!j = fx) & (snakeY!j = fy) THEN hit := TRUE
    UNLESS hit THEN BREAK
  }

  IF hit THEN RETURN    // Oops. Can't find space!

// Store location

  foodX!store := fx
  foodY!store := fy
}


/*
 * drawSnake:
 *      With some imagination...
 *********************************************************************************
 */

LET drawSnake () BE
{
// Tongue
//      Lime greeen...

  vduGCol (colLime)
  TEST xSpeed = 0 THEN          // Moving Up or Down
    vduEllipse (snakeX!0, snakeY!0, snakeRad / 4, snakeRad * 2, TRUE)
  ELSE                          // Moving Left or Right
    vduEllipse (snakeX!0, snakeY!0, snakeRad * 2, snakeRad / 3, TRUE)

// Head
//      Red head with yellow dot inside

  vduGCol (colRed)
  vduCircle (snakeX!0, snakeY!0, snakeRad, 1)
  vduGCol (colYellow)
  vduCircle (snakeX!0, snakeY!0, snakeRad / 2, 1)

// Body

  FOR i = 1 TO numSegs - 1 DO
  {
    vduGCol (colYellow)
    vduCircle (snakeX!i, snakeY!i, snakeRad, 1)
    vduGCol (colOlive)
    vduCircle (snakeX!i, snakeY!i, snakeRad * 3 / 4, 1)
  }
}


/*
 * drawFood:
 *      Draw the food on the display
 *********************************************************************************
 */

AND drawFood () BE
{
  FOR i = 0 TO maxFood DO
  {
    LET fx = foodX!i
    LET fy = foodY!i
    UNLESS fx = 0 THEN
    {
      vduGCol (colRed)
      vduCircle (fx, fy, snakeRad * 8 / 10, TRUE)
      vduGCol (colLime)
      vduCircle (fx - snakeRad3, fy + snakeRad3, snakeRad3, 1)
      vduCircle (fx + snakeRad3, fy + snakeRad3, snakeRad3, 1)
    }
  }
}


/*
 * drawBorder: drawScene:
 *      Draw the details on a blank canvass
 *********************************************************************************
 */

AND drawBorder () BE
{
  vduGCol (colPink)
  vduRectangle (4,4, gWidth-8, gHeight-8,   FALSE)
  vduRectangle (5,5, gWidth-10, gHeight-10, FALSE)
}

AND drawScene () BE
{
  vduCls ()
  drawBorder () 
  drawSnake  () 
  drawFood   () 
  vduUpdate  () 
}


/*
 * collided:
 *      Did we hit something? Either ourself, or a wall
 *********************************************************************************
 */

AND collided () = VALOF
{
  LET x0 = snakeX!0     // Check the head
  LET y0 = snakeY!0

// Wall?

  IF (x0 = 0) | (y0 = 0)                        |
        ((x0 / snakeDia) = segsWide + 1)        |
        ((y0 / snakeDia) = segsHigh + 1) THEN
  {
    snakeDie ("Your snake hit the wall!")
    RESULTIS TRUE
  }

// Myself?

  FOR i = 1 TO numSegs-1 DO
    IF (snakeX!i = x0) & (snakeY!i = y0) THEN
    {
      snakeDie ("Your snake bit itself!")
      RESULTIS TRUE
    }

  RESULTIS FALSE
}


/*
 * mainGameLoop:
 *      Is where is all happens
 *********************************************************************************
 */

AND mainGameLoop () BE
{
  LET key = ?
  LET now, then = ?,?

  then := sys (Sys_cputime)

  drawScene ()
  
  key := sapollrdch ()
  UNLESS key < 0 THEN
  {
    SWITCHON key INTO
    {
      CASE KEY_ARROW_UP:
        xSpeed :=  0
        ySpeed :=  snakeDia
      ENDCASE

      CASE KEY_ARROW_DOWN:
        xSpeed :=  0
        ySpeed := -snakeDia
      ENDCASE

      CASE KEY_ARROW_LEFT:
        xSpeed := -snakeDia
        ySpeed :=  0
      ENDCASE

      CASE KEY_ARROW_RIGHT:
        xSpeed :=  snakeDia
        ySpeed :=  0
      ENDCASE

      CASE 'q': CASE 'Q':
        done := TRUE
        BREAK
      ENDCASE

// Debug/Cheat?

      CASE ' ':
        longer (1)
      ENDCASE
    }
  }

// Basic checks

  checkFood   ()
  updateSnake ()

// Collided?

  IF collided () THEN
    BREAK

// Quick ticker?

  IF sys (Sys_cputime) > quickTick THEN
  {
    moreFood (1)
    quickTick := quickTick + quickTickTime
  }

// Slow ticker?
//      Snake can die here, so check at the end, or insert a BREAK...

  IF sys (Sys_cputime) > slowTick THEN
  {
    shorter (1)
    slowTick := slowTick + slowTickTime
  }

// We really don't want to move the snake more (or less) than 10 times a second, so
//      Somewhat crudely ...

  now := sys (Sys_cputime)

  sys (Sys_delay, snakeSpeed - (now - then))

} REPEATUNTIL done


/*
 * reset:
 *      Reset for a new game
 *********************************************************************************
 */

AND reset () BE
{
  vduCls ()

// Initialise the snake with 5 segments

  numSegs := 5
  FOR i = 0 TO numSegs - 1 DO
  {
    snakeX!i := segsWide / 2 * snakeDia + i * snakeDia
    snakeY!i := segsHigh / 2 * snakeDia
  }

// Start going Left

  xSpeed :=  -snakeDia ; ySpeed := 0

/***
  -- Need to re-position snake if we do this
// Random direction

  SWITCHON randno (4) INTO
  {
    CASE 0: xSpeed :=  -snakeDia ; ySpeed :=         0 ; ENDCASE        // Left
    CASE 1: xSpeed :=   snakeDia ; ySpeed :=         0 ; ENDCASE        // Right
    CASE 2: xSpeed :=          0 ; ySpeed := -snakeDia ; ENDCASE        // Up
    CASE 3: xSpeed :=          0 ; ySpeed :=  snakeDia ; ENDCASE        // Down
  }
****/

// Food

  FOR i = 0 TO maxFood DO
  {
    foodX!i := 0
    foodY!i := 0
  }

// Reset timers, scores

  quickTick := sys (Sys_cputime) + quickTickTime
  slowTick  := sys (Sys_cputime) +  slowTickTime

  score := 0
  done  := FALSE
}


/* 
 * setup:
 *      Initial setup, allocate memory, set globals, etc.
 *********************************************************************************
 */

AND setup () = VALOF
{
  gWidth  := vduProps!vduProps_gWidth
  gHeight := vduProps!vduProps_gHeight
  tWidth  := vduProps!vduProps_tWidth
  tHeight := vduProps!vduProps_tHeight

// How big to make the sname segments?

  snakeDia  := gWidth / segsWide
  snakeRad  := snakeDia / 2
  snakeRad3 := snakeRad / 3

// If we need to save RAM we could move to byte arrays rather than words..

  snakeX := getvec (segsWide * segsHigh) ;  UNLESS snakeX THEN RESULTIS FALSE
  snakeY := getvec (segsWide * segsHigh) ;  UNLESS snakeX THEN RESULTIS FALSE

  maxFood := (segsWide * segsHigh / 200)

  foodX := getvec (maxFood) ; UNLESS foodX THEN RESULTIS FALSE
  foodY := getvec (maxFood) ; UNLESS foodY THEN RESULTIS FALSE

  RESULTIS TRUE
}

/*
 * printTitle
 *      Print the title and intro page
 *********************************************************************************
 */

AND printTitle () BE
{
  vduCls ()
  vduXY (tWidth / 2 - 5, 4)
  vduInk (colRed)    ; sawrites ("S ")
  vduInk (colYellow) ; sawrites ("N ")
  vduInk (colRed)    ; sawrites ("A ")
  vduInk (colYellow) ; sawrites ("K ")
  vduInk (colRed)    ; sawrites ("E")
  vduXY (tWidth / 2 - 5, 5)
  vduInk (colYellow)
  sawrites ("=========")
}


/*
 * welcome
 *      Splash screen and instructions
 *********************************************************************************
 */

AND welcome () BE
{
  LET key = ?

  vduInk (colWhite)

  center ("*n")
  center ("Instructions ? ")

  key := sardch ()
    REPEATUNTIL (key = 'y') | (key = 'n')

  TEST key = 'y' THEN
  {
    sawritef ("Yes*n")

// For 40-column PicoCalc
//                     1         2         3
//           0123456789012345678901234567890123456789
    center ("*n")
    center ("Control the snake with the arrow keys*n")
    center ("Use the UP, DOWN, LEFT and RIGHT keys*n")
    center ("to change the snakes direction.*n")
    center ("*n")
    center ("Look for food: Raspberry-like things*n")
    center ("but avoid rocks!*n")
    center ("*n")
    center ("Food will make the snake grow,  but if*n")
    center ("you don't feed it, then it will shrink*n")
    center ("and die.*n")
    center ("*n")
    center ("The longer the snake...*n")
    center ("... the more points you get.*n")
    center ("*n")
  }
  ELSE
    sawritef ("No*n*n")

  getSpace () 
}



/*
 * start:
 *      Where we begin
 *********************************************************************************
 */

AND start () = VALOF
{
  LET key = ?

  UNLESS setup () THEN
  {
    writef ("snake: Setup faile - out of memory*n")
    RESULTIS 1
  }

  printTitle ()
  welcome    ()

// Outer Game loop

  {
    {
      reset        ()
      drawScene    () 
      mainGameLoop ()
    } REPEATUNTIL done

// Score is the snake length

    vduXY (3,tHeight-1)
    vduDeleteEOL ()
    vduInk (colLime)
    sawritef (" Score: %3d. Again? ", numSegs)
    vduInk (colWhite)
    key := sardch ()
      REPEATUNTIL (key = 'y') | (key = 'n')

  } REPEATUNTIL key = 'n'

  sawrites ("No.*n")

  freevec (snakeX)
  freevec (snakeY)
  freevec (foodX)
  freevec (foodY)

  RESULTIS 0
}

Cheers,

-G

4 Likes

Here is a collection of games for zeptoforth on the PicoCalc:

Snake at zeptoforth/test/rp_common/picocalc_snake_keys_enhanced.fs at master · tabemann/zeptoforth · GitHub

\ Copyright (c) 2025-2026 Travis Bemann
\ 
\ Permission is hereby granted, free of charge, to any person obtaining a copy
\ of this software and associated documentation files (the "Software"), to deal
\ in the Software without restriction, including without limitation the rights
\ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\ copies of the Software, and to permit persons to whom the Software is
\ furnished to do so, subject to the following conditions:
\ 
\ The above copyright notice and this permission notice shall be included in
\ all copies or substantial portions of the Software.
\ 
\ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
\ SOFTWARE.

\ The controls are:
\ 
\ Up: Go up
\ Down: Go down
\ Right: Go right
\ Left: Go left
\ S: Take a screenshot
\ Q: Give up in shame

begin-module snake
  
  oo import
  picocalc-term import
  picocalc-keys import
  picocalc-screenshot import
  pixmap8 import
  st7365p-8-common import
  tinymt32 import
  
  $B4 constant LEFT_ARROW
  $B5 constant UP_ARROW
  $B6 constant DOWN_ARROW
  $B7 constant RIGHT_ARROW
  
  tinymt32-size buffer: prng
  
  : init-prng ( -- )
    rng::random prng tinymt32-init
    prng tinymt32-prepare-example
  ;
  
  initializer init-prng
  
  : random { x -- u }
    x s>f prng tinymt32-generate-uint32 0 f* round-half-zero
    x min
  ;

  8 constant cell-width
  8 constant cell-height
  
  term-pixels-dim@
  cell-height / constant world-height
  cell-width / constant world-width
  
  world-width world-height * constant max-snake-len
  
  <object> begin-class <food>
  
    cell member food-count
    world-width world-height * 8 align 8 / cell align
    member food-bits
    
    method food-count@ ( food -- count )
    method create-food ( x y food -- )
    method eat-food ( x y food -- )
    method food-at? ( x y food -- at? )
    
  end-class
  
  <food> begin-implement
    
    :noname { self -- }
      self <object>->new
      self food-bits [ world-width world-height * 8 align 8 / ]
      literal 0 fill
      0 self food-count !
    ; define new
    
    :noname ( self -- count ) food-count @ ; define food-count@
    
    :noname { x y self -- }
      x y self food-at? not if
        1 self food-count +!
        y world-width * x + { index }
        index 7 and bit index 3 rshift self food-bits + cbis!
      then
    ; define create-food
    
    :noname { x y self -- }
      x y self food-at? if
        -1 self food-count +!
        y world-width * x + { index }
        index 7 and bit index 3 rshift self food-bits + cbic!
      then
    ; define eat-food
    
    :noname { x y self -- at? }
      y world-width * x + { index }
      index 7 and bit index 3 rshift self food-bits + cbit@
    ; define food-at?
    
  end-implement
  
  4 constant min-snake-len
  2 constant snake-len-incr
  
  0 constant none
  1 constant up
  2 constant down
  3 constant left
  4 constant right
  
  : opposite-dir ( dir -- dir' )
    case
      none of none endof
      up of down endof
      down of up endof
      left of right endof
      right of left endof
    endcase
  ;
  
  : adjust-coord ( x y dir -- x' y' )
    case
      up of 1- endof
      down of 1+ endof
      left of swap 1- swap endof
      right of swap 1+ swap endof
    endcase
  ;
  
  : in-bounds? { x y -- in-bounds? }
    x 0>= y 0>= and x world-width < and y world-height < and
  ;
  
  <object> begin-class <snake>
  
    cell member snake-dir
    cell member snake-head-index
    cell member snake-tail-index
    cell member snake-target-len
    max-snake-len cell align member snake-x
    max-snake-len cell align member snake-y
    
    method extend-snake ( dir snake -- alive? )
    method grow-snake ( snake -- )
    method shorten-snake ( snake -- )
    method push-snake-head ( x y snake -- )
    method drop-snake-tail ( snake -- )
    method snake-head@ ( snake -- x y )
    method snake-at? ( x y snake -- at? )
    method snake-len@ ( snake -- len )
    
  end-class
  
  <snake> begin-implement
    
    :noname { x y self -- }
      self <object>->new
      none self snake-dir !
      0 self snake-head-index !
      0 self snake-tail-index !
      min-snake-len self snake-target-len !
      x self snake-x c!
      y self snake-y c!
    ; define new
    
    :noname { dir self -- alive? }
      dir none = if self snake-dir @ to dir then
      dir none <> if
        dir opposite-dir self snake-dir @ = if
          self snake-dir @ to dir
        then
        dir self snake-dir !
        self snake-head@ dir adjust-coord { x y }
        x y in-bounds? if
          x y self snake-at? not if
            x y self push-snake-head true
          else
            false
          then
        else
          false
        then
      else
        true
      then
    ; define extend-snake
    
    :noname { self -- }
      snake-len-incr self snake-target-len +!
    ; define grow-snake
    
    :noname { self -- }
      self snake-len@ self snake-target-len @ > if
        self drop-snake-tail
      then
    ; define shorten-snake
    
    :noname { x y self -- }
      self snake-head-index @ 1+ max-snake-len umod
      dup { index } self snake-head-index !
      x self snake-x index + c!
      y self snake-y index + c!
    ; define push-snake-head
    
    :noname { self -- }
      self snake-tail-index @ { index }
      index self snake-head-index @ <> if
        index 1+ max-snake-len umod self snake-tail-index !
      then
    ; define drop-snake-tail
    
    :noname { self -- x y }
      self snake-head-index @ { index }
      self snake-x index + c@
      self snake-y index + c@
    ; define snake-head@
    
    :noname { x y self -- at? }
      self snake-tail-index @ { index }
      self snake-head-index @ { head-index }
      begin
        self snake-x index + c@ x =
        self snake-y index + c@ y = and if true exit then
        index head-index =
        index 1+ max-snake-len umod to index
      until
      false
    ; define snake-at?
    
    :noname { self -- len }
      self snake-head-index @ { head-index }
      self snake-tail-index @ { tail-index }
      head-index
      tail-index head-index > if max-snake-len + then
      tail-index - 1+
    ; define snake-len@
    
  end-implement
  
  4 constant init-food-count
  100 constant food-chance \ Actually the reciprocal
  0 255 0 rgb8 constant body-color
  255 255 0 rgb8 constant head-color
  255 0 0 rgb8 constant food-color
  
  <object> begin-class <world>
    
    <snake> class-size member the-snake
    <food> class-size member the-food
    
    method cycle-world ( dir world -- continue? )
    method draw-world ( world -- )
    method create-random-food ( world -- )
    
  end-class
  
  <world> begin-implement
  
    :noname { self -- }
      self <object>->new
      world-width 1- random world-height 1- random
      <snake> self the-snake init-object
      <food> self the-food init-object
      init-food-count 0 ?do self create-random-food loop
    ; define new
    
    :noname { dir self -- continue? }
      dir self the-snake extend-snake if
        self the-snake snake-head@ { x y }
        x y self the-food food-at? not if
          self the-snake shorten-snake
        else
          x y self the-food eat-food
          self the-snake grow-snake
        then
        food-chance random 0=
        self the-food food-count@ 0= or if
          self create-random-food
        then
        true
      else
        false
      then
    ; define cycle-world
    
    :noname ( self -- )
      [: { self display -- }
        display clear-pixmap
        world-height 0 ?do
          world-width 0 ?do
            0 { color }
            i j self the-snake snake-at? if
              self the-snake snake-head@ j = swap i = and if
                head-color
              else
                body-color
              then
              to color
            else
              i j self the-food food-at? if
                food-color to color
              then
            then
            color ?dup if
              i cell-width * j cell-height *
              cell-width cell-height
              display draw-rect-const
            then
          loop
        loop
        display update-display
      ;] with-term-display
    ; define draw-world
    
    :noname { self -- }
      begin
        world-width 1- random { x }
        world-height 1- random { y }
        x y self the-snake snake-at? not
        x y self the-food food-at? not and dup if
          x y self the-food create-food
        then
      until
    ; define create-random-food
    
  end-implement
  
  $1B constant escape
  
  : empty-keys ( -- ) begin key? while key drop repeat ;

  : handle-screenshot ( -- )
    [:
      screenshot-fs@ { fs }
      fs if
        screenshot-path@ fs ['] take-screenshot try-and-display-error 0<> if
          drop 2drop
        then
      then
    ;] console::with-serial-error-output
  ;
  
  : handle-key ( -- dir exit? )
    reset-keymap
    update-keymap
    [char] q keymap-pressed@ if none true exit then
    [char] s keymap-released@ if handle-screenshot then
    none 0
    UP_ARROW keymap-pressed@ if 1+ nip up swap then
    DOWN_ARROW keymap-pressed@ if 1+ nip down swap then
    RIGHT_ARROW keymap-pressed@ if 1+ nip right swap then
    LEFT_ARROW keymap-pressed@ if 1+ nip left swap then
    1 <> if drop none then
    false
  ;
  
  1875 constant snake-delay-ticks
  
  : play-snake ( -- )
    <world> [: { the-world }
      ansi-term::hide-cursor
      true raw-keys-enabled!
      clear-keymap
      page
      [: dup clear-pixmap update-display ;] with-term-display
      the-world draw-world
      begin
        handle-key not if
          systick::systick-counter { start-systick }
          the-world cycle-world { alive? }
          the-world draw-world
          start-systick snake-delay-ticks
          task::current-task task::delay
          alive? not
        else
          drop true
        then
      until
      ." *** GAME OVER ***" cr
      false raw-keys-enabled!
      clear-keymap
      1000 ms
      empty-keys
      ansi-term::show-cursor
    ;] with-object
  ;

end-module

Rocks with Recoil at zeptoforth/test/rp2350/picocalc_rocks_recoil.fs at master · tabemann/zeptoforth · GitHub

\ Copyright (c) 2025-2026 Travis Bemann
\ 
\ Permission is hereby granted, free of charge, to any person obtaining a copy
\ of this software and associated documentation files (the "Software"), to deal
\ in the Software without restriction, including without limitation the rights
\ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\ copies of the Software, and to permit persons to whom the Software is
\ furnished to do so, subject to the following conditions:
\ 
\ The above copyright notice and this permission notice shall be included in
\ all copies or substantial portions of the Software.
\ 
\ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
\ SOFTWARE.

\ The controls are:
\ 
\ Up: Thrust
\ Right: Turn right
\ Left: Turn left
\ Space: Fire
\ S: Take a screenshot
\ Q: Give up in shame

begin-module rocks
  
  oo import
  picocalc-term import
  picocalc-sound import
  picocalc-keys import
  picocalc-screenshot import
  pixmap8 import
  pixmap8-utils import
  font import
  st7365p-8-common import
  float32 import
  tinymt32 import
  
  $B4 constant LEFT_ARROW
  $B5 constant UP_ARROW
  $B6 constant DOWN_ARROW
  $B7 constant RIGHT_ARROW

  tinymt32-size buffer: prng
  
  : init-prng ( -- )
    rng::random prng tinymt32-init
    prng tinymt32-prepare-example
  ;
  
  initializer init-prng
  
  : urandom ( -- u ) prng tinymt32-generate-uint32 ;
  
  : vrandom ( -- v ) urandom 0 f64>v ;
  
  16e0 constant border-width
  16e0 constant border-height
  320e0 constant arena-width
  320e0 constant arena-height
  
  : convert-coord { x y -- x' y' }
    x v>n arena-height y v- v>n
  ;
  
  0 0 0 rgb8 constant bk-color
  255 255 0 rgb8 constant info-color
  
  16 constant info-x
  16 constant info-y
  
  180e0 constant base-shot-speed
  -2e0 constant recoil-speed
  8e0 vpi v* constant ship-heading-decel
  8e0 constant ship-body-radius
  16e0 constant ship-dir-radius
  0 255 0 rgb8 constant ship-color
  40000 constant wave-start-ticks
  
  variable wave
  variable lives
  variable wave-start
  variable wave-start-systick
  
  : render-label ( color addr bytes n x y display -- )
    256 [: { color addr bytes n x y display buffer }
      addr buffer bytes move
      n s>d <# #s #> dup { bytes' } buffer bytes + swap move
      color buffer bytes bytes' + x y display term-font@
      draw-string-to-pixmap8
    ;] with-allot
  ;
  
  : render-info { color display -- }
    color s" Wave:  " wave @ info-x info-y display render-label
    color s" Lives: " lives @ 0 max
    info-x info-y term-font@ char-rows @ +
    display render-label
  ;
  
  : draw-info { display -- } info-color display render-info ;
  
  : erase-info { display -- } bk-color display render-info ;
   
  <object> begin-class <entity>
  
    cell member entity-active
    cell member entity-x
    cell member entity-y
    cell member entity-delta-x
    cell member entity-delta-y
    
    method entity-active? ( entity -- active? )
    method entity-coord@ ( entity -- x y )
    method entity-delta@ ( entity -- delta-x delta-y )
    method activate-entity ( x y delta-x delta-y entity -- )
    method deactivate-entity ( entity -- )
    method adjust-entity-delta ( adjust-x adjust-y entity -- )
    method update-entity ( interval entity -- )
    method draw-entity ( display entity -- )
    method erase-entity ( display entity -- )
    method do-update-entity ( interval entity -- )
    method do-draw-entity ( display entity -- )
    method do-erase-entity ( display entity -- )
    
  end-class
  
  <entity> begin-implement
  
    :noname { self -- }
      self <object>->new
      false self entity-active !
      0e0 self entity-x !
      0e0 self entity-y !
      0e0 self entity-delta-x !
      0e0 self entity-delta-y !
    ; define new
    
    :noname { self -- active? }
      self entity-active @
    ; define entity-active?
    
    :noname { self -- x y }
      self entity-x @ self entity-y @
    ; define entity-coord@
    
    :noname { self -- delta-x delta-y }
      self entity-delta-x @ self entity-delta-y @
    ; define entity-delta@
    
    :noname { x y delta-x delta-y self -- }
      true self entity-active !
      x self entity-x !
      y self entity-y !
      delta-x self entity-delta-x !
      delta-y self entity-delta-y !
    ; define activate-entity
    
    :noname { self -- }
      false self entity-active !
      0e0 self entity-x !
      0e0 self entity-y !
      0e0 self entity-delta-x !
      0e0 self entity-delta-y !
    ; define deactivate-entity
    
    :noname { adjust-x adjust-y self -- }
      self entity-delta-x @ adjust-x v+
      self entity-delta-x !
      self entity-delta-y @ adjust-y v+
      self entity-delta-y !
    ; define adjust-entity-delta
    
    :noname { interval self -- }
      self entity-active? if
        interval self do-update-entity
      then
    ; define update-entity
    
    :noname { display self -- }
      self entity-active? if display self do-draw-entity then
    ; define draw-entity
    
    :noname { display self -- }
      self entity-active? if display self do-erase-entity then
    ; define erase-entity
    
    :noname { interval self -- }
      self entity-x @
      self entity-delta-x @ interval v* v+ { x }
      self entity-y @
      self entity-delta-y @ interval v* v+ { y }
      x border-width vnegate v< if
        x arena-width v+ border-width v+ to x
      then
      x arena-width border-width v+ v> if
        x arena-width v- border-width v- to x
      then
      y border-height vnegate v< if
        y arena-height v+ border-height v+ to y
      then
      y arena-height border-height v+ v> if
        y arena-height v- border-height v- to y
      then
      x self entity-x !
      y self entity-y !
    ; define do-update-entity
    
    :noname { display self -- } ; define do-draw-entity
    
    :noname { display self -- } ; define do-erase-entity
    
  end-implement
  
  <entity> begin-class <shootable>
    
    method shootable-radius@ ( shootable -- radius )
    method try-shoot ( x y shootable -- hit? )
    method do-hit ( shootable -- )
  
  end-class
  
  <shootable> begin-implement
    
    :noname { self -- radius } 0e0 ; define shootable-radius@
    
    :noname { shot-x shot-y self -- hit? }
      self entity-active? if
        self entity-coord@ { x y }
        self shootable-radius@ { radius }
        shot-x x v- dup v* shot-y y v- dup v* v+
        radius dup v* v<= if self do-hit true else false then
      else
        false
      then
    ; define try-shoot
    
    :noname { self -- } ; define do-hit
    
  end-implement
  
  16e0 constant max-asteroid-radius
  3 constant max-divide-count
  max-asteroid-radius 4e0 v/ constant min-asteroid-radius
  20e0 constant max-asteroid-speed
  
  <shootable> begin-class <asteroid>
  
    cell member asteroid-radius
    
    method do-spawn-asteroid ( radius x y asteroid -- )
    method render-asteroid ( color display asteroid -- )
    method check-collide ( x y asteroid -- collide? )
    
  end-class
  
  120 constant max-asteroid-count
  max-asteroid-count <asteroid> class-size * buffer: asteroids
  
  : asteroid@ ( index -- asteroid )
    <asteroid> class-size * asteroids +
  ;
  
  : spawn-asteroid { radius x y -- }
    max-asteroid-count 0 ?do
      i asteroid@ { asteroid }
      asteroid entity-active? not if
        radius x y asteroid do-spawn-asteroid exit
      then
    loop
  ;
  
  : start-asteroid ( -- )
    max-asteroid-radius
    arena-width vrandom v* arena-height vrandom v*
    spawn-asteroid
  ;
  
  : init-asteroids ( -- )
    max-asteroid-count 0 ?do
      <asteroid> i asteroid@ init-object
    loop
  ;
  
  : deactivate-asteroids ( -- )
    max-asteroid-count 0 ?do
      i asteroid@ deactivate-entity
    loop
  ;
  
  : update-asteroids { interval -- }
    max-asteroid-count 0 ?do
      interval i asteroid@ update-entity
    loop
  ;
  
  : draw-asteroids { display -- }
    max-asteroid-count 0 ?do
      display i asteroid@ draw-entity
    loop
  ;
  
  : erase-asteroids { display -- }
    max-asteroid-count 0 ?do
      display i asteroid@ erase-entity
    loop
  ;
  
  : try-shoot-asteroids { x y -- hit? }
    false
    max-asteroid-count 0 ?do
      x y i asteroid@ try-shoot or
    loop
  ;
  
  : check-collide-asteroids { x y -- collide? }
    false
    max-asteroid-count 0 ?do
      x y i asteroid@ check-collide or
    loop
  ;
  
  : asteroid-count ( -- count )
    0
    max-asteroid-count 0 ?do
      i asteroid@ entity-active? if 1+ then
    loop
  ;
  
  255 255 255 rgb8 constant asteroid-color
  
  <asteroid> begin-implement
    
    :noname { self -- }
      self <shootable>->new
      0e0 self asteroid-radius !
    ; define new
    
    :noname { radius x y self -- }
      max-asteroid-speed vrandom v* { speed }
      2e0 vpi v* vrandom v* { angle }
      speed angle vcos v* { delta-x }
      speed angle vsin v* { delta-y }
      x y delta-x delta-y self activate-entity
      radius self asteroid-radius !
    ; define do-spawn-asteroid
    
    :noname { self -- radius }
      self asteroid-radius @
    ; define shootable-radius@
    
    :noname { self -- }
      max-divide-count 2 - u>v vrandom v* 2e0 v+
      vround-half-away-zero v>u { divide }
      self entity-coord@ { x y }
      self asteroid-radius @ divide u>v v/ { radius }
      self deactivate-entity
      radius min-asteroid-radius v>= if
        divide 0 ?do radius x y spawn-asteroid loop
      then
    ; define do-hit
    
    :noname { ship-x ship-y self -- hit? }
      self entity-active? if
        self entity-coord@ { x y }
        self shootable-radius@ { radius }
        ship-x x v- dup v* ship-y y v- dup v* v+
        radius ship-body-radius v+ dup v* v<= dup if
          self do-hit
        then
      else
        false
      then
    ; define check-collide
    
    :noname { color display self -- }
      self entity-coord@ { x y }
      color x y convert-coord
      self asteroid-radius @ v>n
      display draw-pixel-circle
    ; define render-asteroid
    
    :noname { display self -- }
      asteroid-color display self render-asteroid
    ; define do-draw-entity
    
    :noname { display self -- }
      bk-color display self render-asteroid
    ; define do-erase-entity
    
  end-implement
  
  <entity> begin-class <shot>
  
    cell member shot-energy
    
    method do-spawn-shot ( x y delta-x delta-y shot -- )
    method render-shot ( color display shot -- )
  
  end-class
  
  1e0 constant init-shot-energy
  120 constant max-shot-count
  max-shot-count <shot> class-size * buffer: shots
  
  : shot@ ( index -- shot )
    <shot> class-size * shots +
  ;
  
  : spawn-shot { x y delta-x delta-y -- }
    max-shot-count 0 ?do
      i shot@ { shot }
      shot entity-active? not if
        x y delta-x delta-y shot do-spawn-shot exit
      then
    loop
  ;
  
  : init-shots ( -- )
    max-shot-count 0 ?do <shot> i shot@ init-object loop
  ;
  
  : deactivate-shots ( -- )
    max-shot-count 0 ?do i shot@ deactivate-entity loop
  ;
  
  : update-shots { interval -- }
    max-shot-count 0 ?do interval i shot@ update-entity loop
  ;
  
  : draw-shots { display -- }
    max-shot-count 0 ?do display i shot@ draw-entity loop
  ;
  
  : erase-shots { display -- }
    max-shot-count 0 ?do display i shot@ erase-entity loop
  ;
  
  255 0 0 rgb8 constant shot-color
  4e0 constant shot-size
  
  <shot> begin-implement
    
    :noname { self -- }
      self <entity>->new
      0e0 self shot-energy !
    ; define new
    
    :noname { x y delta-x delta-y self -- }
      x y delta-x delta-y self activate-entity
      init-shot-energy self shot-energy !
    ; define do-spawn-shot
    
    :noname { interval self -- }
      interval self <entity>->do-update-entity
      self entity-coord@ try-shoot-asteroids if
        self deactivate-entity exit
      then
      self shot-energy @ interval v- dup { energy }
      self shot-energy !
      energy 0e0 v<= if self deactivate-entity then
    ; define do-update-entity
    
    :noname { color display self -- }
      self entity-coord@ { x y }
      self entity-delta@ { delta-x delta-y }
      delta-y vnegate delta-x vnegate vatan2 { angle }
      color x y convert-coord
      x angle vcos shot-size v* v+
      y angle vsin shot-size v* v+ convert-coord
      display draw-pixel-line
    ; define render-shot
    
    :noname { display self -- }
      shot-color display self render-shot
    ; define do-draw-entity
    
    :noname { display self -- }
      bk-color display self render-shot
    ; define do-erase-entity
    
  end-implement
  
  <entity> begin-class <ship>
    
    cell member ship-heading
    cell member ship-delta-heading
    
    method do-spawn-ship ( ship -- )
    method turn-ship ( angle ship -- )
    method thrust-ship ( speed ship -- )
    method ship-shoot ( ship -- )
    method render-ship ( color display ship -- )
    
  end-class
  
  <ship> class-size buffer: ship
  
  : init-ship ( -- ) <ship> ship init-object ;
  
  : spawn-ship ( -- )
    true wave-start !
    systick::systick-counter wave-start-systick !
    ship do-spawn-ship
  ;
  
  : deactivate-ship ( -- ) ship deactivate-entity ;
  
  : update-ship ( interval -- ) ship update-entity ;
  
  : draw-ship ( display -- ) ship draw-entity ;
  
  : erase-ship ( display -- ) ship erase-entity ;
  
  <ship> begin-implement
    
    :noname { self -- }
      self <entity>->new
      0e0 self ship-heading !
      0e0 self ship-delta-heading !
    ; define new
    
    :noname { self -- }
      arena-width 2e0 v/ arena-height 2e0 v/ 0e0 0e0
      self activate-entity
      vpi 2e0 v/ self ship-heading !
      0e0 self ship-delta-heading !
    ; define do-spawn-ship
    
    :noname { angle self -- }
      self ship-delta-heading @ angle v+
      self ship-delta-heading !
    ; define turn-ship
    
    :noname { thrust self -- }
      self ship-heading @ { heading }
      heading vcos thrust v* heading vsin thrust v*
      self adjust-entity-delta
    ; define thrust-ship
    
    :noname { self -- }
      self ship-heading @ { heading }
      heading vcos { heading-cos }
      heading vsin { heading-sin }
      self entity-delta@ { delta-x delta-y }
      self entity-coord@
      delta-x heading-cos base-shot-speed v* v+
      delta-y heading-sin base-shot-speed v* v+
      spawn-shot
      heading-cos recoil-speed v* heading-sin recoil-speed v*
      self adjust-entity-delta
    ; define ship-shoot
    
    :noname { interval self -- }
      interval self <entity>->do-update-entity
      self ship-heading @ { heading }
      self ship-delta-heading @ { delta-heading }
      delta-heading v0<> if
        heading delta-heading interval v* v+
        self ship-heading !
        delta-heading vabs ship-heading-decel interval v* v-
        0e0 vmax delta-heading dup vabs v/ v*
        self ship-delta-heading !
      then
      self entity-coord@ check-collide-asteroids if
        wave-start @ not if
          beep
          -1 lives +!
          lives @ 0>= if spawn-ship then
        then
      then
    ; define do-update-entity
    
    :noname { color display self -- }
      self entity-coord@ { x y }
      color x y convert-coord ship-body-radius v>n
      display draw-filled-circle
      self ship-heading @ { heading }
      x heading vcos ship-dir-radius v* v+ { x1 }
      y heading vsin ship-dir-radius v* v+ { y1 }
      color x y convert-coord x1 y1 convert-coord
      display draw-pixel-line
    ; define render-ship
    
    :noname { display self -- }
      ship-color display self render-ship
    ; define do-draw-entity
    
    :noname { display self -- }
      bk-color display self render-ship
    ; define do-erase-entity
    
  end-implement
  
  : init-world ( -- ) init-asteroids init-shots init-ship ;
  
  initializer init-world
  
  : deactivate-world ( -- )
    deactivate-asteroids deactivate-shots deactivate-ship
  ;
    
  : update-world { interval -- }
    interval update-asteroids
    interval update-shots
    interval update-ship
  ;
  
  : draw-world { display -- }
    display draw-asteroids
    display draw-shots
    display draw-ship
    display draw-info
  ;
  
  : erase-world { display -- }
    display erase-asteroids
    display erase-shots
    display erase-ship
    display erase-info
  ;
  
  3 constant init-asteroid-count
  
  : start-world ( -- )
    deactivate-world
    wave @ init-asteroid-count + 0 ?do start-asteroid loop
    spawn-ship
  ;
  
  vpi constant ship-turn-speed
  10e0 constant ship-thrust
  
  : handle-thrust ( -- ) ship-thrust ship thrust-ship ;
  
  : handle-turn-right ( -- )
    ship-turn-speed vnegate ship turn-ship
  ;
  
  : handle-turn-left ( -- )
    ship-turn-speed ship turn-ship
  ;
  
  : handle-shoot ( -- ) ship ship-shoot ;
  
  $1B constant escape
  
  : empty-keys ( -- ) begin key? while key drop repeat ;

  : handle-screenshot ( -- )
    [:
      screenshot-fs@ { fs }
      fs if
        screenshot-path@ fs ['] take-screenshot try-and-display-error 0<> if
          drop 2drop
        then
      then
    ;] console::with-serial-error-output
  ;
  
  : handle-key ( -- exit? )
    reset-keymap
    update-keymap
    [char] q keymap-pressed@ if true exit then
    UP_ARROW keymap-pressed@ if handle-thrust then
    RIGHT_ARROW keymap-pressed@ if handle-turn-right then
    LEFT_ARROW keymap-pressed@ if handle-turn-left then
    bl keymap-pressed@ if handle-shoot then
    false
  ;
  
  2 constant init-lives
  5 constant extra-life-wave
  
  : play-rocks ( -- )
    0 wave !
    init-lives lives !
    begin
      ansi-term::hide-cursor
      true raw-keys-enabled!
      clear-keymap
      [: dup clear-pixmap update-display ;] with-term-display
      start-world
      systick::systick-counter { last-systick }
      begin
        last-systick [: { last-systick display }
          display erase-world
          systick::systick-counter { current-systick }
          wave-start @ if
            current-systick wave-start-systick @ -
            wave-start-ticks < wave-start !
          then
          current-systick last-systick - u>v 10000e0 v/
          update-world
          handle-key { exit-key? }
          display draw-world
          display update-display
          current-systick exit-key?
        ;] with-term-display
        { exit-key? } to last-systick
        [char] s keymap-released@ if handle-screenshot then
        exit-key? lives @ 0< or if
          [: dup clear-pixmap update-display ;]
          with-term-display
          page
          ." *** GAME OVER ***" cr cr
          ." You survived " wave @ . ." waves" cr
          false raw-keys-enabled!
          clear-keymap
          1000 ms
          empty-keys
          ansi-term::show-cursor
          exit
        then
        asteroid-count 0=
      until
      1 wave +!
      wave @ extra-life-wave umod 0= if 1 lives +! then
    again
  ;
  
end-module

Bricks at zeptoforth/test/rp2350/picocalc_bricks_keys_enhanced.fs at master · tabemann/zeptoforth · GitHub

\ Copyright (c) 2026 Travis Bemann
\
\ Permission is hereby granted, free of charge, to any person obtaining a copy
\ of this software and associated documentation files (the "Software"), to deal
\ in the Software without restriction, including without limitation the rights
\ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\ copies of the Software, and to permit persons to whom the Software is
\ furnished to do so, subject to the following conditions:
\ 
\ The above copyright notice and this permission notice shall be included in
\ all copies or substantial portions of the Software.
\ 
\ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
\ SOFTWARE.

\ The controls are:
\ 
\ Right: Move right
\ Left: Move left
\ S: Take a screenshot
\ Q: Give up in shame

begin-module bricks
  
  float32 import
  picocalc-term import
  picocalc-keys import
  picocalc-screenshot import
  tinymt32 import
  pixmap8 import
  pixmap8-utils import
  st7365p-8-common import
  ansi-term import
    
  begin-structure coord-size
    field: coord-x
    field: coord-y
  end-structure
  
  begin-structure delta-size
    field: delta-x
    field: delta-y
  end-structure
  
  begin-structure ball-size
    coord-size +field ball-coord
    delta-size +field ball-delta
    field: ball-radius
    field: ball-color
  end-structure
  
  begin-structure block-size
    coord-size +field block-coord
    field: block-width
    field: block-height
    field: block-color
    field: block-active
  end-structure
  
  begin-structure player-size
    coord-size +field player-coord
    delta-size +field player-delta
    field: player-width
    field: player-height
    field: player-color
  end-structure
  
  160e0 constant blocks-center-x
  80e0 constant blocks-center-y
  20e0 constant default-block-width
  7.5e0 constant default-block-height
  2e0 constant default-ball-radius
  160e0 constant init-ball-x
  160e0 constant init-ball-y
  120e0 constant init-ball-speed
  vpi 2e0 v/ constant init-ball-angle-range
  60e0 constant default-player-width
  7.5e0 constant default-player-height
  160e0 constant init-player-x
  300e0 constant init-player-y
  16 constant horiz-block-count
  8 constant vert-block-count
  horiz-block-count vert-block-count * constant block-count
  
  begin-structure world-size
    ball-size +field world-ball
    block-size block-count * +field world-blocks
    player-size +field world-player
    tinymt32-size +field world-prng
    field: world-last-tick
    field: world-tick
  end-structure
  
  : rnd ( world -- v )
    world-prng tinymt32-generate-uint32 0 f64>v
  ;
  
  : interval ( world -- v )
    dup world-tick @ swap world-last-tick @ - u>v 10000e0 v/
  ;
  
  : convert-angle ( n -- n' ) 255 * 60 / ;
  
  : angle-color { angle -- rgb8 }
    angle 60 < if
      255 angle convert-angle 0
    else
      angle 120 < if
        120 angle - convert-angle 255 0
      else
        angle 180 < if
          0 255 angle 120 - convert-angle
        else
          angle 240 < if
            0 240 angle - convert-angle 255
          else
            angle 300 < if
              angle 240 - convert-angle 0 255
            else
              255 0 360 angle - convert-angle
            then
          then
        then
      then
    then
    rgb8
  ;
  
  : rnd-color ( world -- rgb8 )
    rnd 360e0 v* v>n angle-color
  ;
  
  : init-block { x y world block -- }
    horiz-block-count 1- n>v default-block-width v* 2e0 v/
    blocks-center-x swap v-
    default-block-width x n>v v* v+
    block block-coord coord-x !
    vert-block-count 1- n>v default-block-height v* 2e0 v/
    blocks-center-y swap v-
    default-block-height y n>v v* v+
    block block-coord coord-y !
    default-block-width block block-width !
    default-block-height block block-height !
    world rnd-color block block-color !
    true block block-active !
  ;
  
  : init-blocks { world -- }
    vert-block-count 0 ?do
      horiz-block-count 0 ?do
        i j world j horiz-block-count * i + block-size *
        world world-blocks + init-block
      loop
    loop
  ;
  
  : init-ball { world ball -- }
    init-ball-x ball ball-coord coord-x !
    init-ball-y ball ball-coord coord-y !
    default-ball-radius ball ball-radius !
    vpi 2e0 v/ init-ball-angle-range 2e0 v/ v-
    init-ball-angle-range world rnd v* v+ { angle }
    angle vcos init-ball-speed v* ball ball-delta delta-x !
    angle vsin init-ball-speed v* ball ball-delta delta-y !
    world rnd-color ball ball-color !
  ;
  
  : init-player { world player -- }
    init-player-x player player-coord coord-x !
    init-player-y player player-coord coord-y !
    0e0 player player-delta delta-x !
    0e0 player player-delta delta-y !
    default-player-width player player-width !
    default-player-height player player-height !
    world rnd-color player player-color !
  ;
  
  : init-prng { prng -- }
    rng::random prng tinymt32-init
    prng tinymt32-prepare-example
  ;
  
  : init-world { world -- }
    systick::systick-counter dup world world-tick !
    world world-last-tick ! 
    world world-prng init-prng
    world init-blocks
    world dup world-ball init-ball
    world dup world-player init-player
  ;
  
  0 0 0 rgb8 constant erase-color
  
  : draw-rect { erase? color x y width height display -- }
    width 2e0 v/ { width2/ }
    height 2e0 v/ { height2/ }
    x width2/ v- vround-zero v>n { x0 }
    y height2/ v- vround-zero v>n { y0 }
    erase? if erase-color else color then
    x0 y0
    width 0.5e0 v+ vround-zero v>n
    height 0.5e0 v+ vround-zero v>n
    display draw-rect-const
  ;
  
  : draw-block { erase? display block -- }
    erase? block block-color @
    block block-coord coord-x @
    block block-coord coord-y @
    block block-width @
    block block-height @
    display draw-rect
  ;
  
  : draw-player { erase? display player -- }
    erase? player player-color @
    player player-coord coord-x @
    player player-coord coord-y @
    player player-width @
    player player-height @
    display draw-rect
  ;
  
  : draw-circle { erase? color x y radius display -- }
    erase? if erase-color else color then
    x v>n y v>n radius v>n display draw-filled-circle
  ;
  
  : draw-ball { erase? display ball -- }
    erase? ball ball-color @
    ball ball-coord coord-x @
    ball ball-coord coord-y @
    ball ball-radius @
    display draw-circle
  ;
  
  : draw-world { display world -- }
    display clear-pixmap
    horiz-block-count vert-block-count * 0 ?do
      false display i block-size * world world-blocks +
      draw-block
    loop
    false display world world-player draw-player
    false display world world-ball draw-ball
  ;
  
  : collide-rect { time x y width height ball -- collide? }
    ball ball-coord coord-x @ { bx }
    ball ball-coord coord-y @ { by }
    ball ball-delta delta-x @ { bdx }
    ball ball-delta delta-y @ { bdy }
    ball ball-radius @ { radius }
    bx bdx time v* v+ { bx' }
    by bdy time v* v+ { by' }
    width 2e0 v/ { width2/ }
    height 2e0 v/ { height2/ }
    x width2/ v- radius v- { x0 }
    y height2/ v- radius v- { y0 }
    x width2/ v+ radius v+ { x1 }
    y height2/ v+ radius v+ { y1 }
    bx x0 v< bx' x0 v>= and
    bx x1 v> bx' x1 v<= and or
    bx' x0 v>= bx' x1 v<= and or
    by y0 v< by' y0 v>= and
    by y1 v> by' y1 v<= and or
    by' y0 v>= by' y1 v<= and or and dup if
      bx x0 v<= bx' x0 v>= and if
        bdx vabs vnegate ball ball-delta delta-x !
        x0 ball ball-coord coord-x !
      then
      bx x1 v>= bx' x1 v<= and if
        bdx vabs ball ball-delta delta-x !
        x1 ball ball-coord coord-x !
      then
      by y0 v<= by' y0 v>= and if
        bdy vabs vnegate ball ball-delta delta-y !
        y0 ball ball-coord coord-y !
      then
      by y1 v>= by' y1 v<= and if
        bdy vabs ball ball-delta delta-y !
        y1 ball ball-coord coord-y !
      then
    then
  ;
  
  : collide-block { time display ball block -- }
    block block-active @ if
      time
      block block-coord coord-x @
      block block-coord coord-y @
      block block-width @
      block block-height @
      ball collide-rect if
        false block block-active !
        true display block draw-block
      then
    then
  ;
  
  : collide-blocks { time display ball world -- }
    horiz-block-count vert-block-count * 0 ?do
      time display ball
      i block-size * world world-blocks + collide-block
    loop
  ;
  
  0.5e0 constant collide-player-fract
  
  : collide-player { time ball player -- }
    time
    player player-coord coord-x @
    player player-coord coord-y @
    player player-width @
    player player-height @
    ball collide-rect if
      player player-delta delta-x @ collide-player-fract v*
      ball ball-delta delta-x @ v+
      ball ball-delta delta-x !
    then
  ;
  
  : collide-wall { time ball -- bottom? }
    ball ball-coord coord-x @ { bx }
    ball ball-coord coord-y @ { by }
    ball ball-delta delta-x @ { bdx }
    ball ball-delta delta-y @ { bdy }
    ball ball-radius @ { radius }
    bx bdx time v* v+ { bx' }
    by bdy time v* v+ { by' }
    term-pixels-dim@ n>v swap n>v { height width }
    bx' width radius v- v>= if
      bdx vabs vnegate ball ball-delta delta-x !
    then
    bx' radius v<= if
      bdx vabs ball ball-delta delta-x !
    then
    by' radius v<= if
      bdy vabs ball ball-delta delta-y !
    then
    by' height v>=
  ;
  
  : +delta! { time delta coord -- }
    delta delta-x @ time v* coord coord-x @ v+ coord coord-x !
    delta delta-y @ time v* coord coord-y @ v+ coord coord-y !
  ;
  
  : move-ball { time ball -- }
    time ball ball-delta ball ball-coord +delta!
  ;
  
  0.0625e0 constant friction0
  1e0 64e0 v/ constant friction1
  
  : apply-friction { time player -- }
    friction0 time v** friction1 time v** v* { friction' }
    player player-delta delta-x @ friction' v*
    player player-delta delta-x !
  ;
  
  : move-player { time player -- }
    time player player-delta player player-coord +delta!
    player player-width @ 2e0 v/ { width2/ }
    player player-coord coord-x @ { x }
    term-pixels-dim@ drop n>v { width' }
    x width2/ v<= dup if
      width2/ player player-coord coord-x !
    then
    x width' width2/ v- v>= dup if
      width' width2/ v- player player-coord coord-x !
    then
    or if
      0e0 player player-delta delta-x !
    then
  ;
   
  : update { display world -- died? }
    world interval { time }
    true display world world-ball draw-ball
    true display world world-player draw-player
    time display world world-ball world collide-blocks
    time world world-ball world world-player collide-player
    time world world-ball collide-wall dup if
      world dup world-ball init-ball
      world dup world-player init-player
    else
      time world world-ball move-ball
      time world world-player apply-friction
      time world world-player move-player
    then
    false display world world-ball draw-ball
    false display world world-player draw-player
  ;
  
  240e0 constant accel
  640e0 constant max-speed
  
  : accel-left { time player -- }
    player player-delta delta-x @ accel ( time v* ) v-
    max-speed vnegate vmax
    player player-delta delta-x !
  ;

  : accel-right { time player -- }
    player player-delta delta-x @ accel ( time v* ) v+
    max-speed vmin
    player player-delta delta-x !
  ;
  
  : reset-tick { world }
    systick::systick-counter dup world world-tick !
    world world-last-tick ! 
  ;
  
  $B4 constant LEFT_ARROW
  $B7 constant RIGHT_ARROW
  
  : wait-key ( -- )
    begin
      update-keymap
      false 256 0 do i keymap-pressed@ or loop
    until
  ;
  
  : handle-screenshot ( -- )
    [:
      screenshot-fs@ { fs }
      fs if
        screenshot-path@ fs ['] take-screenshot try-and-display-error 0<> if
          drop 2drop
        then
      then
    ;] console::with-serial-error-output
  ;

  : run-game ( -- )
    hide-cursor
    true raw-keys-enabled!
    page
    world-size [: { world }
      world init-world
      world [: { world display }
        display world draw-world
        display update-display
      ;] with-term-display
      wait-key
      [char] q keymap-pressed@ reset-keymap if exit then
      world reset-tick
      begin
        world [: { world display }
          display world update
          display update-display
        ;] with-term-display
        if
          wait-key
          [char] q keymap-pressed@ reset-keymap if exit then
          world reset-tick
        else
          update-keymap
          LEFT_ARROW keymap-pressed@ if
            world interval world world-player accel-left
          then
          RIGHT_ARROW keymap-pressed@ if
            world interval world world-player accel-right
          then
          [char] q keymap-pressed@ if exit then
          [char] s keymap-released@ if
            handle-screenshot
            world reset-tick
          then
          reset-keymap
          world world-tick @ world world-last-tick !
          systick::systick-counter world world-tick !
        then
      again
    ;] with-aligned-allot
    reset-keymap
    false raw-keys-enabled!
    1000 ms
    begin key? while key drop repeat
    show-cursor
  ;
  
end-module

Blocks at zeptoforth/test/rp_common/picocalc_block_game.fs at master · tabemann/zeptoforth · GitHub

\ Copyright (c) 2026 Travis Bemann
\ 
\ Permission is hereby granted, free of charge, to any person obtaining a copy
\ of this software and associated documentation files (the "Software"), to deal
\ in the Software without restriction, including without limitation the rights
\ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\ copies of the Software, and to permit persons to whom the Software is
\ furnished to do so, subject to the following conditions:
\ 
\ The above copyright notice and this permission notice shall be included in
\ all copies or substantial portions of the Software.
\ 
\ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
\ SOFTWARE.

\ The controls are:
\ 
\ Up: Move up
\ Down: Move down
\ Right: Move right
\ Left: Move left
\ Space: Select
\ S: Take a screenshot
\ Q: Give up in shame

begin-module block-game
  
  picocalc-term import
  picocalc-keys import
  picocalc-screenshot import
  pixmap8 import
  font import
  st7365p-8-common import
  tinymt32 import
  
  $B4 constant LEFT_ARROW
  $B5 constant UP_ARROW
  $B6 constant DOWN_ARROW
  $B7 constant RIGHT_ARROW

  0 value score
  
  255 255 255 rgb8 constant info-color
  127 127 127 rgb8 constant info-shadow-color
  16 constant info-x
  16 constant info-y
  
  tinymt32-size buffer: my-tinymt32
  
  : init-random ( -- )
    rng::random my-tinymt32 tinymt32-init
    my-tinymt32 tinymt32-prepare-example
  ;
  
  initializer init-random
  
  : random { val -- val' }
    val s>f my-tinymt32 tinymt32-generate-uint32 0 f*
    round-zero val 1- min
  ;
  
  create select-colors
  127 127 127 rgb8 c,
  255 0 0 rgb8 c,
  255 255 0 rgb8 c,
  0 255 0 rgb8 c,
  0 255 255 rgb8 c,
  0 0 255 rgb8 c,
  255 0 255 rgb8 c,
  cell align,
  
  create unselect-colors
  0 0 0 rgb8 c,
  191 0 0 rgb8 c,
  191 191 0 rgb8 c,
  0 191 0 rgb8 c,
  0 191 191 rgb8 c,
  0 0 191 rgb8 c,
  191 0 191 rgb8 c,
  
  6 constant colors
  16 constant game-width
  16 constant game-height
  8 constant game-init-height
  game-width game-height * cell align buffer: game-blocks
  0 value game-falling-block
  0 value game-falling-block-x
  0,0 2value game-falling-block-y
  3125 constant block-fall-delay
  1,25 game-height s>f f* 2constant block-fall-speed
  0 value last-block-fall-tick
  1250 constant select-move-delay
  0 value last-select-move-tick
  7500 constant select-delay
  0 value last-select-tick
  0 value last-tick
  game-width 2 / value select-x
  game-height 1- value select-y
  
  : game-block-addr { x y -- addr }
    game-width y * x + game-blocks +
  ;
  
  : game-block@ ( x y -- color )
    game-block-addr c@
  ;
  
  : game-block! ( color x y -- )
    game-block-addr c!
  ;
  
  : random-color ( -- color ) colors random 1+ ;
  
  : init-game ( -- )
    0 to score
    game-blocks game-width game-height * 0 fill
    game-height game-init-height ?do
      game-width 0 ?do
        random-color i j game-block!
      loop
    loop
    systick::systick-counter { current-tick }
    0 to game-falling-block
    0 to game-falling-block-x
    0,0 to game-falling-block-y
    current-tick to last-block-fall-tick
    current-tick select-move-delay - to last-select-move-tick
    current-tick select-delay - to last-select-tick
    current-tick to last-tick
    game-width 2 / to select-x
    game-height 1- to select-y
  ;
  
  : string> { addr bytes buf-addr -- buf-addr' }
    addr buf-addr bytes move bytes buf-addr +
  ;
  
  : n> ( n buf-addr -- buf-addr' )
    base @ { saved-base }
    [: 10 base ! swap format-integer + ;] try
    saved-base base ! ?raise
  ;

  : draw-info ( display -- )
    256 [: { display buf }
      buf { buf' }
      s" Score: " buf' string> to buf'
      score buf' n> to buf'
      info-shadow-color buf buf' buf - info-x 1+ info-y 1+
      display term-font@ draw-string-to-pixmap8
      info-color buf buf' buf - info-x info-y display
      term-font@ draw-string-to-pixmap8
    ;] with-allot
  ;
  
  term-pixels-dim@
  game-height / constant block-height
  game-width / constant block-width
  
  : draw-block { color x D: y display -- }
    color x block-width * y block-height s>f f* round-zero
    block-width block-height display draw-rect-const
  ;
  
  : draw-static-block { color x y display -- }
    x select-x = y select-y = and if
      color select-colors + c@
    else
      color unselect-colors + c@
    then
    x y s>f display draw-block
  ;
  
  : draw-game ( -- )
    [: { display }
      display clear-pixmap
      game-height 0 ?do
        game-width 0 ?do
          i j game-block@ i j display draw-static-block
        loop
      loop
      game-falling-block if
        game-falling-block unselect-colors + c@
        game-falling-block-x
        game-falling-block-y display draw-block
      then
      display draw-info
      display update-display
    ;] with-term-display
  ;
  
  : empty-keys ( -- ) begin key? while key drop repeat ;

  : game-over ( -- )
    [: dup clear-pixmap update-display ;] with-term-display
    page
    ." *** GAME OVER ***" cr cr ." Your score: " score . cr
    false raw-keys-enabled!
    clear-keymap
    1000 ms
    empty-keys
  ;
  
  : handle-screenshot ( -- )
    [:
      screenshot-fs@ { fs }
      fs if
        screenshot-path@ fs ['] take-screenshot try-and-display-error 0<> if
          drop 2drop
        then
      then
    ;] console::with-serial-error-output
    systick::systick-counter to last-tick
  ;
  
  : find-highest-block { x -- }
    0 game-height 1- ?do
      x i game-block@ 0= if i 1+ unloop exit then
    -1 +loop
    0
  ;
        
  
  : update-game { current-tick -- game-over? }
    game-falling-block if
      game-falling-block-y
      current-tick last-tick - s>f 10000,0 f/
      block-fall-speed f* d+ { D: y' }
      game-falling-block-x find-highest-block { y }   
      y' round-zero 1+ y >= if
        game-falling-block
        game-falling-block-x y 1- game-block!
        0 to game-falling-block
        current-tick to last-block-fall-tick
        false
      else
        y' to game-falling-block-y
        false
      then
    else
      last-block-fall-tick block-fall-delay +
      current-tick <= if
        random-color to game-falling-block
        game-width random to game-falling-block-x
        0,0 to game-falling-block-y
        game-falling-block-x 0 game-block@ 0<>
        dup not if
          game-falling-block-x 1 game-block@ if
            game-falling-block
            game-falling-block-x 0 game-block!
            0 to game-falling-block
          then
        then
      else
        false
      then
    then
    current-tick to last-tick
  ;
  
  : handle-select-move { current-tick -- }
    last-select-move-tick select-move-delay +
    current-tick - 0<= if
      false { update }
      LEFT_ARROW keymap-pressed@ if
        select-x 0> if
          -1 +to select-x true to update
        then
      then
      RIGHT_ARROW keymap-pressed@ if
        select-x game-width 1- < if
          1 +to select-x true to update
        then
      then
      UP_ARROW keymap-pressed@ if
        select-y 0> if
          -1 +to select-y true to update
        then
      then
      DOWN_ARROW keymap-pressed@ if
        select-y game-height 1- < if
          1 +to select-y true to update
        then
      then
      LEFT_ARROW reset-key
      RIGHT_ARROW reset-key
      UP_ARROW reset-key
      DOWN_ARROW reset-key
      update if
        current-tick to last-select-move-tick
      else
        current-tick select-move-delay -
        to last-select-move-tick
      then
    then
  ;
  
  : 2c! { x y addr -- } x addr c! y addr 1+ c! ;
  
  : 2c@ { addr -- x y } addr c@ addr 1+ c@ ;
  
  : push ( x y -- ) ram-here 2 ram-allot 2c! ;
  
  : pop ( -- x y ) ram-here 2 - 2c@ -2 ram-allot ;
  
  : select-blocks ( color x y -- )
    ram-here { addr }
    addr [: { color x y addr }
      0 { count }
      x y push
      begin ram-here addr <> while
        pop { x y }
        x y game-block@ color = if
          1 +to count
          0 x y game-block!
          x 0> if x 1- y push then
          x game-width 1- < if x 1+ y push then
          y 0> if x y 1- push then
          y game-height 1- < if x y 1+ push then
        then
      repeat
      count dup * +to score
    ;] try
    addr ram-here!
    ?raise
  ;
  
  : compact-blocks ( -- )
    game-width 0 ?do
      1 game-height 1- ?do
        j { x }
        x i game-block@ 0= if
          i 1- { y' }
          begin
            y' 0>= if
              x y' game-block@ { color }
              color if
                color x i game-block! 0 x y' game-block! true
              else
                -1 +to y' false
              then
            else
              true
            then
          until
        then 
      -1 +loop
    loop
  ;
  
  : handle-select { current-tick -- }
    last-select-tick select-delay + current-tick - 0<= if
      false { update }
      bl keymap-released@ if
        select-x select-y game-block@ { color }
        color if
          color select-x select-y select-blocks
          compact-blocks
        then
        true to update
      then
      bl reset-key
      update if
        current-tick to last-select-tick
      else
        current-tick select-delay - to last-select-tick
      then
    then
  ;
  
  : play-game ( -- )
    page
    true raw-keys-enabled!
    [: dup clear-pixmap update-display ;] with-term-display
    clear-keymap
    init-game
    begin
      update-keymap
      systick::systick-counter { current-tick }    
      current-tick handle-select-move
      current-tick handle-select
      current-tick update-game
      draw-game
      [char] s keymap-released@ if handle-screenshot then
      [char] q keymap-released@ or
      [char] s reset-key
      [char] q reset-key
    until
    game-over
  ;
  
end-module