-
Notifications
You must be signed in to change notification settings - Fork 339
RFE #7109 CASPAR Protocol - Role Awareness #7736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
|
I'm a little concerned that branding this as CASPAR will cause confusion with the CASPAR project we cancelled earlier in the year |
There was a problem hiding this 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.
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Outdated
Show resolved
Hide resolved
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Outdated
Show resolved
Hide resolved
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Outdated
Show resolved
Hide resolved
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Outdated
Show resolved
Hide resolved
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
|
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
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.
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.
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Dismissed
Show dismissed
Hide dismissed
megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java
Dismissed
Show dismissed
Hide dismissed
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
|
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. |




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
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:
Includes role-based movement order (Scouts first, Juggernauts last) and threat weights (Juggernauts absorb fire, Snipers stay safe).
What Players Should Expect vs Vanilla
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:
Based on: PR #7721, PR #7723