The RetroRoadtrip Engine

RetroRoadtrip, the game I am building, runs on its own small engine, written for the classic Amiga chipset: 68000, OCS, at most 512 KB of chip RAM, 320 × 256 in 32 colours, logic at a steady 50 Hz. A portable C core decides everything; a thin layer on top of ACE only carries out its decisions on the hardware.

No level is code. Each of the game’s 28 levels (30 files with the editor’s demo and a measuring bench) is a data file: a tile map, placed pieces, people, and a small script that says what happens. They are all made in the game’s own level editor, a Python program that also checks every level against the machine’s limits before the game ever loads it.

The game is in closed alpha testing. This post is about what is under the hood, and about the plans behind it.

The game

RetroRoadtrip is a satirical top-down game about hauling a retro computer to a computing fair. The real bosses are not monsters: they are German infrastructure, fragile hardware, and pushy visitors at the fair.

It plays with one fire button. A tap opens a conversation and solves an obstacle peacefully, which costs time and nerves. Three meters pull against each other: Wanted, Mental and Hardware. Save time and you lose nerves; spare your nerves and you escalate; move carefully and you lose time. When the nerves run out, the character loses it in a rant told like a comic panel. The trip starts with a budget of 1,000 €.

A run has four phases:

  1. Preparation. Pick a class: an old computer with a tube monitor, a big tower, a portable in a case, or a Raspberry Pi. The choice sets the figure’s size, inertia and speed, how much a bump hurts the hardware, and how much respect it earns at the fair.
  2. The journey, which is the boss fight: by train through stations and carriages, or by car on the motorway.
  3. At the fair: a hall of stands and people, your own table, a talk to give.
  4. The way home, and the highscore.

The humour is deliberately local: public transport frustration, station architecture, the particular tone of German service announcements. The texts are written in German first; the English version is a localisation, not a word-for-word translation.

A station hall in the game, with the meters and the budget in the strip on top
On foot: a station hall, the meters and the budget in the strip on top. Shown at three times the Amiga’s 320 × 256.

At a glance

A level is data: the editor writes it, the core runs it
A level is data: the editor writes it, the core runs it

Everything above the Amiga box runs on a PC and is checked there; the Amiga only loads finished files, and of the two halves on it, only the core decides.

Why a real Amiga

I did not want a retro-styled game for modern machines; I wanted a game for the actual machine. That decision came first, and almost every other one follows from it.

One target, the weakest one. Chipset and CPU have a single target configuration: OCS/ECS and a 68000. Chip RAM is capped at 512 KB, what every OCS Amiga has, even on machines with more. Besides that, the game needs a hard disk and 2 MB of other memory.

What was dropped, on purpose:

  • RTG graphics cards, because they have no hardware scrolling.
  • AGA features, to keep the test matrix to one line. A finer palette would have been pretty but not relevant to play, and it would have cost a second test path. The game must still run on an A1200 or A4000; it just does not use AGA.
  • Floppies. The game ships as a hard-disk archive only: one drawer with every level and every language, started from Workbench or the shell. From the hard disk it needs 512 KB of chip RAM plus 2 MB of other memory, and it checks both at start.

One limit never moves. That is the 512 KB of chip RAM. When it runs short, content is cut or shrunk instead: that decides how many people walk on screen at once, how many frames an animation gets, and how much chip RAM the music may take. Other limits have moved as the game grew; the memory beside the chip RAM went from 1 MB to 2 MB.

One clock. Game logic runs at a steady 50 Hz regardless of the display, so a run on a PAL machine and one on an NTSC machine stay comparable.

Core and platform

The engine is split in two, and the split is enforced by the build. The logic core (src/core) is plain C11 with no hardware access: movement and collision, the camera, the tile cache, the meters and the money, dialogue, the level script, text wrapping, the HUD’s layout. The platform layer (src/platform/amiga) draws, plays and reads the joystick, and decides nothing.

Four rules I set in the technical design hold it together:

  1. The core makes no Amiga call. Its build target has no include path to ACE, so a stray header fails at compile time.
  2. If it contains a branch, it belongs in the core. The platform draws the figure it is told to draw; it never picks which one.
  3. The core is written for the 68000, not generically: no floating point, no division in the frame path, tables instead of multiplication, 16-bit integers and fixed point. A check in the fast CI rejects floating point, runtime division and display strings in src/core.
  4. No text in code. Every visible string comes from a language pack by ID, so the core holds no names at all.

Two halves, one header

The two halves meet in one header of plain function declarations, swapped at link time rather than through function pointers, which would cost an indirect jump per call on the 68000. The Amiga build links the real implementation; the host build links a record backend that logs what it was asked to do. That is what makes an Amiga game testable: the core runs and is debugged on a PC, in milliseconds, with test coverage of at least 99 % enforced by the build.

The Amiga half, src/platform/amiga, is 5,244 of the 29,610 lines of code in src/ (18 %), far above the design’s target of 5 %. The build holds it on a ratchet instead: the untested hardware code grows only by a recorded decision, and today sits at 5,244 lines against a ceiling of 5,250.

Everything is a script

What happens in a level is written in the level, not in the engine. A ticket machine, a barrier, a person who greets the player once: each is a few lines of TOML in the level file, compiled to a compact op list the core runs. The engine knows the verbs; the levels supply every sentence built from them.

The script has four kinds of entry. Actions, zones and events share one set of ids, 12 characters at most:

EntryWhat it isIn the 28 levels
ActionA named list of do lines, with an optional if and else393
ZoneA rectangle of cells that reacts: tap (fire button), enter, block (a wall while its condition fails), use (an item from the pocket), spot (someone or something that may appear), queue (people who move up)627
EventSomething that happens on a clock: after N seconds, every N seconds with jitter, or at a time of day206
NoteA line on the pocket’s to-do list, shown and ticked by conditions36

An example

A small example, from the level format’s own documentation. A choice in a conversation names an action, the action pays and gives, and a gate opens once the condition holds:

[[action]]
id = "buy_ticket"
if = ["afford 30"]
do = ["pay 30", "give ticket", "say p1.machine.bought"]
else = ["say p1.machine.broke"]

[[zone]]
id = "gate"
rect = [56, 17, 10, 1]
kind = "block"
if = ["has ticket"]

Vocabulary and limits

The words come from a fixed vocabulary of 40 verbs and 23 conditions. Verbs change state (give, take, set, clear, count, pay, stat mental -5), steer the world (zone gate on, timer ID stop, queue ID step, exit ID, warp, walk, sit) or call on the engine (say, fade, shake, mash, gameover, end). Conditions ask about the run: has ticket, flag, afford 30, chance N, class, seated.

What the whole run remembers is declared once, in assets/script.toml: up to 64 items, 160 flags and 17 counters (each 0 to 99, or up to 9999 with its own max). A level holds up to 48 timers, 128 actions, 2,048 ops and 216 zones.

The core keeps no names, so dialogue and script meet through hashes. A conversation’s choice can carry .ev = buy_ticket; the core keeps a 16-bit hash of that name and runs the matching action when the conversation ends. The level check refuses two actions whose hashes collide. Prose can show counters as %1 to %G, and %T shows the time of day, written into the text at draw time without a single division.

The boot measurement never loads a script, so its numbers stay comparable from build to build. Replayed play sessions do measure the script, and a road is measured again whenever its content changes.

The level editor

Every level is built in tools/level_editor.py, a Python and tkinter program, from graphics packs bought from LimeZu. The editor flattens a design into the formats the game loads and refuses anything the Amiga could not hold, before the game ever sees it.

The window

It has three panes. On the left, the catalogue of pieces (packs, themes, prefabs) and an outliner of everything in the level: NPCs, exits, zones, events, actions and nested groups. In the middle, the map, edited on five layers: Terrain, Beneath, Objects, NPCs and Script. On the right, a form for whatever is selected, and above it the level’s limits: tiles in the bank, packed map bytes, chip RAM of the scroll bitmaps, NPCs, placement file size. Those limits are read from the C sources and the asset manifest, so they follow the engine without an editor release. A row turns orange at 90 % and red above its limit.

Tools beyond paint and erase do level-design work in one undo: Room lays a floor with outer walls in one drag, Door cuts a doorway into a wall and redraws the wall’s ends, Fill, Area, find-and-replace. Probe shows a figure’s feet at the exact pixel and whether they fit; Measure says how many free cells a gap is wide. View overlays show walls and unreachable floor, script marks, changes since the last commit, and tile pressure: how full the tile cache would run around each cell.

Checking a level

The check (F6, and make levels-check in CI) holds a level to what the game needs: the map fits the core’s buffers, no stretch of the camera’s band holds more distinct tiles than the cache’s 240 slots, collision matches the picture, every room can be reached. The script gets its own rules: every name resolves, every dialogue choice names an action the level has, and no gate keeps its own key behind it. That last one is a fixpoint search: flood the map with every gate shut, collect what the reachable pieces give, open the gates that now open, and flood again until nothing changes. A plausibility pass catches things a play test once caught by eye, such as street furniture indoors or a busker’s stand with nobody playing.

Simulate the script runs the level’s script with the game’s own C code (script_sim, built from the core) and maps the numbers it prints back to names. A dialogue choice is simulated by naming its action, so a level’s logic can be walked through without the emulator.

The editor also edits the dialogue of the language packs, byte for byte, and builds the character sheets, vehicles, passing trains, pocket pictures and the dialogue box’s frame from recipes. Everything has a command-line form too (level_editor.py export | check | verify | simulate | render | …), which is what CI runs.

From editor to game

  1. The editor saves the level source, <id>.toml: plain TOML, one key order, no timestamps, so a level read and written again gives the same bytes and diffs cleanly.
  2. Export (F7) writes the map (a tile number per cell), the flags (512 bytes, wall bits and collision masks), the tile sheet (at most 512 tiles of 16 × 16) and a sidecar recording hashes of everything it was made from.
  3. make convert compiles the level’s NPCs, exits, zones, events and actions into the binary placement file, .rrp, that the core reads.
  4. The build packs the sheets, maps and placement files onto the hard-disk archive.

The tile sheet carries LimeZu’s purchased pixels and never enters git; the map and flags are numbers and do. Thanks to the sidecar, every machine can verify a level without the packs.

Art and memory

The tightest limit is chip RAM: 512 KB that the blitter, the copper, the sprites, the audio and the screen all share. The game also needs 2 MB of other memory, and it checks both at start and says so on a smaller machine instead of playing with something missing.

Tiles. Levels are drawn in 16 × 16 tiles, up to 512 per level, cut from LimeZu’s packs. A level’s full tile bank is kept outside chip RAM. Chip RAM holds a tile cache of 240 slots, and ACE’s map holds slot numbers instead of tile numbers, so every one of ACE’s draw paths stays as it is. The core decides which tile sits in which slot, with one promise: every cell in a band around the camera, wider than the screen, holds its tile. A sweep frees slots behind the camera, two columns a frame, once free slots run low. That is why the limit a level meets first is not its 512 tiles but the distinct tiles near the camera, which is what the editor’s tile pressure view shows.

Colours and sprites

The playfield has 32 colours, split by owner. The editor derives the tile colours from all levels at once, and the figures‘ colours from their recipes:

RegistersOwner
0Background
1–15Tiles
16–17Fixed text colours
18–25The player figure and the hardware carried
26–31NPCs, with palette swaps for variants

The HUD strip at the top gets its own palette through a copper split. On foot the blitter draws the figures; on the roads the player’s car and the traffic are hardware sprites, four pairs multiplexed by the core, with collision tested pixel run against pixel run.

The motorway seen from above, the player's car and the traffic drawn as hardware sprites
On the road: the cars are hardware sprites over a scrolling playfield.

Dialogue is loaded per level from the language pack into a workspace outside chip RAM, about 76 KB: the level’s file, the outbursts every level shares, and a pack for the fair’s exhibited project. The packs are validated in CI (validate_lang) against those byte and entry budgets in every language, with each counter at its widest.

Proving it works

Cross development has an open feedback loop: it ends in an emulator window no tool can read. The engine closes that loop in three layers.

On the host. The core, the script runtime and the tools run on a PC. 52 C test files exercise the core through the record backend, with 99 % line coverage enforced; the editor has its own Python test suite. This is the layer that runs in seconds.

In the emulator. FS-UAE boots the real 68000 binary under a virtual display, on the measuring machine: 512 KB chip RAM, 1 MB slow, 1 MB fast. The game reports over the serial line. It times each section of a frame (RR: PERF) against a stored baseline and fails a run that got slower, and it reads its own screen back to check what it drew. These figures describe the emulator, not an A500; the 512 + 512 KB configuration once agreed with a real A500 to within 1 %.

Input tapes

The measuring build records what was held on every tick, run-length coded, and writes it out at the end of a run. tools/tape.py turns that into a C file, and a build that carries it plays the session again by itself. The script’s randomness comes back too, because it follows from the input. A play tester’s twenty-minute session becomes a repeatable test; probes can also be written by hand (60:0,press,80:r).

The gates

WhenWhat runsTime
Every commitci-fast: host tests, level checks, language validation, architecture and size rulesabout 1.5 min
Every committools/takeout.py: each new fix is cut out on its own, and a test must failvaries
Every pushthe full make ci: emulator runs, perf-check, route, and drive-sweep over many traffic seedsabout 7 min
On demandperf-drive, for a road whose drive code or content changedby road

The take-out run is the unusual one: a test that was never seen failing proves nothing, so every new rule or branch is removed in turn and its tests must catch it. A commit-message hook also refuses a change to what the player sees unless the manual, the level documentation or the translators‘ guide changes with it, or the commit says why it stays.

The toolchain

One repository, two builds of very different character:

Host buildAmiga build
Coversthe logic core, the tools, the tests: about 80 % of the codethe platform layer, and linking
Compilernative gccbebbo’s amiga-gcc (m68k-amigaos-gcc)
Feedbackseconds, with gdb and gcovan emulator window
Debuggerfullnone

I use the IDE for the host build only. For the Amiga build, it is a button that runs a script and then an emulator. The cross toolchain lives in a container image (built once, 10 to 15 minutes): the compiler, CMake, VASM for the two assembler files, amitools, and FS-UAE built from a pinned release tag, with one local patch, rather than taken from a distribution. ACE is pinned as well, and warnings are errors. The test framework is greatest, a single header kept in the repository. The game logs over an emulated serial port, read through a pseudo-terminal.

The level editor is the deliberate exception: plain Python and tkinter, so that whoever builds a hall needs neither the container nor a cross compiler.

Planned first, built with an AI assistant

Before the first line of code there were five design documents, about 2,500 lines in German: the game’s vision, the technical design, the asset manifest, the development environment and the licence model. They were the repository’s first commit, on 6 September 2026, and they are binding. They set the target machine, the architecture rules, the memory budget, the test strategy, and eleven milestones from the toolchain to polish and release, each with a completion criterion that CI holds its status line to. When a decision changes, the document gets a dated note in the same commit that builds the change, so the plan never drifts from the game.

The code is written with Claude, Anthropic’s AI model, as an assistant: 1,231 of the 1,232 commits are co-authored with it. It assists; it does not decide. The design, the rules and every trade-off are mine. Nothing counts as done until I have seen it: level art, layouts and mechanics are signed off by eye, and the game is play-tested by me and the alpha testers.

The planning is also what makes the assistance work. The design documents say what to build, and the gates in Proving it works catch a mistake whoever made it: the architecture checks, the take-out runs, the emulator measurements, and the hook that refuses a change the manual does not describe.

How the plan changed

I planned the game in release stages: V1.0 a narrow but complete cut through all four phases, each later stage an addition. The order changed on the way: I pulled the car route ahead of V1.0, and pitching ahead of the event packs.

StagePlanned asWhat happened
V1.0The vertical slice: train only, two classes, the fair, the way homeAll four phases playable since 17 October 2026
V1.1Four classes instead of twoBuilt 27 October 2026, with damage that finally follows each class’s robustness
V1.2A second route, by carBuilt 3 October 2026, before V1.0, as a top-down motorway with traffic, roadworks and fuel; the parking search was left out
V1.3Swappable event packsPlanned; no pack format yet, but the fair already holds no content in code (see What comes next)
V2.0A second goal at the fair: pitching your own projectBuilt 28 October 2026, and part of every run instead of a choice

Some things I cut, and the reasons are part of the design:

  • The weapon. The original concept put two answers on the one button: a tap for diplomacy, a long press for brute force. I removed the brute-force option on 18 October 2026: it never had a picture, and a weapon the player can draw is one the game would also have to let them use, which I did not want to start. A rant in the dialogue box took its place.
  • Out-of-tune music when the nerves run low: the music replayer has no pitch control. The flashing warning bar stayed.
  • Flickering colours for the same warning: a play test found them too distracting.
  • Floppy disks as a way to play, which I dropped on 17 October 2026 in favour of the hard-disk archive.

What comes next

The next planned stage is swappable event packs: the fair can be replaced by a data pack built for a real event, so the game can be tailored to a particular show. Someone standing at that event plays through a hall they know and meets people who look familiar, a complete second playthrough without a single new mechanic.

The fair's hall with stands, visitors and the time of day in the strip
The fair today: the part an event pack would replace.
A pack changesA pack keeps
The hall’s layout and its standsThe three meters
The fair’s tilesThe one-button controls
Who stands around and what they sayThe money
What there is to buy, and what it costsThe four phases
Optionally, a loading picture

Two kinds are planned: official packs that belong to the game, and community packs that their authors distribute themselves.

This is why the engine is data-driven. Early on I made it a rule in the technical design that the fair holds no content in code: no hard-wired stand positions, no compiled NPC table, no assumed hall size, no loot table in C. Pulling content out of grown code afterwards is painful, and you never find all of it. The rule has held: the fair lives entirely in level files, texts and picture recipes, and everything the core learned for it is general and open to every level.

Deliberately not planned: several fairs, a campaign mode, multiplayer, or a character who improves across runs. A run is a run.

Open code, closed art

The licence line does not run between code and everything else. It runs between what is open and what stays closed, along directories:

PartLicence
Code, build files and toolsMPL-2.0
Language packsCC BY-SA 4.0, translators credited by name in the game
Graphics, music and soundAll rights reserved
Purchased graphics packsTheir makers‘ licences; never in the repository

My reasoning: the code is a top-down engine for the Amiga. It is useful, but replaceable, and someone could fork it and build a different game without taking anything from this one. The graphics, the music and the level design are what make RetroRoadtrip this game. Code and language-pack contributions come in under a non-exclusive contributor licence agreement. If the game is sold one day, I will most likely publish it myself, but that is not decided yet.

Credits

  • Graphics: the level tiles and figures are made from LimeZu’s packs Modern Interiors, Modern Office, Modern User Interface and Modern Exteriors, used under their licences. The derived and custom graphics are drawn with pxl by uilianks, a fork of claude-pxl by crsolver.
  • Music: Jochen „Virgill“ Feldkötter, written for the game: one module of eleven tracks, and each level plays one of them. They play through ptplayer by Frank Wille, from Aminet, public domain.
  • Play testing: phipslk and „Filou Orion“.
  • Engine base: ACE, the Amiga C Engine.
  • Tools: bebbo’s amiga-gcc, FS-UAE, VASM, amitools, greatest.

By the numbers

Development started on 6 September 2026; by 4 November there were 1,232 commits. Counted in the working tree on 4 November 2026. Line counts are whole files, comments included; the engine’s comments are long by design.

WhatCount
Logic core, src/core33,794 lines
Platform layer, src/platform (with its two assembler files)13,042 lines
C tests, tests/ (52 files)49,664 lines
Level editor, tools/rr_editor25,287 lines
Editor tests21,734 lines
Levels28, plus the editor’s demo and a measuring bench
Placed pieces, in the 28 levels6,178
Script zones / actions / events / notes, in the 28 levels627 / 393 / 206 / 36
NPCs / exits, in the 28 levels40 / 50
German dialogue files5,682 lines
LanguagesGerman, English

Gelesen: 111 · Heute: 24 · Zuletzt: 5. November 2026

Für dich vielleicht ebenfalls interessant …

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.