Light Mode

From Two Buttons to a Pinball Emulator

I wanted a pinball controller with two buttons. By the next summer I had written an emulator for a current commercial pinball platform.

Pokémon running on my cabinet in Visual Pinball X, on the current version of Kiki.

I only wanted two buttons

In fall 2025 I was between jobs and wanted to play pinball with real controls under my hands. Playing in VR had pulled me back into the hobby, and a keyboard wasn't doing it. I figured I could at least build a controller with two buttons. From there it spiraled.

By the following summer, the original Pokémon game was playing on a cabinet in my living room. I had built an emulator for its Spike 3 hardware.

I'm a Salesforce architect by trade. I had never built an emulator, and I started the electronics side by asking why an LED needs a resistor.

AI helped me learn enough to keep going as the project got more complicated. It also gave me plenty of answers I had to check for myself.

Learning enough to build it

The controller used an ESP32 board, a shift register to read arcade buttons, addressable LEDs, an accelerometer for nudging, and haptic feedback. I added solenoids for the thunk of real flippers and a 3D-printed case. It had three Bluetooth modes, each with its own address, because devices kept confusing one mode's pairing with another's.

I had nobody nearby to teach me electronics. I kept asking questions: what the flyback diode protected, where a fuse belonged, how a buck converter worked.

The first habit was checking before trusting. One evening I stopped mid-build because I had started to doubt the circuit. Checking it turned up a mistake of my own: I was powering the board from the wrong pad.

The second was being straight about the help. When the controller worked, I wasn't sure it counted. I had used off-the-shelf parts, and as I wrote then, “I used AI quite a bit to help but I still feel guilty.” Where I landed: the design, the integration and the debugging were mine, and the tools were a reference. When an electronics magazine asked about the project, I decided to tell the editor exactly how I had worked.

I had another reason to keep at it. People at my makerspace in Orlando do this kind of work for the theme parks, and getting good enough to work on something like that has been a dream of mine.

From a TV on the couch to the original game

Next came Visual Pinball, the free simulator. I played it on a TV from the couch, then on a three-screen rig I put together by hand. By February I was modding a Pokémon-themed table and had written a save system so progress carried over between sessions, something I hadn't seen a table do.

Over the same winter I had started poking at the software real machines run: the SD card images, and some early experiments with QEMU that mostly taught me what I didn't know.

When Stern's Pokémon machine came out, I wanted to run the original game on my own hardware, with its rules, video and sound.

At first I couldn't read the game's files. Working out the storage took up most of my time for a while. By the end of March the hardware I needed had arrived and I could read the filesystem.

A cabinet was on order that spring. It didn't arrive until June, so everything in the next three sections happened on a desk.

Two ways to get there

In April I ran two routes side by side.

One was to build a playable version around the game's assets. Pokémon uses the Godot engine for part of what it puts on screen. I recovered that Godot project and got the studio's own preview tool running, which meant writing stand-ins for native pieces the tool expected and didn't have. For a while I thought this might spare me from having to emulate the game at all.

The plan was Godot for the screen, Visual Pinball for the table, and a middleman program I wrote so the two could talk. My own attempt at laying out the table is a file named “Gave Up.vpx”. The name is real.

One Saturday in mid-April I worked straight through and ended the day with a very rough playable prototype. The same weekend, the first emulator source went into Git.

Late April: the hand-built Godot prototype connected to a Visual Pinball table, before the original game was working in the emulator.

The other route was emulation: run the original software under QEMU and model the hardware it expected. That took much longer to produce anything I could interact with. The prototype gave me something to play while I worked on the emulator.

A week later I was back on the emulator at night. I kept both routes going for a few more weeks, but the work needed for the prototype kept growing. Rebuilding by hand meant recreating about 2,100 short sound effects, some 350 more stereo sounds and music tracks, and a full modern rule set. With the emulator, I could concentrate on the machine behavior the original program expected.

Working blind

The approach was to run the game's ARM program under QEMU on an ordinary PC, with a shim library loaded alongside it. The shim intercepts calls to devices and supplies responses in place of the hardware.

A pinball machine's computer talks to boards under the playfield that fire coils and read switches. I had to work out those exchanges well enough for the shim to take the boards' place.

By late April the game reached its logo and a “Startup in progress” screen. Then it sat there, repeating the same sounds.

Late April: the original game reaches its startup screen under QEMU and stalls there.

With that little to go on, my reference was a real machine. I compared the size and position of the startup text, whether the dots animated and how fast, how long the screen stayed up, and whether it appeared at once or after a pause. Each difference was a clue about how far the game had really got.

Pokémon's Godot assets made it tempting to blame Godot for everything. I had to keep checking whether the failing part of startup involved Godot at all, rather than treating its presence as an explanation.

My own debugging tools sometimes sent me in the wrong direction:

Some useful leads were sitting in front of me. The firmware for the boards on the other end of the bus ships with the game, so I pointed the analysis at it and we could read both sides of the conversation. Audio files the AI had filed as probably encrypted were plain uncompressed audio. When the plan was to sweep through switch numbers and watch what happened, I said to use the published manual.

At the start of May I was close to going back to the prototype. Switching between the two helped when I got stuck. A few days later I felt close again. I was determined to get a playable game out of one of them.

An error screen

In early May the splash screen finally gave way to something else: a Tech Alerts page, the game's own list of the hardware it believed was broken.

Game diagnostics screen listing node-board initialization errors.
Early May: the game's own diagnostics, listing what it thought was wrong.

I was delighted to see that error screen. For three weeks I had been trying to infer the game's state from its startup behavior. Now its own diagnostics gave me specific failures to investigate.

The service menu came up within a day or two, with the boards reporting in. The first attract-mode video followed, at about five frames a second.

Five frames a second was too slow to play. Improving that meant changing the architecture. The graphics work moved out of the emulated program to a native process on the host, with media decoding alongside it.

A few days later I could start a game and trigger switches on my test board. It still had bugs, but I was finally interacting with the game.

By mid-May the original Pokémon game was working under my emulator. That was about a month after the first night back on it.

Knowing when to believe it

I worked with Claude and Codex throughout, and with Binary Ninja for disassembly. I said at the time that “I absolutely couldn't have done it in this amount of time without AI”, and that is still true.

I still needed enough understanding of the binary and the tools to question the results. Even a disagreement about an address could come down to the tools displaying the same file at different base addresses.

A detailed explanation can sound convincing while resting on something nobody checked. Some of the confident answers were wrong:

I was wrong sometimes, too:

One change reduced the apparent problems in a run but left buttons stuck down. I went back to the last input behavior I trusted and restored it from Git.

I learned to write down what I had observed, what I thought it meant, and what I still needed to check. That made it easier to notice when an explanation had outrun the evidence.

Onto the table, then past one game

The emulator ran the game. It still needed a table to play on.

I wrote a new bridge so Visual Pinball could stand in for the physical playfield. The game fires a coil and the table's flipper moves. The ball closes a switch on the table and the game hears it.

A refactor in late May left switches reaching the table while game state and lights did not. I got the coils working next, then moved on to the lights. Around then, the emulator got its name, Kiki.

By the end of May my status list read: coils, drivers, switches, the moving toy, the ball trough, multiball, kickers and flashers working, at 30 frames a second or better in game. That met the bar I had set when it ran at five.

The cabinet arrived in the first week of June. I wired it, moved the emulator onto it, and played the real game standing up, with buttons.

Early June: gameplay on the cabinet.
Inside the cabinet, showing wiring, relay boards and physical mechanisms.
June: wiring and physical mechanisms inside the cabinet.

The goal had already widened. In early May I decided the emulator should run every Spike 3 title, and designed for it: what differs between games lives in a swappable overlay and a profile, not in edits to the shim. By early June it held profiles for Star Wars and The Walking Dead alongside Pokémon, and Transformers followed. By late June, Transformers, Walking Dead and Star Wars were booting and appeared playable.

Then came the older platform. Spike 2 is a different generation: 32-bit, older system libraries, and a bus that runs over a different link. My commit message for the first attempt calls it “maybe a long shot”.

A month later Godzilla booted, and the next day it was showing its attract screens. By mid-July the tree held eight Spike 2 titles next to the four Spike 3 ones. Some were at parity with each other and some were still being brought up. I wouldn't call that platform finished, but it ran.

What it adds up to

I still have the two buttons. They are on a cabinet now, and the game behind them is the real one.

I was surprised I'd got it working. I'd been tinkering with things since I was a pre-teen, and this project drew on more of that experience than I expected.

On who did what: I was the only developer of the emulator from its first line through mid-July, with one exception. In early June another contributor added native Linux and Mac launch support, an audio fix and build tooling. The emulation itself, every game profile and all of the Spike 2 work to that point are mine. Kiki has continued since then, with other people involved.

I did not start from nothing. The general approach had been described by people who had done this kind of work before. I began from an existing open-source decoder for the game's scene format, and a private research community was indispensable for knowing which direction was right. I never had another emulator's code. The table layout in Visual Pinball was done with another person's help.

I wonder how much of this a newer model could now do from a shorter prompt. I don't know. What I do know is how much time I spent learning the system, checking explanations and deciding what to try next. As the tools improve, I want to keep getting better at that part of the work.

I plan to follow this up with a separate article on the technical details: the device shim, the node-bus protocol, and moving graphics and media work out of QEMU. The code and journal from the early April–May work are public in kiki-early.

This article was written with AI assistance. Getting the story and the details right meant going back through thousands of my own conversations with Claude and Codex, which was daunting, and I used both to help sort through them and draft this piece. This is independent research, not an affiliated or endorsed project.