Porting a compact Sinclair BASIC to Boriel BASIC is a lesson in constraint translation: the gameplay ideas are tiny and clear, but timing, memory layout and a few helper primitives change how you implement them. I focused on three goals:
- preserve the original feel (timing, pipe rhythm)
- keep code modular and readable for learners
- make minimal runtime changes where Boriel behaved differently
Below I narrate the main decisions and share small code samples so you can follow along or reuse the techniques.

Why this structure? (short answer)
Keeping input, physics, rendering and collision logically separated made the porting work mostly about tuning constants and a couple of small platform-specific changes (not rewriting gameplay). That separation also makes the code easier to showcase in a blog post: each concept maps to a small function.
The per-frame heartbeat (in plain words)
The game runs a simple, predictable frame loop: read input, update physics, scroll the world, draw the bird, then check collisions and scoring. That ordering keeps behavior deterministic — which helped a lot when I slowed the Boriel build to match Sinclair timing.
Practically speaking, the loop is short and readable and composed of these small helpers: screenSync(), readKeyboard(), gravity(), scroll(), redrawBird(), checkScore(), checkBirdCollision().
Key modules and what changed
definitions.bas— tuning knobs: gravity, pipe geometry and display attributes live here.physics.bas— fixed-point motion; tiny math, easy to tweak.draw.bas— here I separated attribute (color) bytes from pixel data; that choice simplified collision and made erasing/drawing the bird straightforward.collision.bas— attribute-based collision: fast and conceptually simple.
I rarely had to rewrite algorithmic logic; most work was adjusting timings and ensuring the attribute buffer writes matched Boriel's memory mapping.
Scoring & pipe rhythm
Pipes are generated by two interleaved slots using a single worldCol counter. This single timing source keeps pipe drawing, scoring and floor patterning in sync. The result is a predictable rhythm that felt right after a couple of tuning passes.

Rendering and collision — the practical choice
I chose attribute-based collision so we could avoid per-pixel masks on the bird. The bird writes pixels only, leaving attributes unchanged; collision is simply "does the 2×2 attribute box at the bird's position contain anything but ATTR_SKY?".
This design is cheap, robust and very easy to explain to someone reading the code for the first time.

Performance note
Boriel timings required a pragmatic tweak: the Boriel build in this repo is slowed by a factor of 4 so gameplay speed matches expectations from the original. That change is purely a runtime tuning to preserve player experience.
Final thoughts — lessons for porters
- Keep gameplay logic separate from rendering and platform glue.
- Use attribute-based collision on systems with character/attribute layers; it's often simpler and faster.
- Tune constants last — most porting bugs are timing-related, not algorithmic.
Code examples
Below are small, copy-pastable examples extracted from the codebase to illustrate common tasks: showing the play screen (game loop), collision detection, and world scrolling.
Show a screen (game loop)
Sub showPlayGameScreen(clearScreen As Ubyte)
initGame(clearScreen)
Do
screenSync()
readKeyboard()
preserveYPosition()
gravity()
scroll()
redrawBird()
checkScore()
If checkBirdCollision(birdX, Int(birdYPos)) Then
showGameOverScreen()
End If
Loop
End Sub
This sequence is the per-frame update: input → physics → world scroll → render → collision/score.
Collision detection (attribute-based)
Function checkBirdCollision(bx As Ubyte, by As Ubyte) As Ubyte
Dim attrBuf(3) As Ubyte
getPaintData(bx, by, 2, 2, @attrBuf(0))
If attrBuf(0) <> ATTR_SKY Then Return 1
If attrBuf(1) <> ATTR_SKY Then Return 1
If attrBuf(2) <> ATTR_SKY Then Return 1
If attrBuf(3) <> ATTR_SKY Then Return 1
Return 0
End Function
Collision is computed by reading the 2×2 attribute bytes at the bird's tile and checking for anything different than ATTR_SKY.
World scroll (attributes + last column)
Implementation detail: when scrolling the playfield we shift attribute bytes row-by-row (using MemMove per row) instead of moving the whole attribute memory at once. Doing the move per row avoids transient "garbage" appearing in the right-most column due to overlapping memory regions during the shift.
Sub scrollPlayfieldAttrs()
Dim row As Ubyte
Dim src As UInteger = $5821
Dim dst As UInteger = $5820
For row = 0 To 23
MemMove(src, dst, 31)
src = src + 32
dst = dst + 32
Next row
End Sub
Sub paintLastColumn()
Dim wc As Ubyte = worldCol Mod PIPE_PERIOD
Dim attribute As Ubyte = ATTR_PIPE
Dim pipeLastCol As Ubyte = PIPE_WIDTH - 1
If wc < PIPE_WIDTH Then
If wc = pipeLastCol Then attribute = ATTR_PIPE_SHADOW
writePipeColumn(31, pipeGap(0), attribute)
Return
End If
If wc >= PIPE_SPAWN_INTERVAL Then
If wc < PIPE_SPAWN_INTERVAL + PIPE_WIDTH Then
If wc - PIPE_SPAWN_INTERVAL = pipeLastCol Then attribute = ATTR_PIPE_SHADOW
writePipeColumn(31, pipeGap(1), attribute)
Return
End If
End If
writeSkyColumn(31)
End Sub
Sub scroll()
scrollPlayfieldAttrs()
paintLastColumn()
worldCol = worldCol + 1
End Sub
The code shifts the attribute buffer left, paints a new right-most column (pipe or sky) based on worldCol, and increments the global column counter.
Drawing / erasing the bird (pixels only)
Sub eraseBird(bx As Ubyte, by As Ubyte)
putChars(bx, by, 2, 2, @blankSprite(0))
End Sub
Sub drawBird()
putChars(birdX, Int(birdYPos), 2, 2, @sprite0(0))
End Sub
Sub redrawBird()
waitretrace
eraseBird(birdX, birdOldY)
drawBird()
End Sub
Note: drawing only writes pixel bytes; attributes are preserved for correct collision detection. A waitretrace is used before redrawBird() to avoid sprite flicker when erasing and drawing the sprite.
More information
For more information: github.com/rtorralba/flapper-boriel