Graphical demos for the PicoCalc

I figured it would be good to have a dedicated thread for graphical demos in any language for the PicoCalc.

(Also, I wanted to post this to the FORTH on the PicoCalc thread, but the board would not allow me to make another post there as I had made the three last posts, but when I tried to modify an existing post to include this it did not show the thread as having new content.)

A while back I wrote a raytracing demo for the Tufty 2040, which I have now ported to the PicoCalc with the RP2350 using hardware single-precision floating point (the original program used S15.16 fixed point, which was non-ideal for a number of reasons). The code can be gotten from zeptoforth/test/rp2350/picocalc_raytracing_ground_light.fs at master · tabemann/zeptoforth · GitHub.

Here is a screenshot:

And here is the full source code:

\ Copyright (c) 2023-2025 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.

begin-module raytracing

  oo import
  pixmap8 import
  st7365p-8-common import
  float32 import
  picocalc-term import

  \ Columns
  320 constant my-cols

  \ Rows
  320 constant my-rows

  \ Three-element constant
  : 3constant ( a b c "name" -- )
    : rot lit, swap lit, lit, postpone ;
  ;

  \ Background color
  0.5e0 0e0 0e0 3constant background-color

  \ Shadow color
  0e0 0e0 0e0 3constant shadow-color
  
  \ Eye coordinate
  0e0 0e0 3.20e0 3constant eye-coord
  
  \ Light coordinate
  -50e0 value light-x
  50e0 value light-y
  25e0 value light-z
  : light-coord light-x light-y light-z ;
\  -50e0 50e0 0e0 3constant light-coord

  \ Find an entity along a line
  defer find-entity ( sx sy sz vx vy vz exclude -- entity distance hit? )
  
  \ Entity class
  <object> begin-class <entity>

    \ Is there a hit?
    method point-hit? ( sx sy sz vx vy vz self -- distance hit? )

    \ Get point color
    method point-color ( hx hy hz vx vy vz self -- r g b )
    
  end-class

  \ Implement entity class
  <entity> begin-implement
  end-implement

  \ Sphere class
  <entity> begin-class <sphere>
    cell member sphere-radius
    cell member sphere-x
    cell member sphere-y
    cell member sphere-z
  end-class

  \ Add two vectors
  : vect+ { ax ay az bx by bz -- cx cy cz } ax bx v+ ay by v+ az bz v+ ;

  \ Subtract two vectors
  : vect- { ax ay az bx by bz -- cx cy cz } ax bx v- ay by v- az bz v- ;

  \ Multiply a vector by a scalar
  : vect* { ax ay az n -- bx by bz } ax n v* ay n v* az n v* ;
  
  \ Get the dot product of two vectors
  : dot { ax ay az bx by bz -- dot } ax bx v* ay by v* az bz v* v+ v+ ;

  \ Get the cross product of two vectors
  : cross { ax ay az bx by bz -- cx cy cz }
    ay bz v* az by v* v- az bx v* ax bz v* v- ax by v* ay bx v* v-
  ;
  
  \ Get the magnitude of a vector
  : mag { x y z -- magnitude } x x v* y y v* z z v* v+ v+ vsqrt ;

  \ Get the unit vector of a vector
  : unit { x y z -- ux uy uz }
    x y z mag { dist } x dist v/ y dist v/ z dist v/
  ;

  \ Get the distance between two points
  : dist ( ax ay az bx by bz -- distance ) vect- mag ;

  \ Get the minimum root of an ax^2 + bx + c equation
  : minroot { a b c -- x found? }
    a v0= if
      c vnegate b v/ true
    else
      b b v* 4e0 a v* c v* v- { disc }
      disc v0>= if
        disc vsqrt { disc' }
        2e0 a v* { 2a }
        b vnegate disc' v+ b vnegate disc' v- vmin 2a v/ true
      else
        0e0 false
      then
    then
  ;
  
  \ Implement sphere class
  <sphere> begin-implement

    \ Constructor
    :noname { radius x y z self -- }
      self <entity>->new
      radius self sphere-radius !
      x self sphere-x !
      y self sphere-y !
      z self sphere-z !
    ; define new

    \ Is there a hit?
    :noname { sx sy sz vx vy vz self -- distance hit? }
      sx sy sz self sphere-x @ self sphere-y @ self sphere-z @ vect-
      { osx osy osz }
      1e0 \ vx vy vz vx vy vz dot
      osx osy osz vx vy vz dot 2e0 v*
      osx osy osz osx osy osz dot abs self sphere-radius @ dup v* v-
      minroot
    ; define point-hit?

  end-implement

  \ The colored sphere class
  <sphere> begin-class <colored-sphere>
    cell member sphere-r
    cell member sphere-g
    cell member sphere-b
  end-class

  \ Implement the colored sphere class
  <colored-sphere> begin-implement

    \ Constructor
    :noname { radius x y z r g b self -- }
      radius x y z self <sphere>->new
      r self sphere-r !
      g self sphere-g !
      b self sphere-b !
    ; define new

    \ Get point color
    :noname { hx hy hz vx vy vz self -- r g b }
      self sphere-x @ self sphere-y @ self sphere-z @ hx hy hz vect- unit
      { nx ny nz }
      hx hy hz light-coord vect- unit { lx ly lz }
      hx hy hz lx ly lz -1e0 vect* 0 find-entity nip nip not if
        nx ny nz lx ly lz dot 0e0 vmax 1e0 vmin { lambert }
        self sphere-r @ self sphere-g @ self sphere-b @ lambert vect*
      else
        shadow-color
      then
    ; define point-color
    
  end-implement

  \ The mirror sphere class
  <sphere> begin-class <mirror-sphere>
    cell member mirror-sphere-r
    cell member mirror-sphere-g
    cell member mirror-sphere-b
    cell member mirror-sphere-a
  end-class

  \ Implement the mirror sphere class
  <mirror-sphere> begin-implement
    
    \ Constructor
    :noname { radius x y z r g b a self -- }
      radius x y z self <sphere>->new
      r a v* self mirror-sphere-r !
      g a v* self mirror-sphere-g !
      b a v* self mirror-sphere-b !
      a self mirror-sphere-a !
    ; define new
    
    \ Get point color
    :noname { hx hy hz vx vy vz self -- r g b }
      self sphere-x @ self sphere-y @ self sphere-z @ hx hy hz vect- unit
      { nx ny nz }
      vx vy vz nx ny nz nx ny nz vx vy vz dot vect* 2e0 vect* vect-
      unit { rx ry rz }
      hx hy hz rx ry rz self find-entity if { entity distance }
        rx ry rz distance vect* hx hy hz vect+ rx ry rz entity point-color
      else
        2drop background-color
      then
      { r g b }
      hx hy hz light-coord vect- unit { lx ly lz }
      hx hy hz lx ly lz -1e0 vect* 0 find-entity nip nip not if
        nx ny nz lx ly lz dot 0e0 vmax 1e0 vmin { lambert }
        self mirror-sphere-r @
        self mirror-sphere-g @
        self mirror-sphere-b @ lambert vect*
      else
        shadow-color
      then
      { mr mg mb }
      self mirror-sphere-a @ { a } 1e0 a v- { a' }
      r a' v* mr v+ g a' v* mg v+ b a' v* mb v+
    ; define point-color
    
  end-implement

  \ Plane class
  <entity> begin-class <plane>
    cell member plane-x
    cell member plane-y
    cell member plane-z
    cell member plane-nx
    cell member plane-ny
    cell member plane-nz
  end-class

  \ Implement plane class
  <plane> begin-implement

    \ Constructor
    :noname { x y z nx ny nz self -- }
      self <entity>->new
      nx ny nz unit to nz to ny to nx
      x self plane-x !
      y self plane-y !
      z self plane-z !
      nx self plane-nx !
      ny self plane-ny !
      nz self plane-nz !
    ; define new
    
    \ Is there a hit?
    :noname { sx sy sz vx vy vz self -- distance hit? }
      self plane-nx @ self plane-ny @ self plane-nz @ { nx ny nz }
      nx ny nz vx vy vz dot { denom }
      denom v0<> if
        self plane-x @ self plane-y @ self plane-z @ sx sy sz vect- nx ny nz dot
        denom v/ { n }
        n v0>= n 16384e0 v< and if n true else 0e0 false then
      else
        0 false
      then
    ; define point-hit?

  end-implement

  \ Tiled plane
  <plane> begin-class <tiled-plane>
    cell member plane-ax
    cell member plane-ay
    cell member plane-az
    cell member plane-bx
    cell member plane-by
    cell member plane-bz
    cell member plane-r0
    cell member plane-g0
    cell member plane-b0
    cell member plane-r1
    cell member plane-g1
    cell member plane-b1
    cell member plane-tile-size
  end-class

  \ Implement tiled plane
  <tiled-plane> begin-implement

    \ Constructor
    :noname { x y z nx ny nz ax ay az r0 g0 b0 r1 g1 b1 tile-size self -- }
      x y z nx ny nz self <plane>->new
      ax ay az unit to az to ay to ax
      ax self plane-ax !
      ay self plane-ay !
      az self plane-az !
      self plane-nx @ self plane-ny @ self plane-nz @ ax ay az cross
      self plane-bz ! self plane-by ! self plane-bx !
      r0 self plane-r0 !
      g0 self plane-g0 !
      b0 self plane-b0 !
      r1 self plane-r1 !
      g1 self plane-g1 !
      b1 self plane-b1 !
      tile-size self plane-tile-size !
    ; define new

    \ Get point color
    :noname { hx hy hz vx vy vz self -- r g b }
      light-coord { lx' ly' lz' }
      lx' hx v- dup v*
      ly' hy v- dup v* v+
      lz' hz v- dup v* v+ vsqrt 180e0 v>= if
        shadow-color exit
      then
      hx hy hz self plane-x @ self plane-y @ self plane-z @ vect- { cx cy cz }
      self plane-ax @ self plane-ay @ self plane-az @ cx cy cz dot { dx }
      self plane-bx @ self plane-by @ self plane-bz @ cx cy cz dot { dy }
      dx self plane-tile-size @ v/ v>f64 floor { dx' }
      dy self plane-tile-size @ v/ v>f64 floor { dy' }
      dx' 2 mod 0= dy' 2 mod 0= xor if
        self plane-r0 @ self plane-g0 @ self plane-b0 @
      else
        self plane-r1 @ self plane-g1 @ self plane-b1 @
      then
      { r g b }
      hx hy hz lx' ly' lz'  vect- unit { lx ly lz }
      hx hy hz lx ly lz -1e0 vect* 0 find-entity nip nip not if
        self plane-nx @ self plane-ny @ self plane-nz @
        lx ly lz dot 0e0 vmax 1e0 vmin { lambert }
        r g b lambert vect*
      else
        shadow-color
      then
    ; define point-color
    
  end-implement
  
  <colored-sphere> class-size buffer: my-sphere-0
  <colored-sphere> class-size buffer: my-sphere-1
  <colored-sphere> class-size buffer: my-sphere-2
  <colored-sphere> class-size buffer: my-sphere-3
  <mirror-sphere> class-size buffer: my-sphere-4
  <tiled-plane> class-size buffer: my-plane

  create my-entities
  my-sphere-0 ,
  my-sphere-1 ,
  my-sphere-2 ,
  my-sphere-3 ,
  my-sphere-4 ,
  my-plane ,
  6 constant entity-count
  
  \ Find the closest, if any, entity
  :noname { sx sy sz vx vy vz exclude -- entity distance hit? }
    0 0 false { found-entity distance hit? }
    entity-count 0 ?do
      my-entities i cells + @ { entity }
      entity exclude <> if
        sx sy sz vx vy vz entity point-hit? if { entity-distance }
          hit? not entity-distance distance v< or entity-distance v0> and if
            entity to found-entity
            entity-distance to distance
            true to hit?
          then
        else
          drop
        then
      then
    loop
    found-entity distance hit?
  ; is find-entity
  
  \ Send a ray
  : send-ray { sx sy sz vx vy vz -- r g b }
    sx sy sz vx vy vz 0 find-entity if { entity distance }
      vx vy vz distance vect* sx sy sz vect+ vx vy vz entity point-color
    else
      drop drop background-color
    then
  ;

  \ Convert a coordinate
  : convert-coord { x y -- x' y' }
    x [ my-cols 2 / ] literal - n>v 100e0 v/
    y negate [ my-rows 2 / ] literal + n>v 100e0 v/
  ;

  \ Color dithering table
  create color-table
  0.8e0 , 1e0 , 1.2e0 , 1e0 , 0.8e0 , 1e0 , 1.2e0 , 1e0 ,
  1e0 , 1.1e0 , 1e0 , 0.9e0 , 1e0 , 1.1e0 , 1e0 , 0.9e0 ,
  1.2e0 , 1e0 , 0.8e0 , 1e0 , 1.2e0 , 1e0 , 0.8e0 , 1e0 ,
  1e0 , 0.9e0 , 1e0 , 1.1e0 , 1e0 , 0.9e0 , 1e0 , 1.1e0 ,
  0.8e0 , 1e0 , 1.2e0 , 1e0 , 0.8e0 , 1e0 , 1.2e0 , 1e0 ,
  1e0 , 1.1e0 , 1e0 , 0.9e0 , 1e0 , 1.1e0 , 1e0 , 0.9e0 ,
  1.2e0 , 1e0 , 0.8e0 , 1e0 , 1.2e0 , 1e0 , 0.8e0 , 1e0 ,
  1e0 , 0.9e0 , 1e0 , 1.1e0 , 1e0 , 0.9e0 , 1e0 , 1.1e0 ,
  
  \ Convert a color
  : convert-color { r g b x y -- color }
    x 3 and y 3 and 8 * + cells color-table + @ { factor }
    r 255e0 v* factor v* vround-zero v>n 0 max 255 min
    g 255e0 v* factor v* vround-zero v>n 0 max 255 min
    b 255e0 v* factor v* vround-zero v>n 0 max 255 min rgb8
  ;
  
  \ Draw world
  : draw-world ( -- )
    page
    [: { display }
      display clear-pixmap
      display update-display
    ;] with-term-display
    1e0 2.5e0 2.5e0 -6.4e0 0e0 1e0 0e0 <colored-sphere> my-sphere-0 init-object
    1.5e0 0e0 -1e0 -4.8e0 1e0 0e0 0e0 <colored-sphere> my-sphere-1 init-object
    2e0 -2.5e0 3e0 -10e0 0e0 0e0 1e0 <colored-sphere> my-sphere-2 init-object
    0.75e0 -2e0 1e0 -3.6e0 0e0 1e0 1e0 <colored-sphere> my-sphere-3 init-object
    1.5e0 0e0 2.5e0 -7.5e0 1e0 0e0 1e0 0.25e0 <mirror-sphere> my-sphere-4 init-object
    0e0 -20e0 0e0 0e0 -1e0 0e0 1e0 0e0 0e0 0.75e0 0e0 0e0 0e0 0.75e0 0e0 10e0
    <tiled-plane> my-plane init-object
    my-rows 0 ?do
      i [: { y display }
        my-cols 0 ?do
          i y convert-coord 0e0 eye-coord vect- unit { vx vy vz }
          eye-coord vx vy vz send-ray i y convert-color
          i y display draw-pixel-const
        loop
        display update-display
      ;] with-term-display
    loop
    begin key? until
    key drop
  ;
  
end-module

10 Likes

That’s a great idea!

1 Like

That’s a really neat demo. If I didn’t know otherwise I wouldn’t have guessed it was done in MMBasic.

Here’s a relatively (for basic) high performance blocks game:

4 Likes

i would love to see a video!

I get a “if without then” error on line 522

The if continues at line 523 but my mmbasic does not compute it.

Is there an option to enable that? Do need another version of picomite?

I had the same problem. I edited that line continuation back to one line. Here’s the corrected version:

' ==========================================
' PicoCalc Blocks Game (with FRAMEBUFFER)
' - True double buffering using FRAMEBUFFER + LAYER
' - Static background on F, dynamic sprites on L
' - FRAMEBUFFER MERGE for flicker-free display
' ==========================================
OPTION EXPLICIT

' ---- Tunables ----
CONST PAD_ACCEL = 2.0
CONST PAD_DECAY = 0.88
CONST PAD_MAX   = 24
CONST BRADIUS   = 6
CONST BALL_SPEED_INIT = 3.0
CONST BALL_SPEED_ACCEL_PER_SEC = 0.03
CONST PADDLE_KICK = 0.2
CONST TICK_MS   = 38

' ---- Screen & colors ----
CONST W% = MM.HRES
CONST H% = MM.VRES
CONST HUDH% = 18

CONST COL_BG%     = RGB(BLACK)
CONST COL_TXT%    = RGB(WHITE)
CONST COL_BORDER% = RGB(MYRTLE)
CONST COL_PAD%    = RGB(GREEN)
CONST COL_BALL%   = RGB(RED)

' ---- Block layout ----
CONST LEVELS% = 10
CONST BLOCK_ROWS% = 5
CONST BLOCK_COLS% = 8
CONST BLOCK_W% = 35
CONST BLOCK_H% = 12
CONST BLOCK_GAP% = 4
CONST BLOCK_TOP% = 40

' ---- Block types ----
CONST BLOCK_RED% = 30        ' 30 points, 1 hit
CONST BLOCK_ORANGE% = 20     ' 20 points, 1 hit
CONST BLOCK_YELLOW_FULL% = 12   ' 10 points, 2 hits (full health)
CONST BLOCK_YELLOW_DMG% = 11    ' 10 points, 1 hit left (damaged)
CONST BLOCK_BLUE% = 99       ' Indestructible, no points

' ---- Editable board layout (R=Red, O=Orange, Y=Yellow, B=Blue/Indestructible, 0=Empty) ----
' Level 1 - Easy warmup
DATA "0","0","0","0","0","0","0","0"
DATA "0","0","0","0","0","0","0","0"
DATA "0","0","R","R","R","R","0","0"
DATA "0","R","R","Y","Y","R","R","0"
DATA "R","R","R","R","R","R","R","R"
' Level 2 - Getting harder
DATA "0","0","R","R","R","R","0","0"
DATA "0","O","O","O","O","O","O","0"
DATA "Y","Y","Y","Y","Y","Y","Y","Y"
DATA "Y","Y","Y","Y","Y","Y","Y","Y"
DATA "Y","Y","Y","Y","Y","Y","Y","Y"
' Level 3 - Mixed blocks
DATA "R","R","O","O","O","O","R","R"
DATA "R","O","Y","Y","Y","Y","O","R"
DATA "O","Y","Y","Y","Y","Y","Y","O"
DATA "O","Y","Y","Y","Y","Y","Y","O"
DATA "Y","Y","Y","Y","Y","Y","Y","Y"
' Level 4 - Pyramid
DATA "0","0","0","R","R","0","0","0"
DATA "0","0","O","O","O","O","0","0"
DATA "0","Y","Y","Y","Y","Y","Y","0"
DATA "Y","Y","Y","Y","Y","Y","Y","Y"
DATA "0","0","0","B","B","0","0","0"
' Level 5 - Box
DATA "R","R","R","R","R","R","R","R"
DATA "R","0","0","0","0","0","0","R"
DATA "R","0","B","B","B","B","0","R"
DATA "R","0","0","0","0","0","0","R"
DATA "R","R","R","R","R","R","R","R"
' Level 6 - Corners
DATA "R","R","0","0","0","0","O","O"
DATA "R","Y","0","Y","Y","0","Y","O"
DATA "0","0","B","Y","Y","B","0","0"
DATA "Y","Y","0","B","B","0","Y","Y"
DATA "O","O","0","0","0","0","R","R"
' Level 7 - Wings
DATA "R","0","0","Y","Y","0","0","R"
DATA "O","O","B","Y","Y","B","O","O"
DATA "Y","Y","Y","B","B","Y","Y","Y"
DATA "Y","Y","B","O","O","B","Y","Y"
DATA "0","0","0","R","R","0","0","0"
' Level 8 - Checkers
DATA "R","O","R","O","R","O","R","O"
DATA "Y","B","Y","B","Y","B","Y","B"
DATA "O","Y","O","Y","O","Y","O","Y"
DATA "B","Y","B","Y","B","Y","B","Y"
DATA "R","O","R","O","R","O","R","O"
' Level 9 - Cross
DATA "0","0","R","R","R","R","0","0"
DATA "0","0","O","B","B","O","0","0"
DATA "Y","Y","Y","B","B","Y","Y","Y"
DATA "Y","Y","Y","B","B","Y","Y","Y"
DATA "0","0","O","O","O","O","0","0"
' Level 10 - Final Challenge
DATA "R","B","O","B","O","B","O","R"
DATA "B","Y","Y","0","0","Y","Y","B"
DATA "O","Y","B","Y","Y","B","Y","O"
DATA "B","Y","Y","0","0","Y","Y","B"
DATA "R","B","O","B","O","B","O","R"

' ---- State ----
DIM INTEGER currentLevel%=1
DIM FLOAT bx!, by!
DIM INTEGER br%
DIM FLOAT vx!, vy!
DIM INTEGER lastAccelTime%
DIM px!, py!, pw%, ph%, pvx!
DIM INTEGER score%, lives%
DIM INTEGER ballLaunched%
DIM INTEGER frames%, t0%
DIM fps$
DIM k$
DIM INTEGER blocks%(BLOCK_ROWS%-1, BLOCK_COLS%-1)
DIM INTEGER totalBlocks%, blocksLeft%
DIM FLOAT hitPos!, angle!
DIM INTEGER explosionActive%, explosionX%, explosionY%, explosionFrame%, explosionColor%
DIM INTEGER oldScore%, oldLives%
DIM INTEGER lastHitRow%, lastHitCol%, hitTimeout%
DIM INTEGER screenshotNum%


' ---- Beeps ----
SUB BeepServe(): PLAY TONE 700,700 : PAUSE 40 : PLAY STOP : END SUB
SUB BeepPaddle(): PLAY TONE 800,800 : PAUSE 20 : PLAY STOP : END SUB
SUB BeepWall(): PLAY TONE 600,600 : PAUSE 20 : PLAY STOP : END SUB
SUB BeepBlock(): PLAY TONE 1200,1200 : PAUSE 25 : PLAY STOP : END SUB
SUB BeepMiss(): PLAY TONE 200,200 : PAUSE 80 : PLAY STOP : END SUB

SUB ResetBall()
  ' Reset ball to paddle position (not launched)
  ballLaunched% = 0
  bx! = px! + pw%/2 - br%
  by! = py! - 2*br% - 2
  IF RND > 0.5 THEN vx! = BALL_SPEED_INIT ELSE vx! = -BALL_SPEED_INIT
  vy! = -BALL_SPEED_INIT
  lastAccelTime% = TIMER
  hitTimeout% = 0
END SUB

SUB DestroyBlock(r%, c%, points%)
  ' Destroy a block with explosion and award points
  LOCAL bx%, by%
  bx% = GetBlockX(c%)
  by% = GetBlockY(r%)
  TriggerExplosion bx%, by%, BLOCK_W%, BLOCK_H%, GetBlockColor(blocks%(r%, c%))
  blocks%(r%, c%) = 0
  blocksLeft% = blocksLeft% - 1
  score% = score% + points%
  EraseBlock r%, c%
END SUB

SUB Draw3DHighlight(x%, y%, w%, h%)
  ' Draw white highlight on top and left edges for 3D effect
  LINE x%, y%, x%+w%-1, y%, , RGB(WHITE)  ' Top edge
  LINE x%, y%, x%, y%+h%-1, , RGB(WHITE)  ' Left edge
END SUB


' ---- Drawing helpers ----
SUB DrawHUD()
  LOCAL s$
  s$ = "L" + STR$(currentLevel%) + " Score " + STR$(score%) + " Lives " + STR$(lives%)
  TEXT 6, 3, s$, "LT", , , COL_TXT%, COL_BG%
  IF fps$ <> "" THEN
    TEXT W%-4, 3, fps$, "RT", , , COL_TXT%, COL_BG%
  END IF
END SUB

SUB DrawPaddleAt(x%, y%)
  ' Draw main paddle
  BOX x%, y%, pw%, ph%, 0, , COL_PAD%
  Draw3DHighlight x%, y%, pw%, ph%
END SUB

SUB DrawBallAt(x%, y%)
  LOCAL cx%, cy%
  cx% = x% + br%
  cy% = y% + br%
  ' Draw main ball
  CIRCLE cx%, cy%, br%, 0, 1.0, , COL_BALL%
  ' Draw small white highlight on top-left for 3D effect
  CIRCLE cx%-2, cy%-2, 1, 0, 1.0, , RGB(WHITE)
END SUB

SUB TriggerExplosion(x%, y%, w%, h%, blockColor%)
  ' Set explosion state - will be drawn in main loop
  explosionActive% = 1
  explosionX% = x% + w%/2
  explosionY% = y% + h%/2
  explosionFrame% = 0
  explosionColor% = blockColor%
END SUB

SUB DrawExplosion()
  LOCAL size%
  IF explosionActive% = 0 THEN EXIT SUB

  ' Draw explosion animation on layer
  size% = (explosionFrame% + 1) * 4

  ' Expanding circle
  CIRCLE explosionX%, explosionY%, size%, 0, 1.0, , explosionColor%

  ' Star burst lines that grow with frame
  LINE explosionX%-size%, explosionY%, explosionX%+size%, explosionY%, , explosionColor%
  LINE explosionX%, explosionY%-size%, explosionX%, explosionY%+size%, , explosionColor%

  IF explosionFrame% > 0 THEN
    LOCAL offset%
    offset% = size% * 0.7
    LINE explosionX%-offset%, explosionY%-offset%, explosionX%+offset%, explosionY%+offset%, , explosionColor%
    LINE explosionX%-offset%, explosionY%+offset%, explosionX%+offset%, explosionY%-offset%, , explosionColor%
  END IF

  ' Advance animation
  explosionFrame% = explosionFrame% + 1
  IF explosionFrame% > 2 THEN
    explosionActive% = 0  ' Animation done
  END IF
END SUB

SUB InitBlocks()
  LOCAL r%, c%, blockChar$, skipRows%, i%
  totalBlocks% = 0
  RESTORE  ' Reset DATA pointer to start

  ' Skip to current level's data
  skipRows% = (currentLevel% - 1) * BLOCK_ROWS% * BLOCK_COLS%
  FOR i% = 1 TO skipRows%
    READ blockChar$
  NEXT

  ' Read current level
  FOR r% = 0 TO BLOCK_ROWS%-1
    FOR c% = 0 TO BLOCK_COLS%-1
      READ blockChar$
      ' Convert character to block type
      SELECT CASE blockChar$
        CASE "R"
          blocks%(r%, c%) = BLOCK_RED%
          totalBlocks% = totalBlocks% + 1
        CASE "O"
          blocks%(r%, c%) = BLOCK_ORANGE%
          totalBlocks% = totalBlocks% + 1
        CASE "Y"
          blocks%(r%, c%) = BLOCK_YELLOW_FULL%
          totalBlocks% = totalBlocks% + 1
        CASE "B"
          blocks%(r%, c%) = BLOCK_BLUE%
          ' Don't count indestructible blocks in total
        CASE ELSE
          blocks%(r%, c%) = 0  ' Empty space
      END SELECT
    NEXT
  NEXT
  blocksLeft% = totalBlocks%
END SUB

FUNCTION GetBlockX(c%) AS INTEGER
  GetBlockX = c% * (BLOCK_W% + BLOCK_GAP%) + BLOCK_GAP%
END FUNCTION

FUNCTION GetBlockY(r%) AS INTEGER
  GetBlockY = BLOCK_TOP% + r% * (BLOCK_H% + BLOCK_GAP% + 3)
END FUNCTION

FUNCTION GetBlockColor(blockType%) AS INTEGER
  SELECT CASE blockType%
    CASE BLOCK_RED%
      GetBlockColor = RGB(RED)
    CASE BLOCK_ORANGE%
      GetBlockColor = RGB(RUST)
    CASE BLOCK_YELLOW_FULL%
      GetBlockColor = RGB(YELLOW)
    CASE BLOCK_YELLOW_DMG%
      GetBlockColor = RGB(BROWN)  ' Darker yellow for damaged
    CASE BLOCK_BLUE%
      GetBlockColor = RGB(BLUE)
    CASE ELSE
      GetBlockColor = RGB(CYAN)
  END SELECT
END FUNCTION

SUB DrawBlocks()
  LOCAL r%, c%, bx%, by%, blockType%
  FOR r% = 0 TO BLOCK_ROWS%-1
    FOR c% = 0 TO BLOCK_COLS%-1
      blockType% = blocks%(r%, c%)
      IF blockType% > 0 THEN
        bx% = GetBlockX(c%)
        by% = GetBlockY(r%)
        BOX bx%, by%, BLOCK_W%, BLOCK_H%, 0, , GetBlockColor(blockType%)
        BOX bx%, by%, BLOCK_W%, BLOCK_H%, 1, COL_BORDER%
        Draw3DHighlight bx%+1, by%+1, BLOCK_W%-2, BLOCK_H%-1
      END IF
    NEXT
  NEXT
END SUB

SUB EraseBlock(r%, c%)
  LOCAL bx%, by%
  bx% = GetBlockX(c%)
  by% = GetBlockY(r%)
  FRAMEBUFFER WRITE F
  BOX bx%, by%, BLOCK_W%, BLOCK_H%, 0, , COL_BG%
  FRAMEBUFFER WRITE L  ' Restore to layer
END SUB

FUNCTION CheckBlockCollision(ballX%, ballY%, ballR%) AS INTEGER
  LOCAL r%, c%, bx%, by%, bx2%, by2%, blockType%
  LOCAL ballLeft%, ballRight%, ballTop%, ballBot%

  ballLeft% = ballX%
  ballRight% = ballX% + 2*ballR%
  ballTop% = ballY%
  ballBot% = ballY% + 2*ballR%

  ' Decrement hit timeout
  IF hitTimeout% > 0 THEN hitTimeout% = hitTimeout% - 1

  FOR r% = 0 TO BLOCK_ROWS%-1
    FOR c% = 0 TO BLOCK_COLS%-1
      blockType% = blocks%(r%, c%)
      IF blockType% > 0 THEN
        bx% = GetBlockX(c%)
        by% = GetBlockY(r%)
        bx2% = bx% + BLOCK_W%
        by2% = by% + BLOCK_H%

        ' AABB collision test
        IF ballRight% >= bx% AND ballLeft% <= bx2% AND ballBot% >= by% AND ballTop% <= by2% THEN
          ' Check if this is the same block hit recently
          IF hitTimeout% > 0 AND r% = lastHitRow% AND c% = lastHitCol% THEN
            ' Still in timeout - ignore this block completely, check others
            ' Don't return, just continue to next block
          ELSE
            ' Record this hit for timeout
            lastHitRow% = r%
            lastHitCol% = c%
            hitTimeout% = 15
          ' Handle different block types
          IF blockType% = BLOCK_BLUE% THEN
            ' Blue block - indestructible, just bounce
            CheckBlockCollision = 1
            EXIT FUNCTION
          ELSE IF blockType% = BLOCK_YELLOW_FULL% THEN
            ' Yellow block at full health - damage it, no points yet
            blocks%(r%, c%) = BLOCK_YELLOW_DMG%
            FRAMEBUFFER WRITE F
            BOX bx%, by%, BLOCK_W%, BLOCK_H%, 0, , GetBlockColor(BLOCK_YELLOW_DMG%)
            BOX bx%, by%, BLOCK_W%, BLOCK_H%, 1, COL_BORDER%
            Draw3DHighlight bx%+1, by%+1, BLOCK_W%-2, BLOCK_H%-1
            FRAMEBUFFER WRITE L
          ELSE IF blockType% = BLOCK_YELLOW_DMG% THEN
            ' Yellow block damaged - destroy it, award 10 points
            DestroyBlock r%, c%, 10
          ELSE IF blockType% = BLOCK_ORANGE% THEN
            ' Orange block - destroy, award 20 points
            DestroyBlock r%, c%, 20
          ELSE IF blockType% = BLOCK_RED% THEN
            ' Red block - destroy, award 30 points
            DestroyBlock r%, c%, 30
          END IF
          CheckBlockCollision = 1
          EXIT FUNCTION
          END IF
        END IF
      END IF
    NEXT
  NEXT
  CheckBlockCollision = 0
END FUNCTION

' ---- Init ----
pw% = W% \ 6 : IF pw% < 30 THEN pw% = 30  ' (no MAX(); clamp via IF)
ph% = 6
px! = (W% - pw%) / 2
py! = H% - (ph% + 6)
pvx! = 0

br% = BRADIUS
bx! = W% \ 2 : by! = H% \ 2
vx! = BALL_SPEED_INIT
vy! = -BALL_SPEED_INIT

score% = 0 : lives% = 3
oldScore% = 0 : oldLives% = 3
ballLaunched% = 0
explosionActive% = 0
hitTimeout% = 0
lastHitRow% = -1
lastHitCol% = -1
screenshotNum% = 0

frames% = 0 : t0% = TIMER
fps$ = ""
lastAccelTime% = TIMER

InitBlocks

' ---- Setup Framebuffers ----
FRAMEBUFFER CREATE           ' Create framebuffer F
FRAMEBUFFER LAYER RGB(BLACK) ' Create layer L with black as transparent

' Draw static background to F
FRAMEBUFFER WRITE F
CLS COL_BG%
DrawBlocks

' Draw initial sprites to layer L
FRAMEBUFFER WRITE L
CLS RGB(BLACK)  ' Clear layer with transparent color
DrawPaddleAt INT(px!), INT(py!)
DrawBallAt INT(bx!), INT(by!)

' Draw HUD to framebuffer F
FRAMEBUFFER WRITE F
DrawHUD

' Start timed continuous merge (updates at TICK_MS rate)
FRAMEBUFFER MERGE RGB(BLACK), R, TICK_MS

BeepServe

' ---- Main loop ----
DO
  ' INPUT (Left=130, Right=131, ESC=27, Space=32, P/p=80/112; 1..8 tones)
  k$ = INKEY$
  IF k$ <> "" THEN
    SELECT CASE ASC(k$)
      CASE 130: pvx! = pvx! - PAD_ACCEL
      CASE 131: pvx! = pvx! + PAD_ACCEL
      CASE  32: IF ballLaunched% = 0 THEN ballLaunched% = 1 : BeepServe
      CASE  27: EXIT DO
      CASE 80, 112: ' P or p key - screenshot
        ' Pause rendering and capture full scene
        FRAMEBUFFER MERGE RGB(BLACK), A  ' Abort continuous merge
        PAUSE 50
        FRAMEBUFFER WRITE N
        CLS COL_BG%
        DrawBlocks
        DrawHUD
        DrawPaddleAt INT(px!), INT(py!)
        DrawBallAt INT(bx!), INT(by!)
        IF ballLaunched% = 0 THEN
          TEXT W%\2, H%\2, "Hit SPACE to start", "CT", , , COL_TXT%, RGB(BLACK)
        END IF
        PAUSE 50
        screenshotNum% = screenshotNum% + 1
        SAVE IMAGE "screen" + STR$(screenshotNum%) + ".bmp"
        PAUSE 50
        FRAMEBUFFER WRITE L
        FRAMEBUFFER MERGE RGB(BLACK), R, TICK_MS  ' Resume continuous merge
        PLAY TONE 1200,1200 : PAUSE 30 : PLAY STOP  ' Confirmation beep
      CASE  49 TO 56
        PLAY TONE 220 * (2 ^ ((ASC(k$)-48)/12.0)), 0 : PAUSE 60 : PLAY STOP
    END SELECT
  ENDIF

  ' ---- Paddle physics ----
  pvx! = pvx! * PAD_DECAY
  IF pvx! >  PAD_MAX THEN pvx! =  PAD_MAX
  IF pvx! < -PAD_MAX THEN pvx! = -PAD_MAX
  px! = px! + pvx!
  IF px! < 0 THEN px! = 0 : IF pvx! < 0 THEN pvx! = 0
  IF px! > (W% - pw%) THEN px! = W% - pw% : IF pvx! > 0 THEN pvx! = 0

  ' ---- Ball physics ----
  IF ballLaunched% = 0 THEN
    ' Ball follows paddle until launched
    bx! = px! + pw%/2 - br%
    by! = py! - 2*br% - 2
  ELSE
    ' Gradually increase speed over time (once per second)
    IF TIMER - lastAccelTime% >= 1000 THEN
      IF vy! > 0 THEN
        vy! = vy! + BALL_SPEED_ACCEL_PER_SEC
      ELSE
        vy! = vy! - BALL_SPEED_ACCEL_PER_SEC
      END IF
      lastAccelTime% = lastAccelTime% + 1000
    END IF

    bx! = bx! + vx!
    by! = by! + vy!
  END IF

  ' Elastic wall bounces
  IF INT(bx!) < 0 THEN
    bx! = 0
    vx! = -vx!
    hitTimeout% = 0
    BeepWall
  END IF
  IF INT(bx!) > W% - 2*br% THEN
    bx! = W% - 2*br%
    vx! = -vx!
    hitTimeout% = 0
    BeepWall
  END IF
  IF INT(by!) < HUDH% + 1 THEN
    by! = HUDH% + 1
    vy! = -vy!
    hitTimeout% = 0
    BeepWall
  END IF

  ' ---- Block collision (elastic) ----
  IF ballLaunched% = 1 AND CheckBlockCollision(INT(bx!), INT(by!), br%) = 1 THEN
    vy! = -vy!
    BeepBlock
  END IF

  ' ---- Paddle collision (angle variation based on hit position) ----
  IF ballLaunched% = 1 AND INT(by!) + 2*br% >= INT(py!) AND INT(by!) <= INT(py!) + ph% AND INT(bx!) + 2*br% >= INT(px!) AND INT(bx!) <= INT(px!) + pw% THEN
    by! = INT(py!) - 2*br%
    vy! = -vy!
    hitTimeout% = 0

    ' Calculate where on paddle ball hit (0=left edge, 1=right edge, 0.5=center)
    hitPos! = (bx! + br% - px!) / pw%
    IF hitPos! < 0 THEN hitPos! = 0
    IF hitPos! > 1 THEN hitPos! = 1

    ' Adjust horizontal velocity based on hit position
    ' Center hits = straighter, edge hits = sharper angle
    angle! = (hitPos! - 0.5) * 5.0  ' Range: -2.5 to +2.5
    vx! = vx! + angle! + pvx! * PADDLE_KICK

    score% = score% + 1
    BeepPaddle
  ENDIF

  ' ---- Miss (lose a life) ----
  IF ballLaunched% = 1 AND INT(by!) > H% - 2*br% THEN
    lives% = lives% - 1
    BeepMiss

    IF lives% > 0 THEN
      ' Reset ball to paddle (not launched)
      ResetBall
      BeepServe
    ELSE
      ' Game over
      FRAMEBUFFER MERGE RGB(BLACK), A
      FRAMEBUFFER CLOSE
      FRAMEBUFFER WRITE N
      CLS COL_BG%
      PRINT "GAME OVER!  Score="; score%
      END
    END IF
  ENDIF

  ' ---- Level complete / Win condition ----
  IF blocksLeft% = 0 THEN
    IF currentLevel% < LEVELS% THEN
      ' Advance to next level
      currentLevel% = currentLevel% + 1
      ResetBall

      ' Redraw static background with new level
      FRAMEBUFFER WRITE F
      CLS COL_BG%
      InitBlocks
      DrawBlocks
      DrawHUD
      oldScore% = score%  ' Force HUD update on next frame
      oldLives% = lives%

      BeepServe
    ELSE
      ' All levels complete!
      FRAMEBUFFER MERGE RGB(BLACK), A
      FRAMEBUFFER CLOSE
      FRAMEBUFFER WRITE N
      CLS COL_BG%
      PRINT "YOU WIN ALL LEVELS!  Score="; score%; "  Lives: "; lives%
      END
    END IF
  END IF

  ' ---- Redraw layer with new sprite positions ----
  FRAMEBUFFER SYNC
  FRAMEBUFFER WRITE L
  CLS RGB(BLACK)    ' Clear layer with transparent color
  DrawPaddleAt INT(px!), INT(py!)
  DrawBallAt INT(bx!), INT(by!)
  DrawExplosion     ' Draw explosion animation if active

  ' Show start message when ball not launched
  IF ballLaunched% = 0 THEN
    TEXT W%\2, H%\2, "Hit SPACE to start", "CT", , , COL_TXT%, RGB(BLACK)
  END IF

  ' ---- Update HUD only on score/lives change ----
  IF score% <> oldScore% OR lives% <> oldLives% THEN
    oldScore% = score%
    oldLives% = lives%
    FRAMEBUFFER WRITE F
    DrawHUD
  END IF

  ' ---- Update FPS display ----
  frames% = frames% + 1
  IF TIMER - t0% >= 1000 THEN
    fps$ = STR$(frames%) + " FPS"
    frames% = 0
    t0% = TIMER
    FRAMEBUFFER WRITE F
    DrawHUD
  END IF

  ' No PAUSE needed - continuous merge handles timing
LOOP

' ---- Cleanup ----
FRAMEBUFFER MERGE RGB(BLACK), A  ' Abort continuous merge
FRAMEBUFFER CLOSE
FRAMEBUFFER WRITE N  ' Back to normal screen
CLS COL_BG%
PRINT "Thanks for playing!  Final Score: "; score%
END

1 Like

OPTION CONTINUATION LINES ON

1 Like

I pushed a version that has OPTION CONTINUATION LINES ON, I think this was just set locally on my build by other programs. Sorry about that.