From 9672e643d32edea02d68907f0f8df14b3913e372 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Wed, 24 Apr 2024 16:11:44 -0700 Subject: [PATCH 1/4] Implement RFE #5408: update Princess ammo conservation values to make Princess more aggressive Updated type of ammoConservation Map --- .../megamek/client/bot/princess/Princess.java | 23 ++- .../client/bot/princess/PrincessTest.java | 150 +++++++++++++++++- 2 files changed, 159 insertions(+), 14 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index fda0b728a08..c606ce79436 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, (40-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..f2e75fb377b 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + bin1.setShotsLeft(1); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } + + @Test + public void testCalcAmmoForOneShotWeapons() throws megamek.common.LocationFullException { + // Set aggression to lowest level + 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(10); + 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); + 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(8); + conserveMap = mockPrincess.calcAmmoConservation(mech1); + assertTrue(conserveMap.get(wpn1) <= target); + } } From 23868df33cfa1826d41936998a5984c6daff54f7 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Wed, 24 Apr 2024 18:45:43 -0700 Subject: [PATCH 2/4] Change Double (boxed type) to double (primitive) Missed a find/replace --- .../megamek/client/bot/princess/PrincessTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java index f2e75fb377b..5ffcc366372 100644 --- a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java +++ b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java @@ -559,7 +559,7 @@ public void testCalcAmmoForDefaultAggressionLevel() throws megamek.common.Locati // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - Double target = Compute.oddsAbove(12); + double target = Compute.oddsAbove(12); bin1.setShotsLeft(7); Map conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -594,7 +594,7 @@ public void testCalcAmmoForMaxAggressionLevel() throws megamek.common.LocationFu // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - Double target = Compute.oddsAbove(12); + double target = Compute.oddsAbove(12); bin1.setShotsLeft(7); Map conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -628,7 +628,7 @@ public void testCalcAmmoForZeroAggressionLevel() throws megamek.common.LocationF // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - Double target = Compute.oddsAbove(10); + double target = Compute.oddsAbove(10); bin1.setShotsLeft(7); Map conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -659,7 +659,7 @@ public void testCalcAmmoForOneShotWeapons() throws megamek.common.LocationFullEx // Check default toHitThresholds // For max aggro, shoot OS weapons at TN 10 or better - Double target = Compute.oddsAbove(10); + double target = Compute.oddsAbove(10); Map conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); From 672c07fd3b519829f1539f6571e25fe10fce07b9 Mon Sep 17 00:00:00 2001 From: sleet01 Date: Wed, 24 Apr 2024 20:00:30 -0700 Subject: [PATCH 3/4] Change test values to match ratio format rather than percentage Combine prior changes and new classes in unit tests --- .../megamek/client/bot/princess/Princess.java | 2 +- .../client/bot/princess/PrincessTest.java | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index c606ce79436..e60758f55a8 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -769,7 +769,7 @@ protected Map calcAmmoConservation(final Entity shooter) continue; } else if (weaponType.hasFlag(WeaponType.F_ONESHOT)) { // Shoot OS weapons on a 10 / 9 / 8 for Aggro 10 / 5 / 0 - ammoConservation.put(weapon, (40-aggroFactor)/100.0); + ammoConservation.put(weapon, (35 - 2.0 * aggroFactor) / 100.0); msg.append(" One Shot weapon."); continue; } diff --git a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java index 5ffcc366372..07c16c2395b 100644 --- a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java +++ b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java @@ -559,19 +559,19 @@ public void testCalcAmmoForDefaultAggressionLevel() throws megamek.common.Locati // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - double target = Compute.oddsAbove(12); + 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); + 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); + target = Compute.oddsAbove(10) / 100.0; bin1.setShotsLeft(1); conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -594,7 +594,7 @@ public void testCalcAmmoForMaxAggressionLevel() throws megamek.common.LocationFu // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - double target = Compute.oddsAbove(12); + double target = Compute.oddsAbove(12) / 100.0; bin1.setShotsLeft(7); Map conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -605,7 +605,7 @@ public void testCalcAmmoForMaxAggressionLevel() throws megamek.common.LocationFu assertTrue(conserveMap.get(wpn1) <= target); // Default toHitThreshold for 1 rounds for this level should allow firing on 10s - target = Compute.oddsAbove(10); + target = Compute.oddsAbove(10) / 100.0; bin1.setShotsLeft(1); conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -628,19 +628,19 @@ public void testCalcAmmoForZeroAggressionLevel() throws megamek.common.LocationF // Check default toHitThresholds // Default toHitThreshold for 7+ rounds for this level should allow firing on 12s - double target = Compute.oddsAbove(10); + 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); + 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); + target = Compute.oddsAbove(7) / 100.0; bin1.setShotsLeft(1); conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); @@ -659,19 +659,19 @@ public void testCalcAmmoForOneShotWeapons() throws megamek.common.LocationFullEx // Check default toHitThresholds // For max aggro, shoot OS weapons at TN 10 or better - double target = Compute.oddsAbove(10); + 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); + 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(8); + target = Compute.oddsAbove(10) / 100.0; conserveMap = mockPrincess.calcAmmoConservation(mech1); assertTrue(conserveMap.get(wpn1) <= target); } From 8c0ecde1c8163e709633e8784ecbd68e0ec324bf Mon Sep 17 00:00:00 2001 From: sleet01 Date: Wed, 24 Apr 2024 23:41:15 -0700 Subject: [PATCH 4/4] New commit to help force-push work --- megamek/unittests/megamek/client/bot/princess/PrincessTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java index 07c16c2395b..65eed94bf5c 100644 --- a/megamek/unittests/megamek/client/bot/princess/PrincessTest.java +++ b/megamek/unittests/megamek/client/bot/princess/PrincessTest.java @@ -648,7 +648,7 @@ public void testCalcAmmoForZeroAggressionLevel() throws megamek.common.LocationF @Test public void testCalcAmmoForOneShotWeapons() throws megamek.common.LocationFullException { - // Set aggression to lowest level + // Set aggression to the lowest level first BehaviorSettings mockBehavior = mock(BehaviorSettings.class); when(mockBehavior.getHyperAggressionIndex()).thenReturn(0); when(mockPrincess.getBehaviorSettings()).thenReturn(mockBehavior);