Skip to content

Commit

Permalink
Merge pull request #5415 from Sleet01/RFE_5408_adjust_ammo_conservati…
Browse files Browse the repository at this point in the history
…on_to_hit_thresholds

Implement RFE #5408: update Princess ammo conservation values to make…
  • Loading branch information
SJuliez authored Apr 25, 2024
2 parents d1a7cd6 + 8c0ecde commit 76dafba
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 14 deletions.
23 changes: 15 additions & 8 deletions megamek/src/megamek/client/bot/princess/Princess.java
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ protected void calculateTargetingOffBoardTurn() {
sendDone(true);
}

private Map<WeaponMounted, Double> calcAmmoConservation(final Entity shooter) {
protected Map<WeaponMounted, Double> calcAmmoConservation(final Entity shooter) {
final double aggroFactor = getBehaviorSettings().getHyperAggressionIndex();
final StringBuilder msg = new StringBuilder("\nCalculating ammo conservation for ")
.append(shooter.getDisplayName());
Expand Down Expand Up @@ -763,9 +763,15 @@ private Map<WeaponMounted, Double> 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;
Expand All @@ -776,13 +782,14 @@ private Map<WeaponMounted, Double> 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);
}
Expand Down
150 changes: 144 additions & 6 deletions megamek/unittests/megamek/client/bot/princess/PrincessTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<WeaponMounted, Double> 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<WeaponMounted, Double> 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<WeaponMounted, Double> 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<WeaponMounted, Double> 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);
}
}

0 comments on commit 76dafba

Please sign in to comment.