id: protocol
title: The observation carries what a bot needs to reason
status: open
dependsOn: []
exitCriterion: >
Every feature in the basis can be computed from what the wire carries, without
the bot reaching into MegaMek. #
The observation is the interface. What is not in it does not exist for any bot.
Adding a field is deliberate, with a reason in the commit message.
Fixed already: WeaponType.getDamage() returns -2 for cluster weapons, so the
bot valued every missile below nothing. There is now no plain damage field.
The armour getters have the same shape of trap - ARMOR_NA, ARMOR_DOOMED and
ARMOR_DESTROYED are -1, -2 and -3 - and the bridge converts them to zero.
The corpus tests the arithmetic and never the arguments #
Three bugs in one week where the differential passed and real play failed. They
are one bug.
MASC. The observation sent getRunMP(), which folds in a boost this bot
never declares. A Stalking Spider with walk 5 was offered a run of 10 where
the rules give 8.
Deployment elevation. Units were deployed at a hard-coded elevation 0. A
Mek in depth-1 water belongs at -1, and from an elevation
isElevationValid refuses, MegaMek refuses every path out of the hex for the
rest of the match.
Carried MP.MovePath.getMpUsed starts at Entity.mpUsed rather than at
zero, so the allowance the bot was handed was larger than the one MegaMek
enforced, by whatever the unit had already spent.
The movement corpus could not see any of the three, and the reason is the same
each time: it is handed the inputs and asked about the arithmetic. A prone
case is --prone-case x,y,facing,budget. The budget is an argument to the
fixture and a derived quantity in a match, so the fixture can prove "given
allowance B we agree with MegaMek about what B reaches" and can never prove "B
is the allowance". Every one of the three bugs was in B.
The tell is in the fixture itself. SdsPathfind.search calls
unit.fixElevation(), unit.setDone(false), unit.setHullDown(false) and,
until this epic, left mpUsed at whatever a freshly loaded scenario has. Each
of those lines normalises a piece of entity state that a live movement phase
does not normalise - the second bug is literally the harness repairing the state
the host creates. A line in a fixture that cleans up entity state is a class
of bug that fixture can no longer see, and there are four of them in eleven
lines.
The same split runs through the Rust side. pathfind_differential.rs builds its
Walker as a struct literal, so it tests search, which was correct, and never
Walker::running(&unit), which was not: mpUsed had been on the wire for
months, documented, and read by the standing plan's key, and nothing ever read
it for the budget. A field can be carried, parsed, and dropped, and no test here
was shaped to notice.
What to do about it, cheapest first.
Not yet needed: the observation is sent per decision, ~450KB a round at 8v8.
Sending it once per phase would remove most of that. Measured at 291us against
~10ms of thinking, so it is tidiness, not performance.