Class MutualSupportPathRanker

All Implemented Interfaces:
IPathRanker

public class MutualSupportPathRanker extends BasicPathRanker
Path ranker implementing the Mutual Support movement doctrine (CASPAR divergence #1): maneuver elements advance on the enemy while remaining within supporting range of one another - close enough that any enemy engaging one element can itself be engaged by another - and commit their combat power together rather than sequentially.

Four changes relative to BasicPathRanker:

  1. Supporting-range cohesion replaces herding: instead of a pull toward the (historical) friendly center of mass, the unit is penalized only for ending BEYOND the effective weapons envelope of its nearest friendly element. Inside the envelope, spacing is free - normal combat spread and flanking cost nothing, so the force cannot collapse into a blob. The envelope is derived per friend from its DamageProfile, so staying supported by a short-range brawler means staying closer than staying supported by a fire-support platform.
  2. Set-friend cover bonus: inside the threat envelope, destinations covered by the engagement envelope of a friend that has already moved (a set base of fire) rank above uncovered ones, so the covered advance is the best advance and a lone splinter push is the worst one - but still an advance.
  3. Uniform closing tempo: the aggression term scores the remaining gap to the unit's own peak engagement range in TURNS AT ITS OWN SPEED rather than raw hexes, so a 3/5 assault and a 6/9 medium share one commit tempo (a full move closes one turn's worth for either), and each unit closes to its own band - brawlers to knife range, fire-support to its optimum - not blindly to contact.
  4. Combat posture at water: a defending force does not cross the water it is defending behind - any water, fords included, since a fordable river is one the enemy can cross anywhere. Whether the force is attacking or defending is set explicitly in BehaviorSettings, or read each round from the mission and the enemy's movement (PostureResolver); a defender's crossing paths are charged a full turn of advance, so it holds its bank and fights the enemy in the water instead of wading into the same trap (see calculatePosturePenalty).

The rule that must always hold: keeping formation never stops a unit closing with the enemy. Two things enforce it. The cohesion term charges nothing for a path ending inside the force's formation, and the centre travels with the force, so advancing as a body is free however fast the body moves - what the term costs is leaving the force behind. And the penalty is bounded: it can never exceed what a turn's advance is worth, however far out of position a unit is (see maximumFormationPenalty). Cohesion therefore chooses between ways of closing.

Where "advance as a body" stops being possible, the doctrine gives way. Terrain that a force can only pass a few units at a time - a river ford, a bridge, a city gate - makes someone go first, and going first means leaving the formation. Two things keep that from stalling a crossing: the bound above, and FormationSide, which stops a force split by deep water being measured against a centre sitting in the river. Measured on a river crossing at default settings, one turn charged 13.7 against crossing where a whole turn of advance is worth 37.5 - better than a third of a turn, given up to hold a formation - and companies hesitated at the bank a round at a time. This does not make a crossing free: water entry piloting risk and wading cost are far larger than either figure and still argue against it. It stops cohesion being the term that decides.

An earlier version instead exempted any closing path from cohesion entirely. That sounded like the same guarantee and was much weaker: during an approach nearly every path closes, so cohesion switched off for exactly the phase where formation matters. Measured, a company handed a formation 10.5 hexes tighter than its opponent's had given almost all of it back within four rounds.

  • Constructor Details

    • MutualSupportPathRanker

      public MutualSupportPathRanker(Princess owningPrincess)
  • Method Details

    • getSupportEnvelope

      protected SupportEnvelope getSupportEnvelope(Entity entity)
      Returns the given unit's SupportEnvelope, cached for the round because computing one walks the unit's whole weapon list and a company turn asks about every unit repeatedly. Overridable for tests.
      Parameters:
      entity - the unit whose envelope is wanted
      Returns:
      the unit's engagement envelope; zero ranges for a weaponless unit
    • calculateMutualSupportMod

      protected double calculateMutualSupportMod(Coords friendsCoords, MovePath path)
      Mutual-support modifier, replacing the herding pull toward a center-of-mass point. Positive values (a penalty for ending outside the force's formation) are subtracted from the path utility like the stock cohesion modifier; negative values are the set-friend cover bonus.

      The rule: ending inside the formation costs nothing, and the formation travels with the force, so moving with your own force is always free - mutual support selects among advancing paths, it never vetoes the advance.

      Overrides:
      calculateMutualSupportMod in class BasicPathRanker
      Parameters:
      friendsCoords - the stock ally anchor; unused - the formation centre is computed from live positions
      path - the movement path being evaluated
      Returns:
      the mutual-support modifier (subtracted from utility; negative values are a bonus)
    • doctrineScores

      protected Map<String,Double> doctrineScores()
      Records why the doctrine scored this path the way it did, as extra TSV columns.

      The modifier totals alone cannot answer "why did the bot do this". These are the inputs behind them: where the force's formation actually was, how wide it was allowed to be, how far outside it this path ended, and how many friends covered the destination. Note that the stock friendsCoords columns record the heat-map anchor, which this doctrine does not use - formationCentre is the point it actually measured against.

      Overrides:
      doctrineScores in class BasicPathRanker
      Returns:
      named values to record alongside the path's modifiers
    • formationRadius

      protected int formationRadius(Game game)
      How far from its centre of mass the force may spread, in hexes.

      Deliberately the same figure MutualSupportDeployment uses to place the force in the first place, so movement holds the formation deployment handed it rather than inventing its own idea of one. Cached per round: computing it walks every unit's weapons. Overridable for tests.

    • calculateAggressionMod

      protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game game)
      Uniform closing tempo, replacing the raw-hex aggression scoring. The remaining gap to the unit's own peak engagement range is measured in turns of movement at the unit's own speed, so every element of the force shares one commit tempo: a full move closes one turn's worth whether the unit walks 3 or runs 9. Zero once inside the band - nothing pulls a fire-support unit past its optimum toward point-blank range.

      A laid defense does not pay tempo. Once the enemy is inside contact range and the force's posture is DEFEND, the enemy is coming to us: the closing charge that keeps an attack from dithering would here bleed the defender off its firing positions one hex at a time - measured as the biggest payer in 40% of the defender's remaining two-steps after the attacker-movement fix. Out of contact the charge stands, so a defending force still closes ranks toward its line instead of scattering. The gate reads the unit's CURRENT distance, not the path's, so every candidate path of the pass sees the same flat field.

      Overrides:
      calculateAggressionMod in class BasicPathRanker
      Parameters:
      movingUnit - the unit being moved
      path - the path being evaluated
      game - the current game
      Returns:
      the aggression modifier (subtracted from utility)