bayes for days
sds plan mounting.md
18 kB


id: mounting title: A platoon aboard a transport is worth something the basis cannot say status: open dependsOn: [candidates, features] exitCriterion: > A carried unit's worth is scored, and a platoon offered a ride takes it when the ride is worth more than the ground. #

mounting #

The route exists and nothing chooses it. canCarry is on the wire, a candidate that walks into a carrier's hex and ends with LOAD is generated, and in the acceptance scenario it was offered once and declined:

round the platoon chose mount offered
1 stand @ (6,1) 0
2 stand @ (5,0) 1
3-7 stand / flank 0

It is scored as an ordinary stand in the carrier's hex - no line of sight, nothing adjacent - so it loses to every alternative, and by round 3 the platoon had walked away and it was never offered again.

What a carried unit is worth #

Two things are measured and no current column expresses either:

  • it moves at the carrier's speed. A foot platoon runs 1 and a Badger runs 9, so the ride is worth nine times its own pace; a Maxim hover transport is 12. A battle armour squad runs 4, so the same Badger is worth 2.3 - the multiplier is a fact about the pair, not about mounting.
  • it cannot be shot at separately: Entity.isTargetable answers false once the transport id is set.

Whether it can fire is unverified. isEligibleFor(FIRING) answers true for a carried unit, which is not the same question as having a legal shot, and the difference has not been settled. Do not price a silence that has not been measured.

So the trade is at least mobility and protection, bought with something not yet established. The question this epic answers is what that is worth.

What mounting is #

A MoveStepType, not an action. LOAD, MOUNT and UNLOAD are steps, so mounting belongs to the movement decision, and SdsClient.buildPath already carries them - MoveStepType.valueOf has no whitelist. Measured: FORWARDS+LOAD is legal at 2 MP, MOUNT at 0, LOAD alone from an adjacent hex refused.

Stacking and loading are separate permissions. A carrier's hex is barred by stacking and open for loading, and MegaMek grants the two independently, so a carrier that can take a unit makes its hex terminal: it may end there, and nothing walks through. Without that the search never reaches the carrier and there is no route to append LOAD to.

Three predicates, and they are not the same question:

  • Transporter.canLoad - capacity and type alone, blind to where the machines are. It accepts a foot platoon into a FixedWingSupport flying at elevation 999.
  • Entity.canLoad - that plus a position check. The boolean toggles it: canLoad(e, false) matches the transporter, canLoad(e) applies it. This is the one to ask.
  • MovePath.isMoveLegal - neither. It is true for an Atlas that has no handles for the squad, because it validates the path; the load is resolved by the server afterwards.

Open #

Getting off is the carrier's decision #

A carried unit gets no turn, so nothing asks it where it wants to be put down: in a match, from the round after it mounts, only the carrier is ever offered a movement decision. UNLOAD is measured to match - it is the carrier's step, and every form of it on the passenger's own path is refused:

path form legal mp
carrier UNLOAD bare no -
carrier UNLOAD naming the passenger yes 0
carrier FORWARDS then UNLOAD naming the passenger yes 1
passenger any form no -

Three steps, three different rules, which is why each was measured rather than reasoned from the last: LOAD is refused from an adjacent hex and MOVE_ILLEGAL from inside the carrier's own; MOUNT is the passenger's, legal from beside the carrier, and free; UNLOAD is the carrier's, needs the passenger named, is free, and is legal both standing still and after driving.

So a drop costs the carrier nothing but the ground it covers, and the carrier can move and then put its passenger down.

What a drop does, measured #

The named hex is honoured. The carrier sent {"name":"UNLOAD","target":1,"hex":{"x":3,"y":1}} from (3,2) and the passenger was on (3,1) in the next frame, twice in one match. So a drop location is chosen rather than decided by the rules, and SafeDropoff has something to choose between.

A dropped unit gets a movement turn the following round. Dropped in round 2's movement phase, it was asked for a movement decision in round 3.

A unit dropped in the movement phase gets neither a firing turn nor a movement turn that round. Measured on scenarios/tactics/mount-close.mms, where the same platoon is asked for firing decisions with a menu of seven in five other rounds - so the absence in the drop round is the drop and not a lack of targets:

round passenger's movement turn passenger's firing turn
1-3 yes yes, 7 candidates
4 yes (it mounts) -
5 none - the carrier unloads it none
6 yes yes, 7 candidates

Two instruments failed before this one and both failed on the target rather than the mechanism: a Locust that stayed out of the platoon's range all match, and a gun emplacement that was destroyed in round one and stalled the match. An UrbanMech walks 2 and survives.

The jar agrees, and names the mechanism. MovePathHandler.processSteps sends an UNLOAD step to TWGameManager.unloadUnit, which reaches Entity.setTransportId(-1); that one setter sets both unloadedThisTurn and done. isSelectableThisTurn is false while either holds, and determineTurnOrder allocates turns only to entities that pass it, so no firing turn is generated. isEligibleForMovement tests unloadedThisTurn directly. resetEntityPhase clears done between phases but not unloadedThisTurn, which is cleared only in Entity.newRound - which is why the flag survives movement into firing. The one exception is a grounded Small Craft unloading something that is not infantry: that branch clears both flags and grants a movement turn at half MP. It grants movement, not shooting.

This is what MegaMek does. Whether it matches the rules is a question for jmm. In particular, a dropped unit shooting the round it is put down is a thing the code refuses in every branch, and it is worth confirming that is the rule rather than an implementation choice.

So a drop is positional: what the passenger gets is a place to be at the start of its next turn, and SafeDropoff is about where it wants to stand, not where it wants to shoot from this phase.

This is what MegaMek does. Whether it matches the rules is not established here.

Four duties and two profiles, one mechanism #

TaskOverride and TacticOverride now carry Spot, Board, Carry, Deliver, and the Embark and SafeDropoff profiles. None of them needed new machinery. Spot was expressible in existing columns, Embark found the basis gap and recorded it, and the drop-off column is the one addition jmm has sanctioned. Four consumers of one design is a better answer to "can tactics be built from what exists" than any single tactic working.

The algorithm, as jmm gave it #

  • Calculate eligible moves for the carrier with normal feature scoring
  • From that set, calculate all eligible drop-off points for the passenger (typically a subset of moved locations + possibly some adjacency superset depending on type of dismount)
  • For the set of all possible drop-off points, score the passenger's evaluation of where it would LIKE to go
  • For all scored drop-off points, they give their score to the hex the carrier would have to walk to, in order to use that drop-off point
  • This is now a per-hex feature for the carrier, which is 0 in hexes that aren't drop-off eligible, and some normalized-to-1 as "best dropoff I could ask for"
  • The carrier has a TacticsOverride to have it even consider that feature which would normally be weighted 0

It is a back-projection. The passenger's valuation of a drop hex flows backwards through the reachability relation onto the carrier's move hex that would enable it. The carrier is not evaluating the passenger; the passenger's own scoring is being projected.

And it is a new column, which is worth stating rather than slipping past: Spot needed none, Embark found a gap and recorded it, and this one jmm has explicitly sanctioned.

The clamp is the hard part #

The biggest issue with this is that we have to ensure the feature stays 0 if none of the drop-off points are any good for the carried unit at this time. That's how you don't have 3-range infantry mount, and then immediately dismount next turn not being any closer to the enemy. If none of their hexes would lead to good shots they should score the hex 0 as a clamp.

A relative normalisation defeats this outright. "Normalised to 1 as the best drop-off I could ask for", read as best within this set, makes the least-bad drop of a uniformly bad set score 1 - and the platoon dismounts pointlessly, which is the anti-pattern the clamp exists to stop. The reference has to be absolute: what a good position is for that unit, not the best of what is on offer.

range_band_fit is the measure, and two halves of it had to be fixed before it could read. Both were found by using it, and both were the same defect in different places: a conventional platoon's numbers are not on its weapons.

  • The damage. projected_damage summed weapon.damage, and WeaponType.getDamage() answers a sentinel for an infantry weapon - -15.0 for a point that delivers 3.00. ConvInfantry.getDamagePerTrooper() times the trooper count is the real figure, it is on the wire as infantryDamage, and projected_damage reads it.

  • The reach. Fixing the damage moved the column not at all, because projected_damage still asked brackets::projected for the shot - and that refuses a weapon whose bracket damage is nought before it reports the range. Every bin on a conventional infantry weapon is nought, so the rifle that does the shooting was thrown away at the damage gate. Measured off the wire, the 25-trooper Motorized Infantry platoon's whole armament:

    weapon usable short medium long damage bins
    Rifle (Bolt-Action) yes 0 0 3 0.00 / 0.00 / 0.00
    Swarm Mek yes 0 0 0 0.00 / 0.00 / 0.00
    Stop Swarm Attack yes 0 0 0 0.00 / 0.00 / 0.00
    Leg Attack yes 0 0 0 0.00 / 0.00 / 0.00

    The rifle's long bracket of 3 is the only range in the list, and it is the three-range platoon of jmm's requirement. brackets::reaches answers the half that is populated - which bracket, and what the range and the weapon's own modifier charge - and the platoon supplies the damage.

With both, best_damage for that platoon reads 0.24 where it read 0.00, and range_band_fit reads 1.00 inside three hexes and 0.00 beyond.

The clamp, demonstrated #

scenarios/tactics/mount-approach.mms is the instrument, and it exists because neither earlier scenario could ask the question. mount-probe.mms mounts reliably and its enemy is never in reach, so every drop reads nought and a working clamp is indistinguishable from a broken one; mount-close.mms starts everybody on one edge, where the platoon has a shot from the ground and never boards. This one puts them in sequence: the pair deploy apart, the platoon boards while there is nothing to shoot at, and the carrier then drives the length of the board towards an UrbanMech slow enough to still be there.

So the same question is asked in both directions inside one match. Seed 1:

round carrier enemy drop hexes offered best worth past the clamp carrier chose
1 (3,2) (12,14) - - - the platoon mounts
2 (3,2) (12,11) 6 0.00 0 drives to (3,8)
3 (3,8) (10,10) 4 0.00 0 drives to (8,9)
4 (8,9) (9,9) 4 1.00 4 drop at (7,8)
5 (8,9) (9,9) - - - the platoon is on the ground at (7,8)

Both outcomes, and the reason differs the way the requirement says it should: in rounds 2 and 3 the enemy is seven and eight hexes off, outside the rifle's three, so a platoon put down there would be no closer to anything. In round 4 it is adjacent.

It clamps within a round as well as between them. On seed 2 and later in seed 1 the carrier offers six hexes and keeps two, so this is a per-hex bar rather than a per-round switch: the hexes beside the carrier are not all worth the same.

For this passenger the column is a step, not a slope, and that is a fact about the design rather than about the feature. A bolt-action rifle has a single bracket - 0 / 0 / 3 - so its to-hit is the same at one hex as at three, and range_band_fit reads 1.00 everywhere inside the reach and 0.00 outside it. That is all a clamp needs, and it is not enough to rank one drop hex above another by range. A passenger with two brackets would separate them; this one cannot.

Suppressing them all is how the carrier drives on, and it needs no separate branch. Duty::Deliver narrows the carrier's menu to its UNLOAD routes; with none left the narrowing is empty, harmonise::constrain restores the whole menu and counts the duty unsatisfiable. Rounds 2 and 3 above are that count.

See plan/ev.md for the category the damage figure belongs to - four weapon classes now where getDamage() returns a code.

The pattern is still right. It is Bounded, and its denominator is best_damage(posture.unit) - the unit's own best firepower - so it scores against that unit's own standard rather than against the board, the force, or the other candidates. Its zero case is the clamp, already written: "A machine with nothing left to fire has no band to be in. Zero rather than a half: there is no range that would help it."

Two jobs, and the vocabulary already separates them #

"A feature like this is my best option" and "stay 0 when none of them are any good" pull against each other: the least-bad drop of a uniformly bad set is that unit's best option. They are two jobs and the basis already distinguishes them.

  • Local is min-max across this decision's own candidates - literally "my best option". It exists: damage_lead. And Weights will not accept it, enforced by a compile_fail doctest, because a local column "means nothing across two decisions". It ranks a menu and cannot be weighed against anything outside it.
  • Bounded means a rule gives the domain, and the rule may be about the unit: range_band_fit divides by best_damage(posture.unit) and returns zero when the unit has nothing that would help.

So the carrier's column must be Bounded with a per-unit reference, and the clamp is its zero. The carrier is not choosing among drops - it is weighing dropping against driving on, which is a comparison across candidates of different kinds, and a Local column cannot be weighted into that at all. A self-relative ranking is welcome inside the drop set, where argmax is the only thing asked of it.

That resolves both of jmm's constraints without a fourth normalisation: rank within the set if it helps, but what reaches the carrier is measured against the passenger's own standard, and is nought when nothing clears it.

This is a missing feature, not a missing capability. Bounded demonstrably takes a per-unit reference today, so the drop-off column is a feature to write rather than a normalisation to invent.

A carrier is a specific population: an Atlas and a Manticore have MekArms, ClampMountMek, RoofRack and TankTrailerHitch and carry no infantry at all. 286 designs have an InfantryCompartment, and the 1047 with BattleArmorHandles are all OmniMeks.