robo_pilot (faber_tweann v2.4.0)
View SourceRobo Rumble: the controller contract. 17 sensor channels in, 5 intents out.
WHY THIS MODULE EXISTS, AND WHY IT DID NOT UNTIL NOW. The phase 0 pre-registration placed the controller in a faber-tweann module named robo_pilot. No such module existed at the pinned engine commit, and the pin IS the provenance of every phase 0 number, so the controller was written inside the experiment runner instead, with the deviation recorded there rather than hidden. That runner's own note says the fix is "a pin bump plus a file move, and nothing about the measurement changes". This is that move.
It is not tidying. The engine was a library that could simulate a match and could not DRIVE one: the code turning arena state into a genome's inputs, and a genome's outputs into engine intents, lived in a research runner in a different repository. A host machine handed a foreign genome had no way to run it. Every rule of the game a visitor must agree to was therefore unavailable to the only component that needs it. That, and not the wire format, is what blocked the mesh half of this front.
BEHAVIOUR-IDENTICAL BY CONSTRUCTION. Every function body below is the runner's body character for character. Only the names lost their pilot_ prefix, which is redundant in a module called robo_pilot, and act/4 now matches robo_gauntlet:act/4 exactly so a host can drive a genome and a scripted bot through one call shape.
THE EQUIVALENCE IS NOW MEASURED, NOT ASSERTED, AND NO LONGER OWED. An earlier version of this note said robo_pilot_tests proved it. That was wrong twice over: robo_pilot_tests cannot reach the archived champions, which live in the research repository, and until 2026-07-30 no run had compared this module against the runner it was copied from. That replay has now been done. All 20 archived arm S champions were driven through both controllers over all 80 pre-registered held-out starts in both seats: 3200 matches, 599859 turns, 0 intent divergences, 0 pilot-state mismatches, and every individual match outcome identical to the phase 0 feed, all 20 seeds agreeing exactly on wins, losses and draws. The suite was also shown able to go red, by three named corruptions of a copy of this module. The record is faber-programmes/programmes/p7_coevolution/exp066_competence_floor/ exp066_pilot_extraction_equivalence.txt Scope, from that record: equivalence is established on the states those 3200 matches visit, against the predictive_gun floor bot, for arm S genomes of topology 17-12-5. It is not a proof over all inputs. robo_pilot_tests below is the contract, the boundary and the golden vector, and it is not that proof.
THE PERCEPTION BOUNDARY IS A SHAPE, NOT A COMMENT. act/4 destructures the arena to its scans field and to NOTHING ELSE, so #arena.tanks and #arena.bullets are out of scope below that line and an opponent's #tank{} cannot be reached from decide/3 even by accident. The defence is the destructuring; a comment would not survive review, a shape the compiler can check does.
TWO LIMITATIONS THAT ARE PRE-REGISTERED AS LIMITATIONS, not defects, and which any claim about this controller must respect:
The controller CANNOT SEE BULLETS. There is no incoming-fire channel. The only proprioception of being shot is channel 4, an energy delta that also conflates hitting a wall, being rammed, paying to fire and being paid for a hit.
The controller DOES NOT PREDICT. Channels 9 to 14 report the contact's estimated position NOW, by dead reckoning, never where it will be. Solving flight time is strategy and is withheld. So "learned to track" and "learned to handle intermittent observation" are claims this controller cannot support, by design.
NO FLOAT AND NO LIBM, the same rule the rest of the engine keeps. Every quantity is an integer at the arena's scale of 256, and bearings are produced by rotating a scan's vector against robo_sim's own sine table, never by atan2. A scan carries a vector, never an angle, precisely so no inverse trig is needed anywhere in the match path.
Summary
Types
Functions
-spec act({[non_neg_integer()], [integer()]}, state(), #tank{id :: term(), x :: integer(), y :: integer(), heading :: 0..255, gun :: 0..255, radar :: 0..255, vel :: integer(), energy :: integer(), gun_heat :: integer(), dead :: boolean(), damage_dealt :: integer()}, #arena{turn :: non_neg_integer(), tanks :: list(), bullets :: list(), scans :: list()}) -> {#intent{turn_body :: integer(), turn_gun :: integer(), turn_radar :: integer(), accel :: integer(), fire :: integer()}, state()}.
-spec decide({[non_neg_integer()], [integer()]}, state(), #tank{id :: term(), x :: integer(), y :: integer(), heading :: 0..255, gun :: 0..255, radar :: 0..255, vel :: integer(), energy :: integer(), gun_heat :: integer(), dead :: boolean(), damage_dealt :: integer()}) -> #intent{turn_body :: integer(), turn_gun :: integer(), turn_radar :: integer(), accel :: integer(), fire :: integer()}.
-spec init() -> state().
-spec inputs() -> pos_integer().
-spec outputs() -> pos_integer().