Skip to content

Conversation

@HammerGS
Copy link
Member

@HammerGS HammerGS commented Dec 8, 2025

RFE #7109: CASPAR Protocol - Princess AI Enhancement (Alpha)

Combines PR #7721 (Allied Damage) and PR #7723 (Role-Aware Positioning) into a unified "CASPAR Protocol" system for Princess AI.

What is CASPAR?

Combined Advanced Situational Positioning And Response - an enhanced AI system that makes Princess fight more like a coordinated military force.

The Three Pillars

1. Allied Threat Assessment

Vanilla Princess sums ALL enemy damage against each unit while only considering her own damage to ONE enemy - making her overly cautious.

CASPAR divides perceived threat by allies who can engage:
Perceived Threat = Enemy Damage / (Allies Engaging + 1)

Example: Enemy Atlas (50 dmg) vs 4 Hunchbacks

  • Vanilla: Each Hunchback fears 50 damage, all hesitate
  • CASPAR: Each sees 10 damage share, all advance confidently

2. Damage Source Pool Tracking

Tracks threat dynamically as units move. First unit to engage an enemy "claims" some threat. Later-moving units see reduced danger from already-engaged enemies, enabling coordinated lance tactics.

3. Role-Aware Positioning

Units position based on weapon loadouts using Alpha Strike damage conversion:

  • Long damage highest: Stay at 21 hexes (Snipers, LRM boats)
  • Medium damage highest: Stay at 12 hexes (Skirmishers)
  • Short damage highest: Close to 3 hexes (Brawlers)
  • MEL ability: Close to 1 hex (dedicated melee units)

Includes role-based movement order (Scouts first, Juggernauts last) and threat weights (Juggernauts absorb fire, Snipers stay safe).

What Players Should Expect vs Vanilla

Situation Vanilla Princess CASPAR Princess
4v1 advantage All units hesitate All advance confidently
LRM Carrier Rushes to melee Maintains 15-21 hex range
Mixed lance All converge on enemy Brawlers close, LRMs back
Scout behavior Moves with main force Screens ahead, moves first

Configuration

CASPAR Protocol is enabled by default. Toggle via Bot Configuration dialog: "CASPAR Protocol (Alpha)" checkbox.

Alpha Warning

Tested primarily with DEFAULT behavior preset. Other presets (Escape, Cowardly, Berserk) may produce unexpected behavior. Please report issues tagged with "Princess" and "CASPAR".

Technical Changes

Files modified:

  • BehaviorSettings.java - Single useCasparProtocol setting
  • BasicPathRanker.java - Aggression modifier, melee threat penalty
  • PathRankerState.java - AS damage calculation, role weights, move order
  • Princess.java - Movement order integration, AS cache initialization
  • FireControl.java - Role-based target preferences
  • BotConfigDialog.java - Single checkbox UI
  • messages.properties - Tooltip with full documentation
  • CASPAR Protocol - Princess AI Enhancement.txt - Player documentation

Based on: PR #7721, PR #7723

HammerGS and others added 24 commits December 5, 2025 14:04
  Fixes #7109

  Root cause: Princess was being overly cautious when she had numerical
  superiority because the bravery formula summed ALL enemy damage against
  her while only considering the MAX damage she could do to ONE enemy.
  This asymmetry caused her to overestimate threats and underestimate
  collective firepower.

  Fix: Implement "Allied Discount" - when calculating expected enemy
  damage, divide each enemy's threat by the number of friendly units
  who can engage that enemy (plus self). This makes Princess more
  aggressive when she has allies who can also shoot back at the same
  targets.

  Example: If an enemy does 50 damage but 3 allies can engage them,
  the perceived threat becomes 50/4 = 12.5 damage.

  Changes:
  - Add considerAlliedDamage setting to BehaviorSettings (default: ON)
  - Add countAlliesWhoCanEngage() helper to BasicPathRanker
  - Apply allied discount in rankPath() enemy damage loop
  - Add UI toggle to BotConfigDialog
  - Add debug logging for decision visibility
  | Issue                          | Status  | Fix
                            |
  |--------------------------------|---------|----------------------------------------------------------------------
  --------------------------|
  | Double-counting moving unit    | ✅ Fixed | Changed countAlliesWhoCanEngage() to accept movingUnit parameter and
   exclude it from the count |
  | Unused myFinalCoords parameter | ✅ Fixed | Removed the unused parameter, replaced with movingUnit
                             |
  | Missing unit tests             | ✅ Fixed | Added 7 test cases in AlliedDamageDiscountTests nested class
                             |

  Test cases added:
  1. testCountAlliesWhoCanEngage_NoAllies - Empty friends list
  2. testCountAlliesWhoCanEngage_OneAllyInRange - Single ally in weapon range
  3. testCountAlliesWhoCanEngage_AllyOutOfRange - Ally beyond weapon range
  4. testCountAlliesWhoCanEngage_ExcludesMovingUnit - Verifies moving unit is excluded
  5. testCountAlliesWhoCanEngage_SkipsOffBoardAllies - Off-board entities ignored
  6. testCountAlliesWhoCanEngage_SkipsNullPositionAllies - Null position handled
  7. testCountAlliesWhoCanEngage_EnemyNullPosition - Enemy null position returns 0
  1. PathRankerState.java

  - Added damageSourcePool map to track remaining unallocated threat from each enemy
  - Added methods:
    - initializeDamagePool() - Initialize pool with all enemies at movement phase start
    - getRemainingThreat() - Get remaining threat from an enemy
    - allocateDamageSource() - Reduce threat after a friendly unit engages
    - isDamagePoolInitialized() - Check if pool has been set up
  - Updated clearState() to also clear the damage pool

  2. BehaviorSettings.java

  - Added useDamageSourcePool boolean setting (default: false)
  - Added getter/setters, copy, XML serialization, toLog, equals, hashCode

  3. BotConfigDialog.java

  - Added useDamageSourcePoolCheck toggle in the UI
  - Integrated with preset saving/loading

  4. messages.properties

  - Added i18n strings for the new checkbox and tooltip

  5. Princess.java

  - Initialize damage pool in initMovement() when setting is enabled
  - Added updateDamagePoolAfterMove() method to allocate threat when a unit commits to a move path
  - Allocation is proportional to damage potential vs enemy health

  6. BasicPathRanker.java

  - Modified threat calculation to use damage pool when enabled
  - Pool approach takes precedence over ally counting heuristic
  - Falls back to considerAlliedDamage when pool is not enabled

  ---
  How It Works

  1. Movement phase start: Pool initializes with all enemy damage potentials
  2. Each unit moves: After path selection, enemies in weapon range get their threat reduced proportionally
  3. Subsequent units: See reduced threat from enemies already being engaged
  4. Benefits: Calculations get faster as pool shrinks, more accurate than counting allies

  The setting useDamageSourcePool is separate from considerAlliedDamage, allowing users to choose which approach
  they prefer.
…sable() during space battle.

  Changes Made

  File 1: MissileWeaponHandler.java
  - Line 1075: Added null check a Summary: Allied Damage Discount Tests Added

                                   I've added 7 new tests to the AlliedDamageDiscountTests nested class in BasicPathRankerTest.java that specifically
                                    cover the damage discount calculation as requested in the dev feedback.

                                   New Tests Added

                                   | Test                                                         | Scenario                       | Expected
                                   Behavior                   |
                                   |--------------------------------------------------------------|--------------------------------|-----------------
                                   --------------------|
                                   | testAlliedDamageDiscount_NoAlliesFullDamage                  | 0 allies engaging              | Full damage (no
                                   discount)           |
                                   | testAlliedDamageDiscount_OneAllyHalvesDamage                 | 1 ally engaging                | Damage / 2 = 50%
                                                       |
                                   | testAlliedDamageDiscount_TwoAlliesThirdsDamage               | 2 allies engaging              | Damage / 3 = 33%
                                                       |
                                   | testAlliedDamageDiscount_ThreeAlliesQuartersDamage           | 3 allies engaging              | Damage / 4 = 25%
                                                       |
                                   | testAlliedDamageDiscount_MixedRangeOnlyCountsInRange         | Some allies in range, some out | Only counts
                                   in-range allies         |
                                   | testAlliedDamageDiscount_FeatureDisabledNoDiscount           | considerAlliedDamage = false   | No discount even
                                    with allies        |
                                   | testAlliedDamageDiscount_MultipleEnemiesIndependentDiscounts | 2 enemies, ally near one       | Each enemy
                                   discounted independently |

                                   Key Formula Documented

                                   perceivedThreat = baseDamage / (alliesEngaging + 1)

                                   Where:
                                   - alliesEngaging = count of friendly units (excluding self) within weapon range of the enemy
                                   - +1 = accounts for self (the moving unit)

                                   Test Coverage Now Includes

                                   Before (existing tests):
                                   - countAlliesWhoCanEngage() method edge cases (7 tests)

                                   After (new tests added):
                                   - Discount formula verification for 0, 1, 2, 3 allies
                                   - Mixed range scenarios
                                   - Feature toggle behavior
                                   - Per-enemy independent calculations

                                   All 14 tests in AlliedDamageDiscountTests pass. start of isNemesisConfusable() method
  - Line 694: Added null check for ammo in handle() method before accessing ammo.getType()

  File 2: CLIATMHandler.java
  - Line 469: Added null check at start of handle() method
  - Line 89: Added null check in calcDamagePerHit()
  - Line 130: Added null check in calcHits()
  - Line 168: Added null check in calcMissileHits()

  Build Status

  - BUILD SUCCESSFUL

  The fix prevents NPE when Aerospace Squadrons fire missile weapons without properly linked ammunition.
  Adds comprehensive Javadoc documentation to the AlliedDamageDiscountTests
  nested class explaining the feature purpose, test scenarios, expected
  behavior, and rationale for each test case.

  Documentation covers:
  - Class-level explanation of RFE-7109 and the discount formula
  - countAlliesWhoCanEngage tests (edge cases for ally counting)
  - Discount calculation tests (formula verification at different ally counts)
  - Feature toggle and per-enemy independence tests
…ts-Option-B' into RFE-7109-Princess-and-Allied-Units-Option-B
  Implements a layered threat assessment and positioning system that
  enables Princess units to fight at optimal ranges based on their
  weapon loadouts and battlefield roles.

  Key features:
  - Optimal range calculation based on expected damage (damage x hit probability)
  - Role-based threat absorption weights (Juggernauts absorb 1.5x, Snipers 0.3x)
  - Role-influenced movement order (Scouts first, Juggernauts last)
  - Role-aware target preferences (15% bonus for preferred matchups)
  - Civilians flee combat (return MAX_VALUE for optimal range)
  - Fallback to armor/speed analysis for units without defined roles

  Behavior changes when enabled:
  - Missile boats stay at 15 hexes instead of rushing to melee
  - Brawlers close to 3 hexes for optimal AC/20 range
  - Long-range units penalized 1.5x for getting too close
  - Mixed lances maintain tactical spacing by role

  New setting: useRoleAwarePositioning (default: false)
  - Requires useDamageSourcePool (auto-enabled)
  - UI checkbox in Bot Config Dialog

  All role weights defined as public static final constants for
  easy tuning during playtesting.
  - useDamageSourcePool from false to true
  - useRoleAwarePositioning from false to true

Clean up some over-logging for damage
  1. Princess.java - Added logRoleAwareInfo() method that logs:
    - Unit's role
    - Optimal engagement range
    - Threat weight
    - Movement order multiplier
    - Whether it's a long-range unit

  This outputs to both the debug log AND the game chat (at DEBUG level).
  2. BasicPathRanker.java - Changed aggression mod logging from TRACE to DEBUG level so it shows in logs.
  3. PathRankerState.java - Changed key logging from DEBUG to INFO:
    - Optimal weapon range calculation
    - Role/weapon mismatch detection

  What you'll see in game chat (when log level is DEBUG):
  [Role-Aware] Atlas AS7-D: Role=JUGGERNAUT, OptRange=3, ThreatWt=1.50, MoveOrder=0.5
  [Role-Aware] Catapult CPLT-C1: Role=MISSILE_BOAT, OptRange=15, ThreatWt=0.30, MoveOrder=3.0 (Long-Range)
  [Role-Aware] Wolverine WVR-6R: Role=SKIRMISHER, OptRange=9, ThreatWt=0.80, MoveOrder=2.0

  What you'll see in the log at DEBUG level:
  - Role info for each unit as it moves
  - Aggression modifier calculations showing optimal range vs actual distance
  - Damage pool allocations with role weights

  You can now test again and should see the role-aware positioning info both in the log file and in the game chat dialog.
  Files Modified:

  1. PathRankerState.java - Added 3 new static calculation methods:
    - calculateOptimalRangeForEntity(Entity) - Returns optimal engagement range based on role
    - calculateThreatWeightForEntity(Entity) - Returns threat absorption weight (0.0-1.5)
    - calculateMoveOrderMultiplierForEntity(Entity) - Returns movement priority (0.1-4.0)
  2. UnitState.java - Added 3 new TSV fields after ROLE:
    - OPTIMAL_RANGE - Role-based optimal engagement range (3, 9, 15, etc.)
    - THREAT_WEIGHT - Role threat absorption weight
    - MOVE_ORDER_MULT - Role movement order multiplier
  3. UnitAction.java - Added same 3 new TSV fields after ROLE:
    - OPTIMAL_RANGE, THREAT_WEIGHT, MOVE_ORDER_MULT

  Expected TSV Output:

  After running a game, the game_actions_0.tsv will now include columns like:
  ROLE | OPTIMAL_RANGE | THREAT_WEIGHT | MOVE_ORDER_MULT
  JUGGERNAUT | 3 | 1.5 | 0.5
  MISSILE_BOAT | 15 | 0.3 | 3.0
  SKIRMISHER | 9 | 0.8 | 2.0

  This allows single-source validation:
  - See at a glance if each unit is moving toward/away from its optimal range
  - Verify threat weights are being calculated correctly
  - Confirm movement order multipliers match roles
  - Track distance to enemy turn-over-turn in context
…ation consistently:

  1. Updated calculateOptimalRange() (instance method) - Now delegates directly to calculateOptimalRangeFromWeapons() instead of using the hybrid
  role-based approach
  2. Removed unused code:
    - ROLE_RANGE_MISMATCH_THRESHOLD constant
    - getRangeForRole() instance method
    - getPreferredRangeForRole() static method
  3. How it now works:
    - For each sample range (1, 3, 6, 9, 12, 15, 18, 21, 24, 30 hexes)
    - Calculate expected damage = sum of (weapon damage x probability to hit)
    - Account for range modifiers (+0 short, +2 medium, +4 long)
    - Account for minimum range penalties
    - Use baseline gunnery 4
    - Return the range with highest expected damage

  Example outcomes:
  - Atlas with AC/20: optimal at short range (3-6 hexes) due to high damage short-range weapon
  - Trebuchet with LRM-15: optimal at medium-long range (9-15 hexes) due to minimum range penalties at close range
  - Mixed loadout mech: finds the range that maximizes total expected damage from all weapons

  The TSV logging and Princess runtime will now both calculate optimal ranges the same way - purely from weapon analysis rather than hardcoded role
  values.
  1. New Constants (lines 104-107):
    - OPTIMAL_RANGE_SHORT = 3 (0-6 hexes)
    - OPTIMAL_RANGE_MEDIUM = 12 (7-24 hexes)
    - OPTIMAL_RANGE_LONG = 21 (25-42 hexes)
  2. Instance method calculateOptimalRangeFromWeapons() (lines 291-325):
    - Converts the entity to Alpha Strike using ASConverter.convert(entity)
    - Gets standard damage via element.getStandardDamage()
    - Compares S, M, L damage values (minimal damage counts as 1)
    - Returns the range with highest damage (ties favor closer range)
  3. Static method calculateWeaponOptimalRange() (lines 567-595):
    - Same logic as above, but static for use by TSV logging (UnitAction/UnitState)
  4. Removed (from previous session):
    - SAMPLE_RANGES constant
    - BASELINE_GUNNERY constant
    - calculateExpectedDamageAtRange() method
    - calculateStaticExpectedDamageAtRange() method
  Enhance optimal range calculation to factor in pilot gunnery skill:
  - Flat damage profiles (S=M=L) now prefer medium range as safe default
  - Elite gunners (Gunnery 2-3) shift optimal range UP one bracket
  - Green gunners (Gunnery 5+) shift optimal range DOWN one bracket
  - Standard gunners (Gunnery 4) use base damage-derived range

  This addresses units like the Trebuchet (2-2-2 damage profile) that
  previously defaulted to short range due to tie-breaker logic. Now
  they prefer medium range for safer positioning at equal damage output.

  Examples with Gunnery 4:
  - Trebuchet (2-2-2): 12 hexes (flat profile -> medium)
  - Atlas (5-5-2): 3 hexes (S=M > L -> short)
  - Catapult (2-3-2): 12 hexes (M > S -> medium)
  - Wolverine (2-2-1): 3 hexes (S=M > L -> short)
  Problem Found: Units with damaged weapons showing S=2, M=2, L=0 were staying at OPTIMAL_RANGE=12 instead of closing to range 3.

  Root Cause: The gunnery adjustment logic for elite gunners (gunnery 2-3) was shifting optimal range UP whenever mDamage >= sDamage. With equal damage at
   S and M (both 2), the elite gunner was being told to stay at medium range - even though there was no damage advantage to doing so.

  Fix: Changed >= to > for the elite gunner shift:
  - Before: mDamage >= sDamage → shift to medium (even if equal)
  - After: mDamage > sDamage → only shift if medium range is BETTER

  New Behavior with S=2, M=2, L=0:
  1. Base optimal range = SHORT (3) because S=M, and that's the aggressive default when equal
  2. Elite gunner check: mDamage > sDamage → 2 > 2 → FALSE, no shift
  3. Final optimal range = 3 (close in!)
I need this as part of this PR for testing.
  Units with the MEL special ability (dedicated melee weapons like
  hatchets, swords, claws) now target 1 hex engagement range instead
  of the standard 3 hex short range. This ensures melee-focused units
  will close to actually use their physical attacks.

  Changes:
  - Add OPTIMAL_RANGE_MELEE constant (1 hex) for physical attacks
  - MEL units add Size+1 damage to short range bracket calculation
  - MEL units with short-range optimal now target 1 hex (melee range)
  - Units with MEL but no ranged weapons are not classified as civilians
  - Add HAS_MEL field to TSV logging (UnitState, UnitAction)
  - Add defensive try/catch for ASConverter in tests with mock entities

  This makes a Berserker (MEL, Size 4) want to close to 1 hex while
  a standard Atlas (no MEL) is content at 3 hexes.
  Fix 1: Range Threshold (Prevent Oscillation)

  Files Modified: PathRankerState.java
  - Added DAMAGE_THRESHOLD = 2 constant
  - Updated determineBaseOptimalRange() (instance and static versions)
  - Now requires 2+ point damage difference before changing optimal range
  - Result: Atlas with S=5, M=5 that loses a weapon (S=4, M=5) will stay at SHORT range instead of suddenly backing to 12 hexes

  Fix 2: Enemy-Aware Armor Facing Bias

  Files Modified: FacingDiffCalculator.java
  - Renamed getBiasTowardsFacing() to getArmorBias()
  - Updated documentation to clarify the bias logic
  - The bias now correctly exposes the stronger armor side to the enemy when turning
  - Result: When facing an enemy, the unit will turn to present its stronger armor side to them

  Fix 3: Role-Based Melee Avoidance

  Files Modified: PathRankerState.java, BasicPathRanker.java
  - Added melee threat multiplier constants (Brawler=1.0, Sniper=0.2, etc.)
  - Added getMeleeThreatMultiplier(), hasEnemyMEL(), and calculateMeleeThreatPenalty() methods
  - Modified evaluateMovedEnemy() to add melee threat penalty when role-aware positioning is enabled
  - Result: Units will avoid being at melee range (1 hex) of Brawlers/Juggernauts, preferring range 2-3

  Tests Added

  - testDamageThreshold_PreventsSinglePointOscillation()
  - testMeleeThreatMultiplier_BrawlerMostDangerous()
  - testMeleeThreatPenalty_OnlyAtMeleeRange()
  - testMeleeThreatPenalty_ScalesByRole()
  - testMeleeThreatMultiplier_UndeterminedUsesDefault()
  Summary of Changes

  1. Fixed determineBaseOptimalRange() (both instance and static versions)

  Old logic (broken):
  // Required L > M + 2 AND L > S + 2 for LONG range
  // A Pillager with 3-3-4 couldn't meet this (4 > 5 is false)

  New logic:
  // Long range is best or tied for best (and better than short)
  if (lDamage >= mDamage && lDamage > sDamage) {
      return OPTIMAL_RANGE_LONG;
  }

  // Medium is strictly better than short
  if (mDamage > sDamage) {
      return OPTIMAL_RANGE_MEDIUM;
  }

  // Short range is best or tied - stay aggressive
  return OPTIMAL_RANGE_SHORT;

  2. Updated adjustRangeForGunnery() for elite pilots

  Old: mDamage > sDamage (strict)
  New: mDamage >= sDamage (allows ties)

  Elite pilots now shift to longer range when damage is equal, leveraging their accuracy advantage at range.

  ---
  Expected Results with New Logic

  | Unit      | S-M-L        | Gunnery   | Old Result | New Result  |
  |-----------|--------------|-----------|------------|-------------|
  | Pillager  | 3-3-4        | 4         | SHORT (3)  | LONG (21)   |
  | Longbow   | 2-3-3        | 4         | SHORT (3)  | LONG (21)   |
  | Atlas     | 5-5-2        | 4         | SHORT (3)  | SHORT (3)   |
  | Daishi    | 6-6-4        | 2 (Elite) | SHORT (3)  | MEDIUM (12) |
  | Nova      | 5-5-0        | 3 (Elite) | SHORT (3)  | MEDIUM (12) |
  | Berserker | 3-3-1 (+MEL) | 4         | MELEE (1)  | MELEE (1)   |
  I've implemented the changes to make GIF summaries and TSV logs use the same timestamp-based naming. Here's a summary:

  Changes Made:

  1. Game.java (lines 125-136, 3596-3605):
    - Added timestampString field initialized when game is created
    - Added TIMESTAMP_FORMAT constant (yyyyMMdd_HHmmss)
    - Added getTimestampString() getter method
    - Updated reset() to regenerate timestamp on game reset
  2. MinimapPanel.java (lines 331, 347):
    - Changed game.getUUIDString() to game.getTimestampString() for:
        - GIF folder naming
      - GIF file naming
  3. GameDatasetLogger.java (lines 75, 93-106, 114):
    - Added nextTimestamp field
    - Added setNextTimestamp() method to accept external timestamp
    - Modified newLogFile() to use provided timestamp if set
    - Modified requestNewLogFile() to clear the stored timestamp
  4. TWGameManager.java (line 1932):
    - Added call to datasetLogger.setNextTimestamp(game.getTimestampString()) before first log entry

  Result:

  Now when you run a game, both files will share the same timestamp:
  - TSV: logs/game_actions_20251207_165432.tsv
  - GIF folder: logs/gameSummary/minimap/20251207_165432/
  - GIF file: logs/gameSummary/minimap/20251207_165432/20251207_165432.gif

  This makes it easy to correlate TSV analysis with GIF review for the same game session.
Needed it fixed to I can tell the sides apart.
  Changes Made

  PathRankerState.java (line 689-702):
  - Added hasAntiMech(Entity entity) method to check for the AM special ability via Alpha Strike conversion

  BasicPathRanker.java (lines 534-552):
  - Added dynamic range logic in calculateAggressionMod() for AM units:
    - Calculates striking distance = Walk MP x 2
    - If closest enemy is within striking distance: set optimal range to MELEE (1 hex) - close aggressively for swarm
    - If enemy is beyond striking distance: set optimal range to SHORT (3 hexes) - wait in ambush, don't charge across open ground

  Behavior Summary

  | Scenario                  | Behavior                                         |
  |---------------------------|--------------------------------------------------|
  | Enemy within MP x 2 hexes | Close to melee range (1 hex) for swarm attack    |
  | Enemy beyond MP x 2 hexes | Wait at short range (3 hexes) in ambush position |

  Example

  - Elemental with 1 MP, enemy 10 hexes away: striking distance = 2 hexes
    - 10 > 2, so stay at SHORT range (3 hexes) - wait for enemy to come closer
  - Same Elemental, enemy 2 hexes away:
    - 2 <= 2, so close to MELEE range (1 hex) - go for the swarm

  This prevents Battle Armor from suicidally charging 15 hexes across open terrain where they'll get shot to pieces. Instead, they'll
  wait in cover until targets get close enough to reach in 1-2 turns.
  Princess: Overwatch herding for long-range units

  Long-range units (snipers, missile boats) now herd to an "overwatch"
  position behind friendly lines rather than joining the main group.
  This allows them to advance with the force while maintaining standoff.

  Changes:
  - calculateHerdingMod() now accepts optional entity and enemy position
  - Long-range units herd to a point offset behind friendsCoords by
    half their optimal range, in the direction away from enemies
  - Increased asymmetric close-range penalty from 1.5x to 2.0x for
    long-range units, making them more strongly prefer backing away
  - Added backward-compatible 2-parameter overload for existing callers

  Root cause: Long-range units were herding directly to friendly center,
  which pulled them into close combat instead of maintaining standoff.

  Fix: Offset herd target for long-range units by optimalRange/2 hexes
  behind the friendly center, away from enemy median position. Combined
  with stronger close-range penalty, this keeps missile boats and snipers
  at appropriate engagement distance while still moving with the force.
  Removed deprecated methods entirely - no legacy wrapper methods for Alpha feature:

  | File                     | Change
                        |
  |--------------------------|------------------------------------------------------------------------
  ----------------------|
  | BehaviorSettings.java    | Removed 9 deprecated wrapper methods, kept only
  isUseCasparProtocol()/setUseCasparProtocol() |
  | BasicPathRanker.java     | Updated all 4 checks to use isUseCasparProtocol()
                        |
  | FireControl.java         | Updated 1 check to use isUseCasparProtocol()
                        |
  | Princess.java            | Updated 5 checks to use isUseCasparProtocol()
                        |
  | BasicPathRankerTest.java | Updated test mocks and comments
                        |

  XML backward compatibility preserved - old save files with considerAlliedDamage,
  useDamageSourcePool, or useRoleAwarePositioning set to true will enable CASPAR Protocol.

  The code is now cleaner for an Alpha feature - just one setting, no deprecated baggage. If CASPAR
  doesn't work out, there's nothing to clean up.
@HammerGS HammerGS requested a review from a team as a code owner December 8, 2025 03:51
Copilot AI review requested due to automatic review settings December 8, 2025 03:51
@HammerGS HammerGS added AI Generated Fix AI-generated fix. Requires human testing and review before merging. Needs Player Testing PR lacks actual play testing. labels Dec 8, 2025
@HammerGS HammerGS added the Princess/AI Issues or PR that relate to the current Bot AI label Dec 8, 2025
@HammerGS HammerGS added the In Development (Draft) An additional way to mark something as a draft. Make it stand out more. label Dec 8, 2025
@IllianiBird
Copy link
Collaborator

I'm a little concerned that branding this as CASPAR will cause confusion with the CASPAR project we cancelled earlier in the year

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the "CASPAR Protocol" (Combined Advanced Situational Positioning And Response) enhancement for Princess AI, combining allied damage awareness, damage source pool tracking, and role-aware positioning into a unified system controlled by a single toggle.

Key Changes:

  • Princess AI now factors in friendly unit support when evaluating threats (enemies facing multiple allies are less threatening per unit)
  • Units position themselves based on weapon loadouts using Alpha Strike damage profiles (LRM boats stay at 15-21 hexes, brawlers close to 1-3 hexes)
  • Tactical movement order based on roles (Scouts first, Juggernauts last) with role-based threat weights

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
BasicPathRankerTest.java Adds 1100+ lines of comprehensive unit tests for allied damage discount and role-aware positioning features
PathRankerState.java Implements core role-aware logic including optimal range calculation, threat weights, movement order, and damage pool tracking
BasicPathRanker.java Integrates role-aware positioning into path ranking with aggression/herding modifiers and melee threat penalties
Princess.java Adds movement phase initialization for AS damage cache and damage pool, plus role-aware logging
FireControl.java Implements role-based target preference multipliers (15% bonus for preferred matchups)
BehaviorSettings.java Adds single useCasparProtocol toggle with XML serialization and legacy field migration
BotConfigDialog.java Adds UI checkbox for CASPAR Protocol with comprehensive tooltip documentation
messages.properties Adds detailed tooltip text explaining the three pillars of CASPAR Protocol
GameDatasetLogger.java Changes to timestamp-based filenames instead of counter-based for consistency with GIF summaries
Game.java Adds timestamp field to game instance for consistent file naming across logs and summaries
MinimapPanel.java Enhances team coloring to distinguish multiple enemy teams with distinct colors
Mek.java Changes crippled status logging from debug to trace level to reduce log spam
TWGameManager.java Sets dataset logger timestamp at phase start for file naming consistency
FacingDiffCalculator.java Refactors armor bias calculation with improved documentation (renamed method, same logic)
UnitState.java / UnitAction.java Adds Alpha Strike damage fields and role-aware metrics to TSV dataset logging

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@HammerGS
Copy link
Member Author

HammerGS commented Dec 8, 2025

I'm a little concerned that branding this as CASPAR will cause confusion with the CASPAR project we cancelled earlier in the year

It borrows heavily from the work that went into that project. If we merge this I'm planning a full blog post on it.

  Problem: NullPointerException at line 162 in PrincessTest.testCalculateMoveIndex()

  Root Cause: The CASPAR Protocol code in Princess.calculateMoveIndex() calls:
  if (getBehaviorSettings().isUseCasparProtocol()) {
      double roleMultiplier = pathRankerState.getRoleMoveOrderMultiplier(entity);

  But the test didn't mock getBehaviorSettings(), causing NPE.

  Fix: Added mock for BehaviorSettings with CASPAR disabled:
  BehaviorSettings mockBehavior = mock(BehaviorSettings.class);
  when(mockBehavior.isUseCasparProtocol()).thenReturn(false);
  when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior);
…s-role-awareness' into RFE-7109-CASPAR-Protocol-Princess-role-awareness
@HammerGS
Copy link
Member Author

HammerGS commented Dec 8, 2025

All comments in Co-pIlot feedback is Claude's answer.

  Code quality improvements based on automated review:
  - Change high-frequency aggression log from debug to trace level
  - Return unmodifiable map from getDamageSourcePool() to prevent internal state modification
  - Remove trivially true/false assertions in test (15 >= 12, 3 >= 12)
  - Clarify armor bias comments to describe intent, not direction
  - Add "TACTICAL OVERRIDE" comment for AM unit dynamic range logic
  - Fix RFE reference format (RFE-7109 -> RFE #7109)
  - Improve role-based positioning Javadoc wording
@HammerGS HammerGS changed the title Rfe 7109 caspar protocol princess role awareness RFE #7109 CASPAR Protocol - Role Awareness Dec 8, 2025
@HammerGS
Copy link
Member Author

HammerGS commented Dec 8, 2025

VANILLA PRINCESS
TEST 1 Vanilla Princess

PRINCESS WITH CASPAR
TEST 1  Princess CASPAR

  Tests were calling getDamageSourcePool().put() to inject test data,
  which failed after making the getter return an unmodifiable map.

  Added setDamageSource(enemyId, threat) method for controlled test
  injection while keeping getDamageSourcePool() safely immutable.
@HammerGS
Copy link
Member Author

HammerGS commented Dec 8, 2025

BOSS FIGHT - VANILLA PRINCESS
Boss Fight Vanilla Princess

BOSS FIGHT - PRINCESS WITH CASPAR
Boss Fight Princess CASPAR 1

  Add comprehensive test coverage for CASPAR Protocol behaviors based on
  the documented test scenarios in CASPAR Scenarios.txt.

  New CASPARScenarioTests nested class with 16 tests covering:
  - Mixed Lance Engagement: role-based optimal ranges and movement order
  - Gunnery Skill Impact: threshold constants and damage oscillation prevention
  - Numerical Superiority: threat distribution and role weight amplification
  - Close Combat: MEL constants, melee danger by role, range-only penalties
  - Standoff Tactics: long-range threat weights and classification thresholds
  - Civilian Handling: flee behavior, move-last order, zero threat absorption

  All tests verify the key CASPAR behaviors that enable Princess AI to
  position units according to their battlefield roles and weapon loadouts.
  Two tests were comparing literal values instead of testing actual code:

  - testAlliedDamageDiscount_FeatureDisabledNoDiscount: Replaced
    assertEquals(50.0, 50.0) tautology with assertions that verify
    the mock behavior and method results

  - testDamageThreshold_PreventsSinglePointOscillation: Renamed to
    testDamageThreshold_ConstantValue and added meaningful assertion
    that the threshold is > 1 (replacing empty comment block)

  These fixes address CodeQL warnings about comparing identical values.
  Files Modified:

  1. megamek/src/megamek/ai/dataset/UnitState.java
    - Added GUNNERY and PILOTING to the Field enum (lines 79-80)
    - Added crew skill extraction logic (lines 155-162)
  2. megamek/src/megamek/ai/dataset/UnitAction.java
    - Added GUNNERY and PILOTING to the Field enum (lines 83-84)
    - Added crew skill extraction logic (lines 194-201)

  New TSV Columns:
  | Column   | Description                        | Default |
  |----------|------------------------------------|---------|
  | GUNNERY  | Pilot gunnery skill (2-8 typical)  | 4       |
  | PILOTING | Pilot piloting skill (2-8 typical) | 5       |

  Data Analysis Enabled:
  - Verify gunnery-based optimal range adjustments (elite vs green pilots)
  - Correlate pilot skill with movement decisions
  - Validate that green pilots (gunnery 5+) close to short range when S >= M damage
@BLR-IIC
Copy link
Contributor

BLR-IIC commented Dec 9, 2025

I'm sorry to see the original Caspar AI opponent project was canceled. But these tweaks look nice.

I'm a little concerned about moving "scout" units first. Personally, if I find my scouts in a brawl and I think they can do enough damage matter, I move them after slower units to take advantage of weakened armor facings of targets which already moved. Has that been considered ?

@HammerGS
Copy link
Member Author

HammerGS commented Dec 9, 2025

I'm sorry to see the original Caspar AI opponent project was canceled. But these tweaks look nice.

I'm a little concerned about moving "scout" units first. Personally, if I find my scouts in a brawl and I think they can do enough damage matter, I move them after slower units to take advantage of weakened armor facings of targets which already moved. Has that been considered ?

It has but for now working on keeping things simple to start.

@HammerGS HammerGS marked this pull request as draft December 11, 2025 18:54
@HammerGS HammerGS added the For New Dev Cycle This PR should be merged at the beginning of a dev cycle label Dec 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Fix AI-generated fix. Requires human testing and review before merging. For New Dev Cycle This PR should be merged at the beginning of a dev cycle In Development (Draft) An additional way to mark something as a draft. Make it stand out more. Needs Player Testing PR lacks actual play testing. Princess/AI Issues or PR that relate to the current Bot AI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants