// 01 — 2024–26
Lidar Autonavigation
A* pathfinding and obstacle avoidance inside a 4096-character budget
- Role
- Sole developer
- Stack
- Lua
- Year
- 2024–26
- Status
- Works
- Licence
- GPL-3.0
// Why it exists
Stormworks: Build and Rescue is a physics sandbox where you program vehicle microcontrollers in Lua. I wanted a boat that could be given a destination and get there on its own, reading the world through laser rangefinders.
There was no navigation library to reach for. The game hands you a GPS readout, a compass and some laser distance sensors; it does not hand you pathfinding, an occupancy grid, or any notion of “get me over there”. In Stormworks you either build the map, the search and the helm controller yourself, or you drive the boat.
// The hard part
Two limits shaped almost every decision in the repository, and they pulled in opposite directions. Scripts were capped at 4096 characters — characters, including whitespace, not kilobytes and not lines. And the engine runs logic at 60Hz and drops a script that overruns its tick. So the code had to be both physically small and cheap to execute, and the obvious implementation of almost anything is one or the other but rarely both.
The A* search is incremental, resumed across ticks, because a complete search fits in neither one tick nor the character budget. The obstacle table is keyed on the string "x,y" because obstacles[x..","..y] is dramatically shorter to write than the nested-table equivalent and faster to probe. The system runs on two microcontrollers because it stopped fitting in one — a stabiliser that keeps the beams level, and a navigator that maps, searches, steers and draws, joined by a single composite wire so the timing-critical gimbal work stays isolated from everything that can afford a tick of lag.
Then I ran out of room. The project sat unfinished for months — not because the remaining features were hard, but because there was physically no space left to express them. A game update raised the cap to 8192 and it restarted the same week. Side lasers, spline following and the ghost track all came after that. I don’t think I’d have written it the same way with 8192 from the start, and it would probably have been worse.

// Decisions
Octile heuristic, not Euclidean.
Movement is 8-connected, so octile distance is the exact cost of an unobstructed path while Euclidean underestimates it. An admissible-but-loose heuristic still finds the optimal path — it just expands far more nodes getting there, which on a tick budget means visibly slower replanning.
Diagonal corner-cutting rejected.
A diagonal hop is “pinched” if either flanking orthogonal cell is occupied; pinched hops are dropped. Without this the boat plots a course through the gap between two rocks and drives into both.
Clearance penalty in the cost function.
A geometrically optimal path that clips scenery on every corner is not optimal for a boat with momentum. The search pays a penalty for hugging obstacles, so it prefers the middle of a channel.
Gimbals solved in the body frame.
The stabiliser reads attitude and drives six laser pivots from the same tick’s attitude. One tick of lag smears the beams during fast roll and drops phantom obstacles into the map. Solving in the body frame also means the lasers stay level through a complete capsize.
// What didn't work
Pure pursuit replaced string-pulling, and the symptom took a while to read correctly. Early versions steered at the next visible node on the path. On any boat with a real turning circle the helm sawed back and forth, and I first assumed the path itself was noisy. It wasn’t — by the time the boat responded to a heading command it had already passed the point it was aiming at, so it was permanently correcting toward a target behind it. The fix was to stop aiming at nodes altogether: project the boat onto the corridor polyline and aim at a carrot a set distance further along it, with the look-ahead scaling with speed. The sawing disappeared entirely.
Path tree rebasing was removed. It was an optimisation to reuse the previous search’s tree after the target moved. It cost more characters than it saved ticks, which under a 4096-character cap is a straightforward loss.
// Current state
Works and verified in-game: autonomous point-to-point navigation with live replanning around discovered obstacles, beam stabilisation holding through a full capsize, and the debug map including the predicted ghost track.
Deliberately deferred: it isn’t on the Steam Workshop yet — a ready-to-use vehicle release is planned once performance tuning is finished, so for now you wire it into your own build. Smoothing is corner-merging plus pure pursuit rather than a proper spline, which is good enough that a spline hasn’t earned its characters.
Known rough edges: isObstructed() is called more often than it needs to be, and that’s the next optimisation. Detection is 2D, so overhanging obstacles are invisible to it — fine for boats, wrong for aircraft. The helm output assumes a single rudder and throttle, so it’s boats only.