Mini Golf Demo – Conceptual Overview

February 21, 2026

 

Mini Golf Demo – Conceptual Overview

This demo shows a simple but powerful way to build a top-down mini-golf style simulation in PlayBASIC.

Rather than focusing on complex physics, the code demonstrates how to combine a few clean ideas to create believable behaviour using fast, data-driven techniques.


1. Separating visuals from gameplay logic

The program uses two images of the same size, each with a different purpose:

• Visible Screen

This is what you actually see: grass, shapes, and moving balls.

• Zone Screen

This image is never shown to the player. Each pixel stores a zone value that tells the code what type of surface the ball is currently rolling over.

This is a very common game-dev trick:

images aren’t just for graphics – they can also be fast lookup maps for gameplay data.


2. Zone-based friction using lookup tables

Each terrain type (short grass, medium grass, long grass) is assigned:

• A zone ID

• A friction value

• A display colour

When a ball moves, the code reads the pixel value from the zone screen at the ball’s position and uses that value as an index into a friction array.

This avoids large blocks of conditionals and makes it easy to add or tweak terrain types later.


3. Direction-based movement

Each ball stores its:

• Position

• Angle

• Speed

Movement is calculated using basic trigonometry (sin and cos) to convert angle and speed into X and Y motion.

The friction value from the current zone scales the speed, giving the impression of different grass lengths slowing the ball down.


4. Ray-based collision and reflection

Instead of moving the ball first and checking overlaps later, the code uses ray intersection:

• A ray is cast from the current position to the next position

• If it hits geometry, the surface normal is used to calculate a reflection angle

• The ball bounces naturally off walls and obstacles

This method is robust, avoids tunnelling, and works well even at higher speeds.


5. Vector-based world geometry

The course is built from vector lines and convex shapes, not tiles.

• Borders are simple line segments

• Obstacles are procedurally generated convex polygons

• The world is partitioned to improve collision performance

This keeps the layout flexible and easy to expand or randomise.


6. Data-driven design

One of the key ideas in this demo is data-driven gameplay:

• Terrain behaviour is defined by data, not hard-coded logic

• Friction, colour, and movement all come from lookup tables

• New terrain types can be added with minimal code changes

This approach scales well and is widely used in real game engines.


Summary

At its core, this demo demonstrates how to:

• Separate visuals from logic

• Use images as gameplay data

• Apply simple physics with believable results

• Handle collision using raycasting

• Build systems that are easy to extend

It’s a compact example of practical game programming techniques, written in a way that new coders can understand and build upon.



  Constant Zone_ShortGrass    =ac(1)
  Constant Zone_MedGrass        =ac(1)
  Constant Zone_LongGrass        =ac(1)


  Dim Palette(100)
  Palette(Zone_ShortGrass)      = rgb(55,200,80)
  Palette(Zone_MedGrass)          = rgbfade(Palette(Zone_ShortGrass),50)
  Palette(Zone_LongGrass)          = rgbfade(Palette(Zone_ShortGrass),25)
 
  dim Friction#(100)
  Friction#(Zone_ShortGrass)    =1
  Friction#(Zone_MedGrass)      =1.5
  Friction#(Zone_LongGrass)      =2
 
 
  ; get the screen size 
  sw=GetScreenWidth()
  sh=GetScreenHeight()

  ; create the visible screen
  DisplayScreen      =NewIMage(sw,sh,true)

  ; create the zone screen.
  ; the zone screen is the same size as the 'visible'
  ; but is just used to tell what zone the ball is currently over
  ZoneScreen        =GetFreeimage()
  createfximageex ZoneScreen,sw,sh,32
 

  // Draw the Visible scene..
  rendertoimage Displayscreen
  c=Palette(Zone_ShortGrass)
  shadebox 0,0,sw,sh,c,rgbfade(c,80),c,rgbfade(c,80)

  inkmode 1+32 
  ellipsec sw*0.7,sh*0.5,200,100,true,Palette(Zone_MedGrass)     
  ellipsec sw*0.7,sh*0.5,100,50,true,Palette(Zone_LongGrass)     
  inkmode 1


  // draw the collision (zone) scene. 
  rendertoimage Zonescreen
  Cls Zone_ShortGrass
  ellipsec sw*0.7,sh*0.5,200,100,true,Zone_MedGrass     
  ellipsec sw*0.7,sh*0.5,100,50,true,Zone_LongGrass     
 


  // create collision world
  CollisionWorld=CreateVectorWorld()
 

 
  rendertoscreen


  camera=newcamera()
  cameracls camera,off

  setfps 60



  Type tBall
        x#,y#,angle#,speed#
  endType

  Dim Ball as tball List

    AddNewBallTime=0

  Do

        CT = Timer() and $7fffffff
      if CT>AddNewBallTime
            ball = new Tball
            ball.x#=rnd(sw)
            ball.y#=rnd(sh)
            Ball.Angle#=rnd(360)
            Ball.speed#=rndrange(2,10)
            AddNewBallTime=CT+1000
      endif

       
       

      capturetoscene
      clsscene
     
      CaptureDepth 100

      CameraGrabWorld Camera,CollisionWorld

      DrawImage DisplayScreen,0,0,false

      CaptureDepth 10

      rendertoimage ZoneScreen

      for each Ball()

        ; check what zone the ball is currently over.       
        ZoneType=Point(Ball.x#,Ball.y#) and 255

        ; get the amount of friction this zone has
        Friction#=Friction#(ZoneType)
 
        ; move the ball 
        Speed#=Ball.Speed#*1/Friction#

        newx#=ball.x#+(cos(Ball.angle#)*speed#*1)
        newy#=ball.y#+(sin(Ball.angle#)*speed#*1)

        if RayIntersectWOrld(CollisionWorld,Ball.x#,Ball.y#,NewX#,NewY#)=true
              x2#  =getintersectx#(0)
              y2#  =getintersecty#(0)
              ; Calc reflection Direction
              WallAngle#=AtanFull(getnormaly#(0),getnormalx#(0))
              RayAngle#=GetAngle2d(x2#,y2#,ball.x#,ball.y#)
              Ball.Angle#=WrapAngle(WallAngle#,WallAngle#-RayAngle#)

              ball.x#=x2#
              ball.y#=y2#
           
        else
              ball.x#=newx#
              ball.y#=newy#
        endif

        ; draw the circle to represent the ball
        circle ball.x#,ball.y#,10,true
      next

      drawcamera camera
      Sync
  loop





Function CreateVectorWorld()
  WorldWidth=GetScreenWidth()
  WorldHeight=GetScreenHeight()


; Create world
  World=NewWorld()
  CaptureToWorld World

; draw a series of boarder line for this world
  line 0,0,worldwidth,0
  line worldwidth,0,worldwidth,worldheight
  line worldwidth,worldheight,0,worldheight
  line 0,worldheight,0,0

  for lp=0 to 7
      x=100+lp*80
      y=100+lp*80
      Make_Convex(4,x,y,50,90+lp*20)
  next

  Make_Convex(6,700,100,90,70)

  PartitionWorld World,32
  DrawGFXImmediate

EndFunction World



  ; This function creates a convex polygon shape

Function Make_Convex(edges,xpos#,ypos#,Size,angle)
      sa#=360.0/edges
      c=rndrgb()
      for lp=0 to edges-1
        a#=angle+(lp*sa#)
        x1#=xpos#+cosRadius(a#,size)
        y1#=ypos#+SinRadius(a#,size)
        if lp<(edges-1)
            a#=angle+((lp+1)*sa#)
        else
            a#=angle
        endif
        x2#=xpos#+cosRadius(a#,size)
        y2#=ypos#+SinRadius(a#,size)
        line x2#,y2#,x1#,y1#
      next lp
endfunction i


At Least We Got Rid of Line Numbers

February 07, 2026

 

At Least We Got Rid of Line Numbers

Modern programming is stressful.

We’ve got frameworks that change every five minutes, dependencies that break themselves for sport, and build systems that require more configuration than a NBN router in 2011.

Some days it feels like software development is just trying to keep a wobbly house of cards standing in a cyclone.

But at least we got rid of line numbers.


A Stress New Programmers Never Knew

There’s an anxiety older coders remember that modern developers will never experience.

Being asked to write real software in a language that required human-generated line numbers.

  • Not line numbers in an editor.
  • Not line numbers for debugging.
  • Actual:

    10 PRINT "HELLO"
    20 GOTO 10
    

    style line numbers.

    If you never had to live like that, congratulations! You skipped a very specific form of programmer misery.


    Coding With Extra Headaches

    Programming was already hard enough.

    You had to think about logic, bugs, memory limits, and whether the computer was going to throw a tantrum.

    And on top of that, you also had to manage imaginary addresses in your head.

    “Right… I’ll start this routine at 3000. That should be plenty of space.”

    It was never plenty of space.

    Add too much code and suddenly you were out of numbers, half your GOTOs were broken, and your “quick change” had just turned into an all-night renumbering party.

    Good times.


    Refactoring Was Basically a Weekend Plan

    Today, refactoring is normal. Back then it was an event!

  • Coffee was made.
  • Backups were taken.
  • Deep breaths were required.
  • Because moving code around wasn’t just tidying things up, it'ss like trying to renovate a house while it was still on fire.

    You didn’t casually improve programs.

    You stared at them and thought:

    “Is this feature really worth ruining my Saturday?”


    Progress… Sort Of

    Of course, modern development replaced those problems with new ones.

    Now we fight with containers, build pipelines, dependency hell, and error messages that look like they were generated by an angry robot having a bad day.

    In many ways we swapped small simple problems for giant complicated ones.

    But still…


    Small Victories Matter

    Whenever I’m knee-deep in some ridiculous modern tech disaster, I try to remember:

  • At least I can add a line of code without planning its numeric future.
  • At least I can move a function without breaking half the program.
  • At least my editor doesn’t force me to think like an accountant.
  • Programming is still stressful.
  • But thank whatever digital gods look after developers…

    At least we got rid of line numbers.

    Building a Procedural Rope Effect in PlayBASIC

    January 29, 2026

     


    Building a Procedural 'Rope' Effect in PlayBASIC

    Scanlines, Gouraud Shading, and Pixel Format Testing

    At first glance, this demo looks like a twisting, three-dimensional rope floating over a smoothly scrolling, colour-banded background. It animates fluidly, reacts to user input, and even lets you switch pixel formats in real time.

    What makes it interesting is how little machinery is actually involved.

    There’s no 3D engine.

  • No meshes.
  • No textures.
  • No z-buffer.
  • Instead, the entire effect is built using 2D horizontal gouraud strips, some trigonometry, and a deliberate approach to colour interpolation. This makes it not only visually appealing, but also an excellent example of how PlayBASIC encourages efficient, low-level graphics thinking.


    What This Demo Is Designed to Show

    This program serves three purposes at once:

  • 1. Demonstrate how complex, organic effects can be built from simple 2D primitives
  • 2. Showcase PlayBASIC’s gouraud rendering and colour interpolation
  • 3. Stress-test different pixel formats (15, 16, 24, and 32-bit) using the same effect
  • You can switch pixel formats at runtime using F5–F8, and press Space to increase the number of edges used to construct the rope.


    Rendering to an FXImage (Why It Matters)

    Rather than drawing directly to the screen, the demo renders everything into an FXImage.

    This allows the program to:

  • Recreate the render target on demand
  • Change pixel depth without restarting
  • Ensure the exact same effect is rendered under different colour formats
  • When the user presses one of the function keys, the FXImage is deleted and recreated at the requested depth. The rendering logic itself doesn’t change — only the underlying pixel format does.

    This makes visual differences between colour depths immediately obvious, especially in gradients and gouraud shading.


    The Background: Colour Bands Built One Scanline at a Time

    The background consists of vertically repeating colour bands that slowly scroll upward. Each band blends smoothly into the next, creating a soft, continuous gradient.

    Pleasant Colours via Golden Ratio Stepping

    Instead of random colours, the palette is generated using a golden-ratio hue step. This ensures evenly spaced hues around the colour wheel and avoids clusters or muddy transitions.

     `NiceRGB()` advances the hue by ~0.618 each step, producing visually balanced colour sequences.
    

    Horizontal Gouraud Strips

    Each scanline is rendered using multiple horizontal gouraud strips, each with its own start and end colour. Because this happens line-by-line, the result is a smooth vertical gradient with excellent control over colour precision.

    This alone already provides a strong test of pixel formats:

  • Lower bit depths show banding earlier
  • Higher depths preserve smoother blends

  • The Rope: A 3D Illusion Built from 2D Spans

    The rope itself looks three-dimensional, but it isn’t.

    At its core, the rope is a rotating ring built from multiple edges arranged in a circle. Each edge is treated as a point in 3D space (X and Z), then projected into screen space.

     Screen X = `(X * focal_length) / Z`
    

    Each pair of adjacent edges becomes a horizontal span — a left and right X coordinate with a colour at each end.

    Only spans that face the camera (left X ≤ right X) are kept, which naturally removes back-facing segments without explicit clipping logic.


    Fake Lighting via Depth-Scaled Colour

    There’s no lighting model in this demo — and it doesn’t need one.

    Instead, colour brightness is scaled based on depth:

    > RGB values are multiplied by a constant and divided by Z
    

    As a segment moves farther away, its colour darkens. As it moves closer, it brightens. This simple relationship produces a convincing sense of depth and curvature, especially when combined with smooth interpolation.

    Because this happens before the colour is packed into the target pixel format, it also makes colour precision differences between formats very obvious.


    One Rope, Many Scanlines

    A key optimisation in this demo is that the rope geometry is calculated once per frame, not once per scanline.

    The projected spans are stored in an array, and then reused for every scanline. The illusion of movement comes from shifting those spans horizontally as the screen is drawn.

    This is where the rope gets its organic motion.


    Motion Through Layered Waves

    Instead of bending geometry, the demo applies several cosine-based offsets per scanline:

  • A slow, wide wave
  • A faster, tighter wave
  • Additional ripples layered on top
  • Each scanline adds these offsets together, producing a flowing, ribbon-like motion that feels far more complex than it really is.

     Geometry stays the same — only the X offset changes per scanline.
    

    This technique is extremely efficient and works beautifully with scanline-based rendering.


    Why Horizontal Gouraud Strips Are Ideal Here

    Every part of this demo is built around horizontal gouraud strips, and that’s very intentional.

    They are:

  • Cache-friendly
  • Deterministic
  • Perfectly suited to scanline effects
  • Consistent across all pixel formats
  • Using the same primitive for both the background and the rope ensures consistent behaviour and makes the demo easier to reason about.


    Scaling the Effect

    Pressing Space increases the number of rope edges. This:

  • Increases the number of spans
  • Adds visual complexity
  • Stresses colour interpolation further
  • Yet the core rendering logic remains unchanged. The effect scales naturally, which is a hallmark of a good procedural design.


    Final Thoughts

    This demo is a great example of PlayBASIC’s strengths:

  • Low-level control without unnecessary complexity
  • Powerful 2D primitives that can fake 3D convincingly
  • A rendering model that encourages efficient thinking
  • Most importantly, it shows that good visual effects are about understanding perception, not just throwing more technology at the problem.


    Full Source Code

    The complete PlayBASIC source code for this demo is included below for those who want to explore it in detail, experiment with it, or adapt the techniques for their own projects.

    Happy coding — and enjoy bending pixels the old-school way.



    	; This is pixel format test version of this demo
    
    	; Function keys f5/f6/f7/f8 to set the display depth
    	;  Space key to add more sides of objects
    
    
    	; open a tall strip like screen for the short
    	openscreen 800,1000,32,1
    
    	PositionScreen GetSCreenXpos(),0
    
    	; Load a new bigger font over the default font
       LoadFont "Courier New",1,48,0,8
       ; set the font rendering mode of font #1 to alpha blend
       fontdrawmode 1,1
    
    	; force a known randomize sequence by seeding  the generator
    	randomize 8262
    
       Global RopeEdges=3
    
    	Dim WaveRippleAngle#(10)
    
    	ScreenWidth  =GetScreenWidth()
    	ScreenHeight  =GetScreenHeight()
    
    	ScreenDepth=32
    	Screen=		NewFXImage(ScreenWidth,ScreenHeight)
    
    	Type RopeVerts
    		X1#,X2#, rgb1,rgb2
    	endtype
    
    
    
    
    For Tests=0 to 20
    
    
    	; init the palette
    	Dim Palette(RopeEdges+2)
    	For lp=0 to RopeEdges-1
    		Palette(lp) =NiceRGB(lp,0.25,0.90)
    	next
    	Palette(lp+1) =Palette(0)
    
    
       Dim RopeSegs(Ropeedges) as Ropeverts
    
    	Do
    
    			if functionkeys(5) then	ScreenDepth = 15
    			if functionkeys(6) then	ScreenDepth = 16
    			if functionkeys(7) then	ScreenDepth = 24
    			if functionkeys(8) then	ScreenDepth = 32
    
    			if GetIMageDepth(Screen) <> ScreenDepth
    					deleteimage Screen
    					Screen =GetFreeImage()
    					CReateFXImageEX Screen,GetScreenWidth(),GetScreenHeight(),ScreenDepth
      			endif
    
    			rendertoimage Screen
    
    
    		// backdrop
    		X1=ScreenWidth *0.00
    		X2=ScreenWidth *0.25
    		X3=SCreenWidth *0.50
    		X4=SCreenWidth *0.75
    		X5=SCreenWidth *1.00
    
    		lockbuffer
    			PaletteBandSize=50
    			ThisRGB1=Palette(0)
    			for ylp=0 to getscreenheight()-1
    
    				YOffset = mod(Ylp+ScrollY,PaletteBandSize*(RopeEdges-1))
    
    				Index = mod(yoffset/PaletteBandSize,(RopeEdges-1))
    				ThisRGB1=Palette(Index)
    				ThisRGB2=Palette(Index+2)
    
    				//  Scale /blend level between bands
    				Scale# = (Yoffset-(PaletteBandSize*Index)) / float(PaletteBandSize)
    
    				ThisRGB	 =  RgbAlphaBlend(ThisRGB1,ThisRGB2, Scale#*80)
    				ThisRGB50 = Rgbfade(ThisRGB,50)
    				ThisRGB20 = Rgbfade(ThisRGB,10)
    
    				GouraudStripH x1,ThisRGB,X2,ThisRGB50,Ylp
    				GouraudStripH x2,ThisRGB50,X3,ThisRGB20,Ylp
    				GouraudStripH x3,ThisRGB20,X4,ThisRGB50,Ylp
    				GouraudStripH x4,ThisRGB50,X5,ThisRGB,Ylp
    
    			next
    
    			DrawRope()
    
    		unlockbuffer
    		ScrollY=mod(ScrollY+1,PaletteBandSize*(RopeEdges-1))
    
    		renderimage Screen,0,0
    
    
    		Sync
    loop spacekey()
    
    RopeEdges++
    	flushkeys
    next
    
    	end
    
    
    
    Function DrawRope()
    
    	Static RadiusAngle
    	RadiusAngle=wrapangle(RadiusAngle +1)
    
       radius=250+cos(RadiusChange)*50
       ScReenHeight=GetScreenHeight()
    	basex=GetScreenwidth()/2
    
    	//  step the global angles
    		AngleStep=2
    		WaveAngle#= WaveRippleAngle#(1)
    		WaveAngle2#= WaveRippleAngle#(2)
    		WaveAngle3#= WaveRippleAngle#(3)
    		WaveAngle4#= WaveRippleAngle#(4)
    
    		WaveRippleAngle#(1)=wrapangle(WaveAngle#,0.7*AngleStep)
    		WaveRippleAngle#(2)=wrapangle(WaveAngle2#,1.2*AngleStep)
    		WaveRippleAngle#(3)=wrapangle(WaveAngle3#,2.1*AngleStep)
    		WaveRippleAngle#(4)=wrapangle(WaveAngle4#,-0.4*AngleStep)
    
    	EdgeAngleStep#=360.0/(RopeEdges)
    
    	BaseZ=1000
    
    	Segments=0
    		static BaseAngle#
    
    		ThisRGB1 = Palette(RopeEdges)
    
    		For lp=0 To RopeEdges-1
    			ThisRGB2 = Palette(lp+1)
    
    			Angle#=BaseAngle#+(EdgeAngleStep#*lp)
    			Angle2#=Angle#+EdgeAngleStep#
    
    
    			X1#=Cosradius(angle#,Radius)
    			X2#=CosRadius(angle2#,Radius)
    			Z1#=basez+SinRadius(angle#,Radius)
    			Z2#=basez+SinRadius(angle2#,Radius)
    
    			Px1#=((X1#*400)/z1#)
    			Px2#=((X2#*400)/z2#)
    
    			If pX1#<=pX2#
    
    				R=RgbR(ThisRgb1)
    				G=RgbG(ThisRgb1)
    				B=RgbB(ThisRGB1)
    
    				R1=(R*500)/z1#
    				G1=(G*500)/z1#
    				B1=(B*500)/z1#
    				R=RgbR(ThisRgb2)
    				G=RgbG(ThisRgb2)
    				B=RgbB(ThisRGB2)
    
    				R2=(R*500)/z2#
    				G2=(G*500)/z2#
    				B2=(B*500)/z2#
    
    				RopeSegs(Segments).x1=px1#
    				RopeSegs(Segments).x2=px2#
    				RopeSegs(Segments).rgb1=rgb(r1,g1,b1)
    				RopeSegs(Segments).rgb2=rgb(r2,g2,b2)
    				inc segments
    			EndIf
    
    			ThisRGB1=ThisRGB2
    		Next
    
    	dec segments
    
    	For ThisScanLine=0 To ScreenHeight-1
    		Xoffset=basex+cosradius(wAVEangle#,50)
    		Xoffset=Xoffset+cosradius(wAVEangle2#+wAVEangle#,40)
    		Xoffset=Xoffset+cosradius(wAVEangle3#,80)
    		Xoffset=Xoffset+cosradius(wAVEangle4#,30)
    
    		For lp=0 to Segments
    				GouraudStripH Xoffset+RopeSegs(lp).x1,RopeSegs(lp).rgb1,Xoffset+RopeSegs(lp).x2,RopeSegs(lp).rgb2,ThisScanline
    	;			GouraudStripV Xoffset+RopeSegs(lp).x1,RopeSegs(lp).rgb1,Xoffset+RopeSegs(lp).x2,RopeSegs(lp).rgb2,ThisScanline
    		next
    		waveangle#	+=	0.15
    		waveangle2#	+=	0.25
    		waveangle3#	+=	0.35
    		waveangle4#	+= 0.33
    
    	Next
    
     	BaseAngle#++
    EndFunction
    
    
    
    
    ; ===============================================================
    ;   Golden Ratio based pleasant random colors
    ;   - Each call with incrementing index gives nicely spaced hues
    ; ===============================================================
    	constant GoldenRatio# = 0.6180339   ; ˜ (v5 - 1) / 2
    
    Function NiceRGB(Index=0, Saturation#=0.75, Value#=0.88)
    	 Local Hue#
    
        ; Start from a random base hue, then walk by golden ratio
        ; (mod 1.0 to wrap around colour wheel)
        Hue# = mod(Rnd#(1) + (Index * GoldenRatio#),1)
        Hue# = Hue# - Floor(Hue#)           ; fractional part only (mod 1)
    
        ; Optional: You can randomise S/V a little for more variety
         Saturation# = Saturation# + RndRange#(-0.15, 0.15)
         Value#      = Value#      + RndRange#(-0.1,  0.1)
        ; Clamp them
        If Saturation# < 0.4 Then Saturation#=0.4
        If Saturation# > 0.98 Then Saturation#=0.98
        If Value#      < 0.5 Then Value#=0.5
        If Value#      > 0.98 Then Value#=0.98
    
    	#if playbasicversion >=165
    	    Result = HSV2RGB(Hue#, Saturation#, Value#)
    	#else
    	    	Result=rndrgb()
    	#Endif
    EndFunction Result
    
    
    
    
    
    Function RenderImage(ThisImage,xpos,ypos,Mode=false)
    	if GetIMageStatus(ThisIMAGE)
    
    			inkmode 1+2048
    			boxc 00,18,275,112,true,$a0a0a0
    			inkmode 1
    
    
    			Xpos+=10
    			Ypos+=20
    			ink $80fFFFFF
    			Text XPos,Ypos		,"  Fps:"+Str$(fps())
    			Text XPos,Ypos+40	,"Depth:"+Str$(GetIMageDepth(ThisIMage))
    
    			rendertoscreen
    			lockbuffer
    				drawimage ThisImage,0,0,false
    			unlockbuffer
    
    	endif
    EndFunction