Devlog

Procedural terrain that is always solvable

Random terrain can generate a level you cannot finish. The fix is not more validation — it is to carve the safe route first and place every hazard around it, so solvability is structural.

Every stage in Flockfjord is generated: the ridge, the pass through it, the lakes, the rock fields, the woods. The flock has to get from one pen to another across it. And the single hardest requirement in the whole project is this: it must always be possible.

Not ”usually possible”. A player who loses a run to a stage that could not be finished has been robbed, and no amount of good design elsewhere repairs that.

The obvious approach, and why it fails

The instinct is: generate terrain, then check that a path exists, and regenerate if not. That works, but it has two nasty properties. It can loop unpredictably on a slow phone, and worse ”a path exists” is not the same as ”a path exists that a flock can be driven along”. A gap one sheep wide passes a flood-fill check and is unplayable in practice.

So Flockfjord inverts it:

Carve the safe corridor first. Then place every hazard around it.

Solvability stops being a property you test for and becomes a property of the construction. Nothing can block the route because nothing is ever allowed to be generated there.

Everything is one Gaussian

The entire terrain generator is built from a single function:

g(d) = exp(-(d / sigma)^2)

One at distance zero, smoothly falling, effectively zero past 2·sigma. Three properties make it perfect here: it is smooth (no seams), it is local (shapes do not bleed into each other), and one parameter controls its width.

The same formula, six jobs:

UseFormResult
Ridgeheight · g(along)a wall across the route
Pass1 − 0.88 · g(lateral)inverted: a notch in that wall
Lakedepth · g(dist)a round bowl
Levellingh·(1−g) + PEN·ga seamless flat pad for a pen
Corridor mask1 − g(lat / half)”water does not bite in this lane”
Basin rimh · g(dist − R)a ring of hills at radius R

Three patterns fall out of that: inverted Gaussians make holes and masks, a Gaussian of a difference makes rings, and a Gaussian used as a blend factor melts two heights together with no visible join. Multiply them and you get the rest ridge × pass is a wall with a doorway.

Route coordinates, not world coordinates

None of this is placed in x/z. Every feature is positioned in two numbers derived with a dot product: distance along the route, and distance lateral to it.

val along = (wx - mx) * ux + (wz - mz) * uz
val lat   = (wx - sx) * nx + (wz - sz) * nz

Because of that, the route can point in any direction and every ridge, lake, ford and grove follows automatically. It also makes the safety rule trivial to express: hazards need |lat| above some threshold.

The ford: a case study in doing it wrong

The coast stages have a river crossing the route with one dry ford. It was by far the hardest thing in the game to get right, and playtesters could not get the flock across at all. There were four separate causes, and the last one is the interesting one.

1. Water avoidance steered uphill. Sheep looked for the highest ground nearby to escape water and at a river crossing the highest ground is the hill behind them. The flock kept retreating. Fixed by choosing the best dry direction that still continues forward, with height used only to break ties.

2. Look-ahead scaled with speed. A panicked sheep looked 3.4 units ahead and swerved before it could ever line up with the gap. Now the look-ahead is a fixed 1.2.

3. A hill and a crossing stacked. The stage had both a descent and a narrow crossing at the same moment. The ford stage is now almost flat by design — the water is the challenge there, and difficulty should arrive one thing at a time.

4. The ford’s width was an accident. This is the one worth remembering. The crossing was made by subtracting less depth in a disc around the ford. Which means the width of the dry strip depended on whatever the underlying terrain happened to be doing there: sometimes four units wide, sometimes 1.7 one sheep wide and sometimes closed entirely.

The fix was to stop subtracting and start building:

val t = dist(x, z, fordX, fordZ) / fordR
val b = when {
    t >= 1f  -> 0f
    t <= 0.5f -> 1f                        // solid causeway core
    else      -> { val u = (1f - t) * 2f   // smooth banks
                   u * u * (3f - 2f * u) }
}
if (b > 0f) h = h * (1f - b) + fordHeight * b

The core is set to a known walkable height rather than nudged toward one. Now the causeway is a guaranteed width whatever the ground does, and the banks use a smoothstep so there is no visible step where it meets the terrain.

The general lesson: if a dimension matters, set it do not compute it as a side effect of something else. A guarantee that emerges from arithmetic is not a guarantee.

The floor that stops the sea from leaking in

One more subtle failure. The coast stages sit at a low base elevation, and the rolling sine hills that make the ground interesting could dip below the water line by themselves creating shoreline nobody had planned and no mask protected. That water could pinch the route closed.

A hard clamp would have worked and looked terrible: large flat plateaus stamped across the coast. Instead the terrain gets a soft floor:

softplus(d) = ln(1 + e^(2d)) / 2     // a smooth max(0, d)

It approaches the true height well above the floor and eases asymptotically down to it, so gentle relief survives near the shore while nothing sinks unintentionally. All water in the game is now deliberate and deliberate water is water the corridor mask knows about.

Variety without breaking the guarantee

The first version of all this generated one spatial sentence: start pen → ridge with a pass → goal pen. After twenty stages players told me correctly that every stage felt the same. Different colours, same level.

The fix was to give each stage a route archetype, each with a different dominant verb:

  • Pass — push through the gap (the original)
  • Turn — the gap sits far to one side; swing the flock
  • Ridgeline — travel along a spine, ground falls away
  • Double pass — push, regroup, push again
  • Maze — no ridge; thread woods or rock fields
  • Basin — bring them down into a hollow ringed by hills

Every one is still carved corridor-first, so the guarantee survives intact the corridor is simply a different shape.

Deal archetypes from a deck, not a die

An independent random roll per stage happily deals the same archetype three times running. That is statistically fair and experientially terrible. Instead each three-stage block shuffles a pool and deals without replacement, and no stage may repeat the previous one. Over 48 stages that leaves two repeats, both forced by rules that must win (the ford stage is always a straight pass).

One more thing I got wrong first: I let the shuffle decide the opening stages too. That makes the game’s shop window depend on a random seed and worse, unverifiable, since the shuffle’s output depends on the platform’s RNG. The first journey is now scripted: every archetype appears once, in an order that matches when the player unlocks the tools to handle it. The deck’s job is keeping laps 2, 3 and 4 different from each other, which is what a deck is actually good at.

The principle, restated

All of this is one idea applied repeatedly. Do not generate and hope. Do not generate and validate. Construct the thing that must be true first, and let everything else arrange itself around it. The corridor exists because it was dug before the hazards arrived; the ford is wide because its width was set rather than derived; no tree stands in a gateway because the gateway is excluded from the scatter, not checked afterwards.

It is more work up front than a validation loop, and it never fails on a player’s phone at 11pm on stage 34.

Flockfjord is a sheep herding game for Android built on real sheepdog research — no engine, no 3D files, every mesh and sound generated in code.

Get it on Google Play About the game