19 August 2026

AmiCrush / Build Log, Writing Candy Crush for the Amiga 500 in Bare-Metal C

Writing a full Candy Crush for a 7 MHz Amiga 500: a match-three engine tested by the thousand on a PC, the blitter tricks that get 24-pixel candies onto the right word boundary, a twenty-colour budget for six candies, difficulty tuned by simulation instead of by feel — and five rounds of fixing the wrong audio bug before I started measuring and found the real one. Boots from floppy, runs on real hardware.

Candy Crush is a match-three game with soft 3D candies, cascading combos and a lot of sparkle. The Amiga 500 has a 7 MHz 68000, half a megabyte of chip RAM, 32 colours on screen, and a blitter that only understands whole 16-bit words. I wanted to know whether the second machine could carry the first game — not a demo of the idea, but a finished thing you boot from a floppy, with levels, specials, stars, a leaderboard and music.

It boots. It plays. This is how it went, including the five rounds I spent fixing the wrong audio problem.

1. The engine first, on the PC

The first decision was the one that saved the most time later: none of the match-three logic knows the Amiga exists. Board, matches, gravity, refills, specials, combos and scoring are plain C89 with no hardware in them at all, which means they compile on Windows and I can test them by the thousand. That suite currently runs 6430 assertions in under a second, and every single bug I found in the rules was found there rather than on a floppy.

Two things that turned out to matter more than they look:

  • The random number generator is my own, seeded per level. So level 5 deals the same board every time you play it, which makes a level learnable and a retry fair, and it lets me reproduce any bug from a seed.
  • The engine calls an animation hook at two moments — just before candies are cleared, and just after gravity has run. On the PC the hook is null and nothing happens. On the Amiga it draws. That one function pointer is the entire boundary between the rules and the machine.

2. Taking the machine over

The game targets Kickstart 1.3 and uses no operating system at all while it runs. It saves the interrupt and DMA registers, takes the display, installs its own copperlist, and puts everything back on the way out. There is no NDK in my compiler setup, so every register is poked by hand out of a header of raw addresses.

The one thing it does still ask the OS for is the high score file, and only twice per run: once before the takeover to load, once after the restore to save. AmigaDOS needs its interrupts to talk to the floppy, and the game has taken them, so there is no third moment where that would be safe. That is written in capitals above the two functions, because it is exactly the kind of thing you cheerfully break six months later.

3. The blitter is word-addressed, and it cost me a day

My candies are 24 pixels wide and sit on a 24-pixel grid. Columns therefore land on alternating even and odd byte offsets, and the Amiga blitter silently rounds a destination down to the nearest word. Result: every second column of candies drew eight pixels to the left, so they appeared in overlapping pairs.

The lesson I would tape to the monitor: a naive byte-level simulation on the PC does not catch this. I only reproduced the bug after I modelled word alignment properly in the test — and once the model showed the bug, it also proved the fix was pixel-clean before I burned a disk.

The fix is the barrel shifter. The blitter can shift source data by 0 to 15 bits on the way through, so the destination stays word-aligned and the candy lands wherever you like. Every tile now goes down as a single cookie-cut blit that covers all five bitplanes at once, because I store the tiles interleaved: for each screen row, the five plane rows sit next to each other in memory rather than in five separate blocks. One blit per candy instead of five.

4. Everything on screen is the blitter's job, not the CPU's

The score panel used to be drawn pixel by pixel by the 68000. I assumed that was fine because it only happens when something changes. It was not fine: one 90 by 26 pixel box is about 11,700 read-modify-write cycles into chip RAM, which is roughly 20 milliseconds — a whole frame, for one box.

So the panel became blitter work too, which needed two tricks worth writing down:

  • Pixel-exact rectangles. The blitter's first and last word masks can clip a fill to any pixel edge. Feed channel A a row of ones, put the colour bit for this plane in a constant, and the logic function does the rest.
  • Text. The font became blitter records too: five small blits per letter instead of about 140 CPU pixel writes, each of which was redoing a 32-bit multiply. The initials entry screen went from unusable to instant.

The measured result: painting the panel dropped from about 180 milliseconds to 12 per level, and from 75 to 2 per move.

5. The candy art, and the twenty-colour budget

Thirty-two colours sounds generous until you count. One is the background, one black, one white. Six are reserved for the interface. The specials — striped, wrapped and the colour bomb — are generated automatically from the six base candies, and they need three colours of their own. That leaves twenty for the candies themselves, all six of them together.

So I built a tool chain instead of drawing pixels by hand. A Python generator turns six 24 by 24 PNGs into the Amiga's planar tile format, generating every special and two smaller copies of each candy for the shrink-and-pop animation. And because tweaking art through a command line is miserable, there is a single-file browser tool — no server, no build step — where I can draw the candies, import an image and have it fitted to the palette, merge colours down when I overshoot, and watch a live counter tell me how far over twenty I am.

Themes are just folders. Drop six PNGs in one and it appears in the game, switchable with a key while you play.

6. The interface should belong to the theme

The panel started out flat pink, which looked like a spreadsheet next to the candies. It now takes its colours from the theme's own candy art: the hue of one candy, normalised through HSV so that a dark purple and a bright yellow give equally readable panels, and a different one per level. Level one wears the red candy, level two the orange, and so on.

On the Amiga this is nearly free. Those colours live in four palette registers that the copper rewrites anyway, so recolouring the entire interface costs four values — no second bitmap, no extra memory. The candy pattern embossed into the panel background is the theme's own artwork too, drawn once per level as a lattice of shadow-and-highlight rims.

7. Sound: five rounds of fixing the wrong thing

This is the part I would most like to have back, and the part I learned the most from.

The music is a ProTracker module played through Paula, which needs a tick fifty times a second. Get the tick rate wrong and the song drifts. It drifted. So:

  • Attempt one compared successive beam positions to spot a new frame. Reading the two beam registers as one 32-bit value is two reads on a 68000, so a line change between them yields nonsense, and the nonsense looked like extra frames. The music ran at double speed.
  • Attempt two waited for the beam to pass line 300. That is 12 of the 312 lines — a 0.76 millisecond window inside a 20 millisecond frame. Any code that did not happen to look inside that window lost the frame entirely, and the CPU panel fills ran 20 milliseconds without looking at all. Most frames went missing, the module crawled, and its sustained pad sample turned into a drone.
  • Attempt three used the vertical blank flag in the interrupt request register, which the hardware latches and holds until you clear it. That cannot be missed. But one bit cannot count: if two frames pass between checks, you only ever learn that at least one did.
  • Attempt four hung the check off the routine that waits for the blitter, so the renderer could not draw anything without passing through it. Now the count was exactly right — I put a counter on screen and measured 50 ticks a second, with a worst backlog of one.
  • And it still hiccupped.

Because the count was never the problem. The tick was on time; the writes to Paula were not. They happened from the middle of a burst of blits, which is exactly when the blitter owns the bus and the CPU is most starved. A note could reach the hardware a frame or two after its tick — while the looping pad, never retriggered, played on undisturbed. Which is precisely what I had been told I was hearing, in those words, three rounds earlier.

The fix is what every commercial Amiga game does and what I should have done first: run the music from the vertical blank interrupt. An interrupt preempts the drawing instead of queueing behind it, so Paula gets its registers at the top of the frame no matter what the blitter is doing.

8. Two more audio bugs, found by looking at a recording

With the timing fixed there was still a crackle at startup. I recorded the emulator, pulled the waveform apart in Python, and there it was: at the moment of takeover, two bursts of garbage — a clipped block, then noise — before the module has triggered a single note.

Cause: enabling audio DMA for all four channels in the same write as everything else, while those channels still held whatever pointers and volumes AmigaOS had last left in them. Paula duly played random memory. All four are now pointed at two bytes of silence at volume zero before their DMA goes on.

The same recording showed a second one: a constant DC offset of about -0.024 of full scale still sitting on the output twenty seconds after the game had stopped making sound. Turning DMA off but leaving a channel's volume up makes Paula hold its last sample value forever. Volumes now go to zero first.

9. Measure it, or you will fix the wrong thing

The animation felt like it stopped the machine during a big cascade, so I made the blits cheaper. Three times. Nobody noticed a difference, which is the clearest possible signal that you are optimising the wrong thing.

So I wrote a tool that plays hundreds of real games, finds the real clears, and counts what the blitter is actually asked to do against the roughly 25,000 bus slots it can have per frame. The answer: an average clear costs 4.4 frames of blitter work spread over eight animation steps. The blitter was idle most of the time. What cost was that every animation step waits for a frame — so the fix was fewer steps, not cheaper pixels.

The same lesson applied to game balance. I had set the star thresholds by feel, and they were badly wrong: a beginner passed level three only 48 percent of the time and level four 14 percent. Worse, the curve was inverted — later levels gave fewer moves while demanding more score, so they punished you twice. Now a simulator plays 400 games per level with a beginner, a normal and an expert model, and the thresholds come out of the measurement. A normal player passes every level between 77 and 100 percent of the time, gets two stars about 70 percent, and three stars around one time in six.

10. The emulator lied to me for an hour

Worth its own section as a warning. I finally started driving WinUAE from the command line so I could boot and screenshot the game myself instead of reasoning about it. First run: the title screen rendered perfectly and the playfield was shredded — candies as coloured vertical stripes, panel full of garbage. I was certain I had broken something badly.

Then I built an old, known-good version and it was shredded too. The bug was my configuration: I had given the machine 1 MB of chip RAM on an OCS chipset, and an OCS Agnus can only address 512 KB. CPU writes worked fine — the title screen is a plain memory copy — while every blit landed somewhere else entirely. 512 KB chip plus 512 KB slow, like a real A500, and it was perfect.

11. The game, and the things I did not know were already in it

Eight levels, all four specials, and the full Candy Crush combination table. That last part was already there and completely invisible: swap two colour bombs and the entire board clears. I discovered this by accident while testing and had no idea what I had done. Depth nobody can find is not depth, so there is now a page on the title screen that lists every special and every combination.

The end-of-level payoff is in too. Reach the target and the level ends there and then, and every move you did not need fires as a striped candy. Stars stop measuring how long you ground away and start measuring how efficiently you got there.

And one genuine softlock, found by reading rather than by crashing: the check for whether a board still has a legal move was only ever called when a level started. A refill can leave a dead board, and moves only tick down on a successful swap — so the level could never end and you were stuck for good. It reshuffles now, keeping any special you had earned.

12. Music, and being honest about it

The soundtrack is Godzilla by Dr. Awesome of Crusaders. Getting it to play properly meant teaching my player things it did not know: all four channels instead of three (channel four is shared with the sound effects now, rather than reserved, so a module keeps its drums), position jump and pattern break — without those a module plays its patterns straight through and silently rearranges the composer's song — and the extended commands. That last one mattered most: this song uses note cut 1541 times. At that density it is not decoration, it is the rhythm, and ignoring it does not sound like a missing effect, it sounds like a badly made module.

Before that I tried generating a module from scratch — synthesised strings, a piano built from inharmonic partials and a hammer thump, the whole thing. It was technically interesting and it sounded bad, so it is gone.

One honest note: the sample names in Godzilla spell out a message from its author — "if you rip this song, please use the protracker playroutine!! thanks - doc." That is a request about playback quality, not permission. I have a tool now that reads every scrap of text out of a module for exactly this reason, because free to download is not a licence, and I will sort the rights out before this goes anywhere.

13. Things I'd tell my past self

  • Put the game logic where you can test it. Six thousand assertions on a PC found every rules bug. None of them needed a floppy.
  • Never derive frame timing from the beam position. Two attempts, two different failures. Use the latched interrupt flag, or better, the interrupt itself.
  • Run the music from an interrupt, not from a poll. Being called the right number of times is not the same as being called at the right moment.
  • Silence Paula before you enable its DMA, and zero the volumes before you disable it. Both ends make noise otherwise.
  • Never put a per-pixel routine in a loop on a 68000. Mine hid a 32-bit multiply per pixel, and I did not find it by reading, I found it by measuring.
  • Measure before you optimise. I made blits cheaper three times while the blitter sat idle two thirds of the time.
  • Set difficulty from a simulation, not from a hunch. My hand-tuned thresholds were not slightly off, they were inverted.
  • When the emulator shows you something impossible, suspect the emulator. Build a version you know worked and compare.
  • Depth nobody can find is not depth. Ten combinations sat in that engine, undiscovered, for months.

Status

Boots from floppy on Kickstart 1.3 and runs on a real A500. Eight levels, four specials and the full combination table, per-level colour schemes, a leaderboard that survives a reboot, sound effects and module music, and a candy designer for making your own themes. Still on the list: the Swedish fish, blockers like icing and chocolate so that not every level plays the same, a better font, and sorting out the music rights. To be continued.