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


Let’s Write a Lexer in PlayBASIC

October 12, 2025

 

Logo

Introduction

Welcome back, PlayBASIC coders!

In this live session, I set out to build something every programming language and tool needs — a lexer (or lexical scanner). If you’ve never written one before, don’t worry — this guide walks through the whole process step by step.

A lexer’s job is simple: it scans through a piece of text and classifies groups of characters into meaningful types — things like words, numbers, and whitespace. These little building blocks are called tokens, and they form the foundation for everything that comes next in a compiler or interpreter.

So, let’s dive in and build one from scratch in PlayBASIC.


Starting with a Simple String

We begin with a test string — just a small bit of text containing words, spaces, and a number:

s$ = "   1212123323      This is a message number"
Print s$

This gives us something to analyze. The plan is to loop through this string character by character, figure out what each character represents, and then group similar characters together.

In PlayBASIC, strings are 1-indexed, which means the first character is at position 1 (not 0 like in some other languages). So our loop will run from 1 to the length of the string.


Stepping Through Characters

The core of our lexer is a simple `For/Next` loop that moves through each character:

For lp = 1 To Len(s$)
    ThisCHR = Mid(s$, lp)
Next

At this stage, we’re just reading characters — no classification yet.

The next question is: how do we know what type of character we’re looking at?


Detecting Alphabetical Characters

We start by figuring out if a character is alphabetical. The simplest way is by comparing ASCII values:

If ThisCHR >= Asc("A") And ThisCHR <= Asc("Z")
    ; Uppercase
EndIf

If ThisCHR >= Asc("a") And ThisCHR <= Asc("z")
    ; Lowercase
EndIf

That works, but it’s messy to write out in full every time. So let’s clean it up by rolling it into a helper function:

Function IsAlphaCHR(ThisCHR)
    State = (ThisCHR >= Asc("a") And ThisCHR <= Asc("z")) Or _
            (ThisCHR >= Asc("A") And ThisCHR <= Asc("Z"))
EndFunction State

Now we can simply check:

If IsAlphaCHR(ThisCHR)
    Print Chr$(ThisCHR)
EndIf

That already gives us all the letters from our string — but one at a time.

To make it more useful, we’ll start grouping consecutive letters into words.


Grouping Characters into Words

Instead of reacting to each character individually, we look ahead to find where a run of letters ends. This is done with a nested loop:

If IsAlphaCHR(ThisCHR)
    For ChrLP = lp To Len(s$)
        If Not IsAlphaCHR(Mid(s$, ChrLP)) Then Exit
        EndPOS = ChrLP
    Next
    ThisWord$ = Mid$(s$, lp, (EndPOS - lp) + 1)
    Print "Word: " + ThisWord$
    lp = EndPOS
EndIf

Now our lexer can detect whole words — groups of letters treated as a single unit.

That’s the first real step toward tokenization.


Detecting Whitespace

The next type of token is whitespace — spaces and tabs.

We’ll build another helper function:

Function IsWhiteSpace(ThisCHR)
    State = (ThisCHR = Asc(" ")) Or (ThisCHR = 9)
EndFunction State

Then use the same nested-loop pattern:

If IsWhiteSpace(ThisCHR)
    For ChrLP = lp To Len(s$)
        If Not IsWhiteSpace(Mid(s$, ChrLP)) Then Exit
        EndPOS = ChrLP
    Next
    WhiteSpace$ = Mid$(s$, lp, (EndPOS - lp) + 1)
    Print "White Space: " + Str$(Len(WhiteSpace$))
    lp = EndPOS
EndIf

Now we can clearly see which parts of the string are spaces and how many characters each whitespace block contains.


Detecting Numbers

Finally, let’s detect numeric characters using another helper:

Function IsNumericCHR(ThisCHR)
    State = (ThisCHR >= Asc("0")) And (ThisCHR <= Asc("9"))
EndFunction State

And apply it just like before:

If IsNumericCHR(ThisCHR)
    For ChrLP = lp To Len(s$)
        If Not IsNumericCHR(Mid(s$, ChrLP)) Then Exit
        EndPOS = ChrLP
    Next
    Number$ = Mid$(s$, lp, (EndPOS - lp) + 1)
    Print "Number: " + Number$
    lp = EndPOS
EndIf

Now we can identify three types of tokens:

Words (alphabetical groups)

Whitespace (spaces and tabs)

Numbers (digits)


Defining a Token Structure

Up to this point, our program just prints what it finds.

Let’s store these tokens properly by defining a typed array.

Type tToken
    TokenType
    Value$
    Position
EndType
Dim Tokens(1000) As tToken

We’ll also define some constants for readability:

Constant TokenTYPE_WORD        = 1
Constant TokenTYPE_NUMERIC     = 2
Constant TokenTYPE_WHITESPACE  = 4

As we detect tokens, we add them to the array:

Tokens(TokenCount).TokenType = TokenTYPE_WORD
Tokens(TokenCount).Value$    = ThisWord$
TokenCount++

Do the same for whitespace and numbers, and our lexer now builds a real list of tokens as it runs.


Displaying Tokens by Type

To visualize the result, we can print each token in a different colour:

For lp = 0 To TokenCount - 1
    Select Tokens(lp).TokenType
        Case TokenTYPE_WORD:       c = $00FF00 ; green
        Case TokenTYPE_NUMERIC:    c = $0000FF ; blue
        Case TokenTYPE_WHITESPACE: c = $000000 ; black
        Default:                   c = $FF0000
    EndSelect

    Ink c
    Print Tokens(lp).Value$
Next

When we run this version, we see numbers printed in blue, words in green, and whitespace appearing as black gaps — exactly how a simple syntax highlighter or compiler front-end might visualize tokenized text.


Wrapping Up

And that’s it — our first lexer!

It reads through a line of text, classifies what it finds, and records each token type for later use.

The same process underpins many systems:

Compilers use it as the first step in parsing code.

Adventure games might use it to process typed player commands.

Expression evaluators or script interpreters rely on it to break down formulas and logic.

The big takeaway? A lexer doesn’t have to be complicated.

This simple approach — scanning text, detecting groups, and tagging them — is the heart of it. Once you understand that, you can expand it to handle symbols, punctuation, operators, and beyond.

If you’d like to see more about extending this lexer or turning it into a parser, let me know in the comments — or check out the full live session on YouTube.

Links:

  • PlayBASIC,com
  • Learn to basic game programming (on Amazon)
  • Learn to code for beginners (on Amazon)