Every strategic defense level is, at its core, a path. The path determines where enemies walk, where you place wardens, and how the entire strategic puzzle feels. Get the path wrong and no amount of beautiful terrain or clever warden design saves the level.
TiGen — the procedural pipeline behind Defense of Kyrath — generates every level’s path and river system from math. 1,900 lines of Python that turn a handful of parameters into the spatial backbone of a playable level. This is how the geometry works, and where it meets game design.
Single-Path Shapes
A path needs entry and exit points on map edges. It needs to fill enough of the map that warden placement feels meaningful. And it needs to look organic, not mechanical. TiGen’s single-path generators each solve this differently.
Zigzag is the workhorse — a path that winds back and forth across the map. The algorithm dispatches on entry/exit topology: horizontal-to-horizontal, vertical, L-turn, or same-edge loop. A single winding parameter (typically 2–5) determines whether a level feels like a sprint or a marathon. Every one of the game’s 17 campaign levels started here.
Switchback produces evenly-spaced full-width passes connected by tight U-turns — think mountain road hairpins. These create the highest path density per map area. “The Nomad Horde” uses 5 switchback passes, producing a level that feels like defending against a slow, relentless advance.
Split is a single-entry, single-exit path that forks at 25% of the map, runs parallel lanes along opposite edges, then merges at 75%. You can’t cover both lanes with the same warden placement. The fork itself creates a resource-allocation dilemma that straight-through paths can’t replicate.
Arc traces a smooth curve between entry and exit. A bow_amount parameter controls how far the path bows toward one side; a bow_side parameter picks which side. A gentle wide arc feels entirely different from a tight inside curve — one parameter, a spectrum of shapes, and none of the switchback density.
Multi-Path Generators
Not every level is a single continuous path. TiGen can compose multiple independent flows on the same battlefield.
Parallel lays down 1–8 independent lanes running roughly side by side. A parallel_wiggle parameter controls how much each lane deviates from its ideal, and parallel_tilt rotates the whole stack up to ±30°. The result: a battlefield where enemies arrive in phalanxes instead of a single column, and warden placement has to account for coverage across lanes.
Converge sends three separate flows — from the north, east, and south edges — down bezier tributaries that merge into a single shared exit on the west. The center flow runs straight; the two flanking tributaries curve in smoothly toward the junction. converge_approach_length and converge_source_spread shift whether the confluence happens near the exit or deep in the map.
Dual is the most flexible: it takes a list of independent path specs and composes them, each with its own shape, winding, and bow. Two zigzags crossing in an X. A zigzag running alongside an arc. Anything the single-path roster can produce, dual can stack two or more of, on the same map.
The Underlying Math
Two classical algorithms do the cell-level heavy lifting.
Bresenham’s line algorithm — the same integer-only algorithm that drew lines on 1960s CRT displays — rasterizes every straight segment between waypoints. It guarantees 4-connectivity (no diagonal gaps that would break autotiling) and is fully deterministic, which matters for reproducible procedural generation.
Quadratic Bézier curves smooth the sharp 90° bends at waypoints into organic curves. For each corner, the curve bends toward the waypoint without passing through it — the mathematical definition of a Bézier control point. The radius is clamped to half the shortest adjacent segment to prevent self-intersections, a small detail that takes an hour to debug when it goes wrong.
From centerline to playable path: Chebyshev distance dilation expands the one-cell-wide centerline to the desired path width. Chebyshev (rather than Euclidean) produces square dilation — straight edges that look like a paved road rather than a blobby organic shape. Each cell then gets a 4-bit NSEW connectivity bitmask that Godot’s tileset system uses for automatic tile selection: straight sections, corners, T-junctions, all resolved from the bitmask.
Rivers and Creeks
Rivers don’t affect gameplay — enemies ignore them — but they make the world feel lived-in. TiGen generates two types on a dedicated layer between terrain and path.
Creeks use a recursive branching algorithm. A primary flow enters from a map edge with perpendicular wander, and at each step there’s a probability of spawning a perpendicular branch. The probability decays with depth — main channels branch freely, sub-branches less so, and fourth-level branches not at all. This mirrors real dendritic drainage patterns: dense near the source, thin at the edges.
Rivers are wide channels with sinusoidal center wander — the center drifts according to a sine function tuned to produce 1–2 full oscillations across the map. Edge cells are randomly omitted based on a roughness parameter, creating irregular banks that look natural rather than canal-like.
Where Geometry Meets Game Design
The interesting part isn’t any individual algorithm. It’s how parameters map to player experience.
winding = 3 means three passes across the map — roughly 12–15 seconds of enemy travel time. A single Deadeye (sniper warden) can cover one full pass, but you need three for the whole path, or cheaper Sylvans to spread coverage. The winding parameter alone reshapes which wardens are viable.
Switchbacks make splash damage dominant because reach circles hit multiple passes. Split paths force resource allocation. Parallel lanes punish single-lane specialists — you can’t cover three lanes with one Deadeye. Corner rounding keeps enemies in reach longer on every turn — change corner_radius from 4 to 8 and the level gets slightly easier, not because the wardens changed, but because the geometry did.
None of this is hand-tuned per level. It’s emergent from the math. The designer’s job is understanding the mapping between parameters and player experience. The code handles the rest.