diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index fda0b728a08..e60758f55a8 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -734,7 +734,7 @@ protected void calculateTargetingOffBoardTurn() { sendDone(true); } - private Map calcAmmoConservation(final Entity shooter) { + protected Map calcAmmoConservation(final Entity shooter) { final double aggroFactor = getBehaviorSettings().getHyperAggressionIndex(); final StringBuilder msg = new StringBuilder("\nCalculating ammo conservation for ") .append(shooter.getDisplayName()); @@ -763,9 +763,15 @@ private Map calcAmmoConservation(final Entity shooter) { final WeaponType weaponType = weapon.getType(); msg.append("\n\t").append(weapon); if (!(weaponType instanceof AmmoWeapon)) { - ammoConservation.put(weapon, 0.0); + // Just require a 12 or lower TN + ammoConservation.put(weapon, 0.01); msg.append(" doesn't use ammo."); continue; + } else if (weaponType.hasFlag(WeaponType.F_ONESHOT)) { + // Shoot OS weapons on a 10 / 9 / 8 for Aggro 10 / 5 / 0 + ammoConservation.put(weapon, (35 - 2.0 * aggroFactor) / 100.0); + msg.append(" One Shot weapon."); + continue; } int ammoCount = 0; @@ -776,13 +782,14 @@ private Map calcAmmoConservation(final Entity shooter) { ammoCount += ammoCounts.get(ammoType); } msg.append(" has ").append(ammoCount).append(" shots left"); - // Desired behavior: - // At min aggro (0 of 10), require ~50% chance to hit with > 3 shots left - // At normal aggro (5 of 10), require at least 10% to-hit chance with > 3 shots left - // At max aggro (10 of 10) require just over 0% chance to hit until at 1 round left. + // Desired behavior, with 7 / 3 / 1 rounds left: + // At min aggro (0 of 10), fire on TN 10, 9, 7 + // At normal aggro (5 of 10), fire on 12, 11, 10 + // At max aggro (10 of 10), fire on 12, 12, 10 final double toHitThreshold = - Math.max(0.0, - (0.8/((aggroFactor) + 2) + 1.0 / ((ammoCount*ammoCount)+1))); + Math.max(0.01, + (0.6/((8*aggroFactor) + 4) + + 4.0 / (4 * (ammoCount*ammoCount) * (aggroFactor + 2) + (4 / (aggroFactor + 1))))); msg.append("; To Hit Threshold = ").append(new DecimalFormat("0.000").format(toHitThreshold)); ammoConservation.put(weapon, toHitThreshold); } diff --git a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java index 2053d7c39e9..65eed94bf5c 100644 --- a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java +++ b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java @@ -22,6 +22,7 @@ import megamek.client.bot.princess.PathRanker.PathRankerType; import megamek.common.*; import megamek.common.enums.GamePhase; +import megamek.common.equipment.WeaponMounted; import megamek.common.options.GameOptions; import megamek.common.options.OptionsConstants; import org.junit.jupiter.api.BeforeAll; @@ -30,10 +31,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -45,6 +45,9 @@ */ public class PrincessTest { + static WeaponType mockAC5 = (WeaponType) EquipmentType.get("ISAC5"); + static AmmoType mockAC5AmmoType = (AmmoType) EquipmentType.get("ISAC5 Ammo"); + static WeaponType mockRL20 = (WeaponType) EquipmentType.get("RL20"); private Princess mockPrincess; private BasicPathRanker mockPathRanker; @@ -63,6 +66,7 @@ public void beforeEach() { when(mockPrincess.getPathRanker(PathRankerType.Basic)).thenReturn(mockPathRanker); when(mockPrincess.getPathRanker(any(Entity.class))).thenReturn(mockPathRanker); when(mockPrincess.getMoraleUtil()).thenReturn(mockMoralUtil); + when(mockPrincess.calcAmmoConservation(any(Entity.class))).thenCallRealMethod(); } @Test @@ -359,19 +363,19 @@ public void testIsFallingBack() { when(mockPrincess.wantsToFallBack(any(Entity.class))).thenReturn(false); when(mockPrincess.isFallingBack(any(Entity.class))).thenCallRealMethod(); - + BehaviorSettings mockBehavior = mock(BehaviorSettings.class); when(mockBehavior.getDestinationEdge()).thenReturn(CardinalEdge.NONE); when(mockBehavior.isForcedWithdrawal()).thenReturn(true); when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); - + // A normal undamaged mech. assertFalse(mockPrincess.isFallingBack(mockMech)); // A mobile mech that wants to fall back (for any reason). when(mockMech.isCrippled(anyBoolean())).thenReturn(true); assertTrue(mockPrincess.isFallingBack(mockMech)); - + // A mech whose bot is set for a destination edge when(mockBehavior.getDestinationEdge()).thenReturn(CardinalEdge.NEAREST); assertTrue(mockPrincess.isFallingBack(mockMech)); @@ -537,4 +541,138 @@ public void testIsImmobilized() { when(mockMech.isStuck()).thenReturn(true); assertTrue(mockPrincess.isImmobilized(mockMech)); } + + @Test + public void testCalcAmmoForDefaultAggressionLevel() throws megamek.common.LocationFullException { + // Expected toHitThresholds should equate to a TN of 12, 11, and 10 for ammo values + // of 7+, 3+, 1. + + // Set aggression to default level + BehaviorSettings mockBehavior = mock(BehaviorSettings.class); + when(mockBehavior.getHyperAggressionIndex()).thenReturn(5); + when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); + + // Set up unit + Mech mech1 = new BipedMech(); + Mounted bin1 = mech1.addEquipment(mockAC5AmmoType, Mech.LOC_LT); + Mounted wpn1 = mech1.addEquipment(mockAC5, Mech.LOC_RT); + + // Check default toHitThresholds + // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s + double target = Compute.oddsAbove(12) / 100.0; + bin1.setShotsLeft(7); + Map conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 3+ rounds for this level should allow firing on 11s + target = Compute.oddsAbove(11) / 100.0; + bin1.setShotsLeft(3); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 1 rounds for this level should allow firing on 10s + target = Compute.oddsAbove(10) / 100.0; + bin1.setShotsLeft(1); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } + + @Test + public void testCalcAmmoForMaxAggressionLevel() throws megamek.common.LocationFullException { + // Expected toHitThresholds should equate to a TN of 12, 12, and 10 for ammo values + // of 7+, 3+, 1. + + // Set aggression to default level + BehaviorSettings mockBehavior = mock(BehaviorSettings.class); + when(mockBehavior.getHyperAggressionIndex()).thenReturn(10); + when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); + + // Set up unit + Mech mech1 = new BipedMech(); + Mounted bin1 = mech1.addEquipment(mockAC5AmmoType, Mech.LOC_LT); + Mounted wpn1 = mech1.addEquipment(mockAC5, Mech.LOC_RT); + + // Check default toHitThresholds + // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s + double target = Compute.oddsAbove(12) / 100.0; + bin1.setShotsLeft(7); + Map conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 3+ rounds for this level should allow firing on 12s + bin1.setShotsLeft(3); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 1 rounds for this level should allow firing on 10s + target = Compute.oddsAbove(10) / 100.0; + bin1.setShotsLeft(1); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } + + @Test + public void testCalcAmmoForZeroAggressionLevel() throws megamek.common.LocationFullException { + // Expected toHitThresholds should equate to a TN of 10, 9, and 7 for ammo values + // of 7+, 3+, 1. + + // Set aggression to default level + BehaviorSettings mockBehavior = mock(BehaviorSettings.class); + when(mockBehavior.getHyperAggressionIndex()).thenReturn(0); + when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); + + // Set up unit + Mech mech1 = new BipedMech(); + Mounted bin1 = mech1.addEquipment(mockAC5AmmoType, Mech.LOC_LT); + Mounted wpn1 = mech1.addEquipment(mockAC5, Mech.LOC_RT); + + // Check default toHitThresholds + // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s + double target = Compute.oddsAbove(10) / 100.0; + bin1.setShotsLeft(7); + Map conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 3+ rounds for this level should allow firing on 11s + target = Compute.oddsAbove(9) / 100.0; + bin1.setShotsLeft(3); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // Default toHitThreshold for 1 rounds for this level should allow firing on 10s + target = Compute.oddsAbove(7) / 100.0; + bin1.setShotsLeft(1); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } + + @Test + public void testCalcAmmoForOneShotWeapons() throws megamek.common.LocationFullException { + // Set aggression to the lowest level first + BehaviorSettings mockBehavior = mock(BehaviorSettings.class); + when(mockBehavior.getHyperAggressionIndex()).thenReturn(0); + when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior); + + // Set up unit + Mech mech1 = new BipedMech(); + Mounted wpn1 = mech1.addEquipment(mockRL20, Mech.LOC_LT); + + // Check default toHitThresholds + // For max aggro, shoot OS weapons at TN 10 or better + double target = Compute.oddsAbove(8) / 100.0; + Map conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // For default aggro, shoot OS weapons at TN 9 or better + when(mockBehavior.getHyperAggressionIndex()).thenReturn(5); + target = Compute.oddsAbove(9) / 100.0; + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + + // For lowest aggro, shoot OS weapons at TN 8 or better + when(mockBehavior.getHyperAggressionIndex()).thenReturn(10); + target = Compute.oddsAbove(10) / 100.0; + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } }