Ballista Siege
I tend to quietly file my weekend projects away instead of writing them up. This post is me finally writing one up.
It started on 4 October. Look at this tweet from jac0xb.sol:
"Ballista needs an audit. Help it get one by spreading the word. If you're a dev, reply with how you'd use Ballista, any feedback on the protocol itself is also welcome. It's on devnet so come contribute! Learn more at ballista.sh."
I used to work at Star Atlas, and seeing ballista.sh in that tweet made me think back to my days there. The game designers wanted a "missions" game loop: you send your ships (or whatever) on a mission, which basically means you stake them in a program. Then the mission "happens", and you get your ships back with a report of what happened on the mission. The idea was to give the game designers a way to design each mission, perhaps with a scripting language or something like that, so they would not have to write custom Solana Rust for each mission.
The idea went nowhere.
Still, Ballista seemed almost custom made for that missions idea. So I sat down with Claude and designed and built a simple Ballista-powered game, just to see if it could actually be done, and to get a taste of using Ballista. It is called Ballista Siege, and it is a small tower defense game.
Okay, let me back up. What is Ballista? Basically, it is a transaction engine for Solana where you write a multi-step transaction once and upload it on chain as a template. The Ballista program checks it. If it passes, it is locked and can never change. After that, anyone can run it with new inputs. A template remembers nothing between runs, and registry entries are where it keeps what it needs to remember.
There is a catch, though. A template holds at most 128 instructions and has no jumps - meaning it runs straight from top to bottom, step by step, and can't skip around or jump back (apart from a few fixed loops). How do you fit a game into that? The first version of the game used 5 Ballista templates. As the game grew, the rules were split further, into the 25 Ballista templates it uses now. Your game lives in two registry entries, and one template, called state, looks after them and calls the other 24 in order. All 25 Ballista templates are written in TypeScript with Ballista's SDK. There is no custom on-chain program and no Rust anywhere in the repo - which is pretty much what those Star Atlas designers wanted all those years ago.
In the game, ten waves of enemies walk down four lanes toward your wall, including boss waves on wave 5 and wave 10. You win if your wall is still standing after wave 10. Between waves, you spend six actions and some gold building on a 4 by 4 grid. Then you watch the wave play out in ten ticks with no input from you.
Remember how I said I wanted to build a "simple" game just to see if it could be done? Well, it got a little out of hand. It now has a tutorial, and a layout that works on phones. Bots tested the balance over 400 fresh seeds: a planning bot won 83.5% of runs, a greedy bot won about 20%, and a bot playing randomly won none. It also has its own music and 22 sound effects, all synthesized in code in the browser with no audio files. I had Gemini listen to recorded battles and score the effects, the music and the mix out of 10. I wanted 9s. It mostly gave 8s. The target was never met because I got impatient and it's a silly throwaway game anyway :)
It really makes you think about how good AI tools have become. What kind of expertise and how long would I have taken on this project just 6 months ago? Solana programs. Game balance. Sound. A phone layout.
Anyway, back to how it runs. One wave is 12 transactions on chain. Confirming takes about 25 seconds. A normal wallet would ask you to approve 12 times per wave, so the page creates a devnet-only "burner" key in your browser's storage to sign every transaction. Meanwhile, the browser runs the same rules locally, so the battle animates at once and never waits for the chain. If the chain ever disagrees, the game stops and shows the mismatch. In testing, 300 complete runs played by bots on LiteSVM (a local Solana runtime) matched the TypeScript simulator field for field across 2,016 turns.
To play on devnet, follow these steps:
- Open the game. It starts an offline run straight away.
- Tap the "Offline" chip at the top of the screen. It opens the "Fund · devnet" panel, which shows the burner address the page made for you.
- Add devnet SOL. Press "Request airdrop · 0.1 SOL". If the faucet is busy, press "Open Solana faucet ↗", which copies your burner address and opens faucet.solana.com. If you have a wallet such as Phantom, Backpack or Solflare set to devnet, you can use "Send from wallet · 0.02 SOL" instead.
- Press "Start devnet run".
- Build, then press "Fight wave 1" (the number follows the wave). Each wave takes about 25 seconds to confirm on chain.
You can play the game here and read the code on GitHub.