How Siren Jam proves every jam has a way out
You never write the solution. The solver does.

Why the solver came first
Siren Jam has 160 levels across eight worlds. In each one an ambulance, a fire engine or a police car is boxed in by a jammed car park. It can’t turn, so it gets out either straight ahead or by reversing straight back. You tap the other cars away, in the right order, until one of those two lanes is clear.
The solver sits at the centre of the level pipeline. A puzzle game makes a promise that every level has an answer, and a move limit makes a second promise that the limit is fair. I didn’t want either promise to depend on me playing 160 levels carefully. So the rule in the project is simple: you never write a level’s solution. The solver works it out, and the level file only describes the jam.
The board as a dependency graph
A tapped vehicle drives straight ahead to a one-way ring road round the lot, follows it and leaves by the first open exit. It only goes if nothing stands on its path through the lot. A vehicle that leaves never comes back.
That last rule does most of the work. Play is monotonic: the board only ever loses pieces. Taking a car away can never put a new obstacle in front of another car. So for each vehicle I can work out, once, which vehicles stand in its lot path at the start. The code calls these its blockers, and the relation never changes during a level. A vehicle can leave once all of its blockers have gone.
That turns the board into a directed graph. Each vehicle points at the vehicles in its way. Barricades, which the player taps to remove, sit on the same paths and count as a move each.
Two lanes, two answers
The unit has two escapes, and the solver treats each as its own puzzle. For one escape, everything standing in that lane has to go. Everything in their way has to go too, and so on down the chain. That set is the required closure:
// The lane's pieces, plus everything transitively in their way, must go.
var stack = new Stack<int>(deps.escapeVehicles[escape]);
while (stack.Count > 0)
{
int v = stack.Pop();
if (!required.Add(v)) continue;
foreach (int o in deps.blockers[v]) stack.Push(o);
}
Because nothing comes back, the shortest solution for that escape is that closure plus the barricades on its paths, removed blockers first. Then the solver checks what can rule the escape out. A required car with no route off the lot, more wrecks than the level grants tows, or a blocking cycle (two cars each in the other’s way) makes it impossible. A lane that runs into a planter or a closed side of the lot is shut from the start.
The solver keeps the cheaper escape as the level’s optimum and records both costs. Ties go to driving out forwards. The difference between the two costs is a design value in its own right, because a big gap means choosing the wrong lane hurts.
Traffic lights break the shortcut
Lights are the one feature that adds moves outside the closure. One tap swaps every light in the level, and a car can’t leave on red. Ordering the closure is no longer enough, because the number of swaps depends on the order. When lights matter and the closure has at most 22 vehicles, the solver runs a breadth-first search over pairs of (vehicles removed so far, light phase) to find the fewest swaps. Larger sets fall back to a greedy order that swaps only when nothing ready is on green.
Checking the shortcut against brute force
A clever shortcut needs a slow, obviously correct check beside it. The solver also has an exhaustive search: breadth-first over every legal action on the board, including every vehicle, barricade, light swap and the drawbridge, capped at 400,000 states.
The test suite builds random small boards, between three and five cells wide and four and seven tall, and compares the two answers. It needs at least 450 boards compared, more than five of them unsolvable, and more than 50 solved in reverse. Solvable or not, the closure analysis has to agree with brute force every time. That test gave me the confidence to trust the fast version on the big 9x12 lots, where brute force would never finish.
Generating a jam on purpose
Random boards make poor levels. Most of them are car parks with a few cars in the way. The generator builds a jam deliberately, and it is deterministic for a given template and seed:
- Park the unit inside the lot with room for both lanes.
- Cover every cell of both lanes with a vehicle whose own way out can be blocked, then fill the cells beside the unit so it is boxed in.
- Lock every lane vehicle, then grow chains of blockers behind them, each lane to its own target depth, so one way out is usually cheaper than the other.
- Pack the rest of the lot with no cycles and no vehicle facing a closed side, an obstacle or the unit.
Each candidate goes through the same solver. It is thrown away if it can’t be solved, if the whole board can’t be cleared, if a lane has a gap or a car that could simply drive off, or if the unit has an open side. The survivors are scored against the template: blocker-chain depth, locked moves, the shortest solution, the vehicle count, the gap between the lanes and how full the lot is. The generator makes up to 400 attempts per seed, and the campaign tool tries six seeds for each level and keeps the best.
The plan: difficulty as a curve
The whole campaign comes from one file, plan.json. Each world gives a start and an end template, and every number is interpolated across its 20 levels: lot size, vehicle count, chain depth, locked moves, the range for the shortest solution, how full the lot is, the move margin and the minimum gap between the lanes. Features switch on for chosen levels: tutorials, traffic lights, crossings, the drawbridge and wrecks.
The shortest solution climbs steadily. Downtown’s first level needs 8 moves with 17 vehicles, and its last needs 14. The last world, Frontline Valley, runs from 31 to between 36 and 38 moves on a 9x12 lot with more than 40 vehicles. Each world past Downtown opens a little below where the one before ended, so a new world starts with a breather.
Move limits and stars
The move limit is the cheaper lane’s solution plus a margin:
level.moves = analysis.optimalMoves
+ Math.Max(2, (int)Math.Ceiling(spec.moveSlack * analysis.optimalMoves));
The margin is 40% at the start of Downtown and shrinks world by world to 20% in Frontline Valley. Early on, a few wasted taps don’t matter. Later, clearing the wrong lane runs out of moves. That is the puzzle I wanted: read the board, pick the lane, then clear it.
The solver reports a few other numbers. A difficulty score from 1 to 10 weighs the shortest solution, locked moves, chain depth, lights, crossings and closed exits. A time estimate adds up a cadence per move, thinking time per layer of blockers, board reading and expected waits at crossings, and the star times come from that estimate. The in-game hint uses the same closure from the current state, and points at the legal car that frees the most other required pieces.
What I’d keep
Monotonic play made the solver simple. It is worth designing a puzzle’s rules so that a clean analysis exists. Every rule I added later, such as wrecks, lights and the drawbridge, had to fit that model or come with its own exact check.
The other thing I’d keep is the brute-force test. It is slow, plain and hard to get wrong, and it is the reason I believe the fast answer. Every level in the game passes ./tools/er test before it ships, and that command is the closest thing the project has to a quality bar for content.