Most tower defense maps give enemies a fixed lane. In one map of Void Parry's tower-defense mode, "Null Grid", there is no lane at all: an open 32x18 grid, an entry on the left, a core on the right, and every tower you place becomes a wall. You build the level. A long route gives your towers more time to shoot, but every wall costs cash you are not spending on damage.
It needs three small pieces of code, and none of them is A*.
1. One distance field, not one path per enemy
Instead of searching a path for each enemy, I compute one distance field from the exit: every free cell stores how far it is from the core. Enemies then just walk "downhill".
export function mazeField(blocked: Uint8Array, exit: number): Float64Array {
const n = GC * GR, d = new Float64Array(n).fill(Infinity), done = new Uint8Array(n);
d[exit] = 0;
for (;;) { // plain O(n^2) Dijkstra: 576 cells, and it only runs when the maze changes
let u = -1, best = Infinity;
for (let i = 0; i < n; i++) if (!done[i] && d[i] < best) { best = d[i]; u = i; }
if (u < 0) return d;
done[u] = 1;
// relax the 8 neighbours (diagonals cost 1.41) ...
}
}
No priority queue, no heap. With 576 cells the naive version runs in well under a millisecond, and it only runs when a tower is built or sold. I left a comment in the code with the ceiling: a bigger grid would need a real heap.
One detail matters for how it looks: a diagonal step is only allowed if both cells it squeezes between are free. Without that rule, enemies slip through the corner between two towers, and the player reads it as a bug. The same rule also holds in the walker.
2. Walking downhill
Each enemy looks at its 8 neighbours and steps to the one with the lowest distance + step cost. That turns the field into a list of points, and I merge straight runs so the path has few corners. The game already moves enemies along point lists on every other map, so the maze map reuses the normal movement code.
3. Never let the player seal the route
The fun breaks if the player can close the maze completely. So before a tower is placed, a quick breadth-first search runs from the exit with the new cell marked as blocked:
const bl = this.blocked!;
bl[i] = 1;
const ok = mazeOpen(bl, exitCell, [entryCell, ...cellsOfGroundEnemies]);
bl[i] = 0;
return ok ? "" : "block";
It must still reach the entry and every enemy already on the grid. Otherwise you could trap a group of enemies in a pocket. Placing a tower on top of an enemy is refused too. Each refusal returns a reason, so the game can say why ("that would seal the route") instead of silently doing nothing.
Re-routing enemies that are already walking
When a tower goes down, the field is rebuilt once. Then every enemy on the grid gets a new path, starting from its current cell. Enemies still outside the field keep the shared entry path. Flying enemies ignore all of this.
Showing the route before you commit
The first version only showed the new route after you placed a tower, and planning a maze blind is no fun. Now, while you hover a free cell, the game blocks that cell for a moment, rebuilds the lane, and draws it as a dashed yellow line. It only recomputes when the hovered cell (or the maze) changes, so it costs nothing while the pointer rests. A test checks that the preview equals the real route after building.
What's next
- Budget the walls. Cheap "barrier" blocks exist so a maze doesn't eat your whole tower budget. Tuning their price is most of the balance work on this map.
The whole feature is about 60 lines of pathfinding. The field approach scales from one enemy to hundreds for the same cost, and "rebuild on change" keeps it simple.
The game and this code were written with an AI coding assistant; the snippets are from the real build.
Top comments (0)