Lab · Aug 2026

Sprout Siege

An idle farm game for iOS, built around a simulation core that never touches the screen.

The farm: low poly plots, windmills and a barn, with collect, new plot and defend across the bottom.
The farm. It runs in real time, and nothing is lost while you are away.
115
Swift files across a package and a thin app target
268
tests in 29 suites, running headless
v7
save format, every version with a migration path

One rule

The package holding the game cannot import a user interface framework. No SwiftUI, no SpriteKit, no UIKit. Economy, waves, upgrades, prestige and saves all live in there and run headless under swift test. The app target stays thin: menus and HUD in SwiftUI, the combat board in SpriteKit, and no game rules in either. If a change needs the core to know about the screen, the change is wrong.

The defense board: a winding lane across a field, four defenders placed, and the defender tray with prices.
SpriteKit draws the board. The package decides what happens on it.

Deterministic by construction

The simulation is tick based rather than frame based. It advances by an interval, and every random number comes from an injected seeded generator, so the same seed and the same inputs produce the same result every time. Offline progress is not a second code path. Coming back after two days is the same call with a larger number. Balance lives in JSON rather than Swift, so tuning the game never requires touching code.

What that caught

Partway in, the structure surfaced a design problem I had not seen. Offline income was being generated by tower defense waves fighting themselves in an empty room. The player came back to a battle they had made no decisions in, which is neither believable nor good idle design.

The fix was structural. The farm became the idle spine: it runs in real time, and nothing wilts or expires or is ever lost while you sleep. Tower defense moved to being the first side game, short, active, and something you have to be present for, where you can lose what you were carrying. Two layers instead of one, with the passive half where passive actually makes sense.

The stake screen: four risk levels from Steady to Harvest Gamble, each with a payout multiplier and what it costs to lose.
You pick what you are risking before the run starts.

Swift 6, iOS 17 and up. Building clean, running in the simulator.

Back to Lab