From 533d45e91c42ee508da6b9694c083e5d93d1ce9d Mon Sep 17 00:00:00 2001 From: Thom293 Date: Thu, 11 Jan 2024 11:11:42 -0600 Subject: [PATCH] Ice & Infantry On Ice Fix: This PR gives Princess some Cryophobia in her movement planning. Currently, On a map with approximately 50/50 Ice/Land, she would jump and walk mechs onto the ice every turn, and lose many before she ever got to combat. With this change she jumped mechs into the ice Zero times in 10 games. And she will still walk on the ice when forced because she is landlocked. Otherwise she will take a land path around. Also fixes an unreported bug related to Infantry and Ice. --- .../client/bot/princess/BasicPathRanker.java | 139 ++++++++++-------- .../bot/princess/BasicPathRankerTest.java | 8 +- 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index 9920f39ec82..5c45b500dc9 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -33,11 +33,11 @@ public class BasicPathRanker extends PathRanker implements IPathRanker { // this is a value used to indicate how much we value the unit being at its destination private final int ARRIVED_AT_DESTINATION_FACTOR = 250; - + // this is a value used to indicate how much we dis-value the unit being destroyed as a result of // what it's doing private final int UNIT_DESTRUCTION_FACTOR = 1000; - + protected final DecimalFormat LOG_DECIMAL = new DecimalFormat("0.00", DecimalFormatSymbols.getInstance()); private final NumberFormat LOG_INT = NumberFormat.getIntegerInstance(); @@ -45,19 +45,19 @@ public class BasicPathRanker extends PathRanker implements IPathRanker { private PathEnumerator pathEnumerator; - // the best damage enemies could expect were I not here. Used to determine + // the best damage enemies could expect were I not here. Used to determine // whether they will target me. private Map bestDamageByEnemies; - + public BasicPathRanker(Princess owningPrincess) { super(owningPrincess); - + bestDamageByEnemies = new TreeMap<>(); - + LogManager.getLogger().debug("Using " + getOwner().getBehaviorSettings().getDescription() + " behavior"); } - + FireControl getFireControl(Entity entity) { return getOwner().getFireControl(entity); } @@ -65,7 +65,7 @@ FireControl getFireControl(Entity entity) { void setPathEnumerator(PathEnumerator pathEnumerator) { this.pathEnumerator = pathEnumerator; } - + PathEnumerator getPathEnumerator() { return pathEnumerator; } @@ -125,13 +125,13 @@ EntityEvaluationResponse evaluateUnmovedEnemy(Entity enemy, MovePath path, EntityEvaluationResponse returnResponse = new EntityEvaluationResponse(); - //Airborne aeros on ground maps always move after other units, and would require an + //Airborne aeros on ground maps always move after other units, and would require an // entirely different evaluation //TODO (low priority) implement a way to see if I can dodge aero units if (enemy.isAirborneAeroOnGroundMap()) { return returnResponse; } - + Coords finalCoords = path.getFinalCoords(); int myFacing = path.getFinalFacing(); Coords behind = finalCoords.translated((myFacing + 3) % 6); @@ -143,9 +143,9 @@ EntityEvaluationResponse evaluateUnmovedEnemy(Entity enemy, MovePath path, } int range = closest.distance(finalCoords); - // I would prefer if the enemy must end its move in my line of fire - // if so, I can guess that I may do some damage to it (cover - // notwithstanding). At the very least, I can force the enemy to + // I would prefer if the enemy must end its move in my line of fire + // if so, I can guess that I may do some damage to it (cover + // notwithstanding). At the very least, I can force the enemy to // take cover on its move. HexLine leftBounds; HexLine rightBounds; @@ -181,7 +181,7 @@ EntityEvaluationResponse evaluateUnmovedEnemy(Entity enemy, MovePath path, Math.ceil(enemy.getWeight() / 5.0) * damageDiscount); } - + return returnResponse; } @@ -227,13 +227,13 @@ private double calculateFallMod(double successProbability, if (!losEffects.canSee()) { return 0; } - + Targetable actualTarget = path.getEntity(); - - // if the target is infantry protected by a building, we have to fire at the building instead. + + // if the target is infantry protected by a building, we have to fire at the building instead. if (losEffects.infantryProtected()) { actualTarget = new BuildingTarget(targetState.getPosition(), game.getBoard(), false); - targetState = new EntityState(actualTarget); + targetState = new EntityState(actualTarget); } int maxHeat = (enemy.getHeatCapacity() - enemy.heat) + (enemy.isAero() ? 0 : 5); @@ -284,7 +284,7 @@ private double calculateFallMod(double successProbability, return 0; } - // If I am an infantry unit that cannot both move and fire, and I am + // If I am an infantry unit that cannot both move and fire, and I am // moving, I can't do damage. boolean isZeroMpInfantry = me instanceof Infantry && (me.getWalkMP() == 0); if (isZeroMpInfantry && path.getMpUsed() > 0) { @@ -328,7 +328,7 @@ EntityEvaluationResponse evaluateMovedEnemy(Entity enemy, MovePath path, Game ga EntityEvaluationResponse returnResponse = new EntityEvaluationResponse(); int distance = enemy.getPosition().distance(path.getFinalCoords()); - + // How much damage can they do to me? double theirDamagePotential = calculateDamagePotential(enemy, new EntityState(enemy), path, new EntityState(path), distance, game); @@ -341,7 +341,7 @@ EntityEvaluationResponse evaluateMovedEnemy(Entity enemy, MovePath path, Game ga // How much damage can I do to them? returnResponse.setMyEstimatedDamage(calculateMyDamagePotential(path, enemy, distance, game)); - + // How much physical damage can I do to them? if (distance <= 1) { returnResponse.setMyEstimatedPhysicalDamage(calculateMyKickDamagePotential(path, enemy, game)); @@ -350,7 +350,7 @@ EntityEvaluationResponse evaluateMovedEnemy(Entity enemy, MovePath path, Game ga return returnResponse; } - // The further I am from a target, the lower this path ranks (weighted by + // The further I am from a target, the lower this path ranks (weighted by // Hyper Aggression. protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game game, StringBuilder formula) { @@ -367,7 +367,7 @@ protected double calculateAggressionMod(Entity movingUnit, MovePath path, Game g return aggressionMod; } - // The further I am from my teammates, the lower this path ranks (weighted + // The further I am from my teammates, the lower this path ranks (weighted // by Herd Mentality). protected double calculateHerdingMod(Coords friendsCoords, MovePath path, StringBuilder formula) { if (friendsCoords == null) { @@ -425,8 +425,8 @@ protected double calculateSelfPreservationMod(Entity movingUnit, MovePath path, Game game, StringBuilder formula) { - BehaviorType behaviorType = getOwner().getUnitBehaviorTracker().getBehaviorType(movingUnit, getOwner()); - + BehaviorType behaviorType = getOwner().getUnitBehaviorTracker().getBehaviorType(movingUnit, getOwner()); + if (behaviorType == BehaviorType.ForcedWithdrawal || behaviorType == BehaviorType.MoveToDestination) { int newDistanceToHome = distanceToHomeEdge(path.getFinalCoords(), @@ -434,17 +434,17 @@ protected double calculateSelfPreservationMod(Entity movingUnit, game); double selfPreservation = getOwner().getBehaviorSettings() .getSelfPreservationValue(); - + double selfPreservationMod = 0; - + // normally, we favor being closer to the edge we're trying to get to if (newDistanceToHome > 0) { selfPreservationMod = newDistanceToHome * selfPreservation; - // if this path gets us to the edge, we value it considerably more than we do paths that don't get us there + // if this path gets us to the edge, we value it considerably more than we do paths that don't get us there } else { selfPreservationMod = -ARRIVED_AT_DESTINATION_FACTOR; } - + formula.append(" - selfPreservationMod [") .append(LOG_DECIMAL.format(selfPreservationMod)) .append(" = ").append(LOG_DECIMAL.format(newDistanceToHome)) @@ -454,11 +454,11 @@ protected double calculateSelfPreservationMod(Entity movingUnit, } return 0.0; } - + /** * Tells me whether this path will result in me flying to a location * from which there is absolutely no way to remain on the board the following turn. - * + * * Not applicable for ground units, so the default behavior is to return 0. */ protected double calculateOffBoardMod(MovePath path) { @@ -481,13 +481,13 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal double successProbability = getMovePathSuccessProbability(pathCopy, formula); double utility = -calculateFallMod(successProbability, formula); - // look at all of my enemies + // look at all of my enemies FiringPhysicalDamage damageEstimate = new FiringPhysicalDamage(); - + double expectedDamageTaken = checkPathForHazards(pathCopy, movingUnit, game); - + expectedDamageTaken += MinefieldUtil.checkPathForMinefieldHazards(pathCopy); - + boolean extremeRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE); boolean losRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_LOS_RANGE); for (Entity enemy : enemies) { @@ -517,7 +517,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal // For units that have not moved this round eval = evaluateUnmovedEnemy(enemy, path, extremeRange, losRange); } - + // if we're not ignoring the enemy, we consider damage that we may do to them; // however, just because we're ignoring them doesn't mean they won't shoot at us. if (!getOwner().getBehaviorSettings().getIgnoredUnitTargets().contains(enemy.getId())) { @@ -528,7 +528,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal damageEstimate.physicalDamage = eval.getMyEstimatedPhysicalDamage(); } } - + expectedDamageTaken += eval.getEstimatedEnemyDamage(); } @@ -536,14 +536,14 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal if (!path.getEntity().isAirborne() && !path.getEntity().isAirborneVTOLorWIGE()) { double friendlyArtilleryDamage = 0; Map artyDamage = getOwner().getPathRankerState().getIncomingFriendlyArtilleryDamage(); - + if (!artyDamage.containsKey(path.getFinalCoords())) { friendlyArtilleryDamage = ArtilleryTargetingControl.evaluateIncomingArtilleryDamage(path.getFinalCoords(), getOwner()); artyDamage.put(path.getFinalCoords(), friendlyArtilleryDamage); } else { friendlyArtilleryDamage = artyDamage.get(path.getFinalCoords()); } - + expectedDamageTaken += friendlyArtilleryDamage; } @@ -556,12 +556,12 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal damageEstimate.physicalDamage = 0; } - // I can kick a different target than I shoot, so add physical to + // I can kick a different target than I shoot, so add physical to // total damage after I've looked at all enemies double maximumDamageDone = damageEstimate.firingDamage + damageEstimate.physicalDamage; - // My bravery modifier is based on my chance of getting to the - // firing position (successProbability), how much damage I can do + // My bravery modifier is based on my chance of getting to the + // firing position (successProbability), how much damage I can do // (weighted by bravery), less the damage I might take. double braveryValue = getOwner().getBehaviorSettings().getBraveryValue(); double braveryMod = successProbability * ((maximumDamageDone * braveryValue) - expectedDamageTaken); @@ -577,11 +577,11 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal // the only critters not subject to aggression and herding mods are // airborne aeros on ground maps, as they move incredibly fast if (!path.getEntity().isAirborneAeroOnGroundMap()) { - // The further I am from a target, the lower this path ranks + // The further I am from a target, the lower this path ranks // (weighted by Aggression slider). utility -= calculateAggressionMod(movingUnit, pathCopy, game, formula); - // The further I am from my teammates, the lower this path + // The further I am from my teammates, the lower this path // ranks (weighted by Herd Mentality). utility -= calculateHerdingMod(friendsCoords, pathCopy, formula); } @@ -592,10 +592,10 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal return new RankedPath(facingMod, pathCopy, formula.toString()); } utility -= facingMod; - + // If I need to flee the board, I want to get closer to my home edge. utility -= calculateSelfPreservationMod(movingUnit, pathCopy, game, formula); - + // if we're an aircraft, we want to de-value paths that will force us off the board // on the subsequent turn. utility -= utility * calculateOffBoardMod(pathCopy); @@ -604,7 +604,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal rankedPath.setExpectedDamage(maximumDamageDone); return rankedPath; } - + /** * Worker function that determines if a given enemy entity should be evaluated as if it has moved. */ @@ -613,7 +613,7 @@ protected boolean evaluateAsMoved(Entity enemy) { // somewhat pointless to try to predict their movement. return !enemy.isSelectableThisTurn() || enemy.isImmobile() || enemy.isAirborneAeroOnGroundMap(); } - + /** * Calculate who all other units would shoot at if I weren't around */ @@ -645,15 +645,15 @@ public void initUnitTurn(Entity unit, Game game) { protected void calcDamageToStrategicTargets(MovePath path, Game game, FireControlState fireControlState, FiringPhysicalDamage damageStructure) { - + for (int i = 0; i < fireControlState.getAdditionalTargets().size(); i++) { Targetable target = fireControlState.getAdditionalTargets().get(i); - + if (target.isOffBoard() || (target.getPosition() == null) || !game.getBoard().contains(target.getPosition())) { continue; // Skip targets not actually on the board. } - + FiringPlanCalculationParameters guess = new FiringPlanCalculationParameters.Builder() .buildGuess(path.getEntity(), @@ -663,12 +663,12 @@ protected void calcDamageToStrategicTargets(MovePath path, Game game, Entity.DOES_NOT_TRACK_HEAT, null); FiringPlan myFiringPlan = getFireControl(path.getEntity()).determineBestFiringPlan(guess); - + double myDamagePotential = myFiringPlan.getUtility(); if (myDamagePotential > damageStructure.firingDamage) { damageStructure.firingDamage = myDamagePotential; } - + if (path.getEntity() instanceof Mech) { PhysicalInfo myKick = new PhysicalInfo( path.getEntity(), new EntityState(path), target, @@ -684,7 +684,7 @@ PhysicalAttackType.RIGHT_KICK, game, getOwner(), } } } - + /** * Gives the distance to the closest enemy unit, or -1 if none exist. * The reason being that the closest enemy unit may be 0 away. @@ -829,13 +829,13 @@ private double checkHexForHazards(Hex hex, Entity movingUnit, boolean endHex, Mo break; } } - + logMsg.append("\n\tTotal Hazard = ") .append(LOG_DECIMAL.format(hazardValue)); return hazardValue; } - + // Building collapse and basements are handled in PathRanker.validatePaths. private double calcBuildingHazard(MoveStep step, Entity movingUnit, boolean jumpLanding, Board board, StringBuilder logMsg) { @@ -869,11 +869,11 @@ private double calcBuildingHazard(MoveStep step, Entity movingUnit, boolean jump .append(LOG_DECIMAL.format(hazard)).append(")."); return hazard; } - + private double calcBridgeHazard(Entity movingUnit, Hex hex, MoveStep step, boolean jumpLanding, Board board, StringBuilder logMsg) { logMsg.append("\n\tCalculating bridge hazard: "); - + // if we are going to BWONGGG into a bridge from below, then it's treated as a building. // Otherwise, bridge collapse checks have already been handled in validatePaths int bridgeElevation = hex.terrainLevel(Terrains.BRIDGE_ELEV); @@ -881,7 +881,7 @@ private double calcBridgeHazard(Entity movingUnit, Hex hex, MoveStep step, boole (bridgeElevation <= (step.getElevation() + movingUnit.getHeight()))) { return calcBuildingHazard(step, movingUnit, jumpLanding, board, logMsg); } - + return 0; } @@ -896,7 +896,16 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath return 0; } - // If there is no water under the ice, don't worry about breaking + // Infantry don't break ice. + if (EntityMovementMode.INF_LEG == movingUnit.getMovementMode() || + EntityMovementMode.INF_MOTORIZED == movingUnit.getMovementMode() || + EntityMovementMode.INF_JUMP == movingUnit.getMovementMode() || + EntityMovementMode.INF_UMU == movingUnit.getMovementMode()) { + logMsg.append("Infantry on Ice (0)."); + return 0; + } + + // If there is no water under the ice, don't worry about breaking // through. if (hex.depth() < 1) { logMsg.append("No water under ice (0)."); @@ -912,7 +921,9 @@ private double calcIceHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath breakthroughMod; logMsg.append("\n\t\tHazard value (") .append(LOG_DECIMAL.format(hazard)).append(")."); - return hazard; + // Changed this to UNIT_DESTRUCTION_FACTOR because she suicided too often. + // No reason to be on the ice at all except as an absolute last resort. + return UNIT_DESTRUCTION_FACTOR; } private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePath movePath, @@ -950,7 +961,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa return 0; } } - + // Most other units are automatically destroyed. if (!(movingUnit instanceof Mech || movingUnit instanceof Protomech || movingUnit instanceof BattleArmor)) { @@ -1008,7 +1019,7 @@ private double calcWaterHazard(Entity movingUnit, Hex hex, MoveStep step, MovePa continue; } - // Mechs or Protomechs having a head or torso breach is deadly. + // Mechs or Protomechs having a head or torso breach is deadly. // For other units, any breach is deadly. //noinspection ConstantConditions if (Mech.LOC_HEAD == loc || @@ -1158,7 +1169,7 @@ private double calcLavaHazard(boolean endHex, Entity movingUnit, return hazardValue; } - + /** * Simple data structure that holds a separate firing and physical damage number. * diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index 5c55fa8fe6b..a5151269f8a 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -1248,7 +1248,7 @@ public void testCheckPathForHazards() { when(mockPath.isJumping()).thenReturn(false); when(mockHexThree.getTerrainTypes()).thenReturn(new int[]{Terrains.ICE, Terrains.WATER}); when(mockHexThree.depth()).thenReturn(1); - assertEquals(166.7, testRanker.checkPathForHazards(mockPath, mockInfantry, mockGame), TOLERANCE); + assertEquals(1000, testRanker.checkPathForHazards(mockPath, mockInfantry, mockGame), TOLERANCE); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); when(mockHexThree.depth()).thenReturn(0); @@ -1280,10 +1280,10 @@ public void testCheckPathForHazards() { when(mockHexThree.depth()).thenReturn(1); when(mockFinalHex.depth()).thenReturn(2); when(mockUnit.getArmor(Mech.LOC_CT)).thenReturn(0); - assertEquals(166.7, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2000, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockUnit.getArmor(Mech.LOC_CT)).thenReturn(10); when(mockUnit.getArmor(Mech.LOC_RARM)).thenReturn(0); - assertEquals(8.334, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(2000, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockUnit.getArmor(Mech.LOC_RARM)).thenReturn(10); when(mockHexTwo.getTerrainTypes()).thenReturn(new int[0]); when(mockHexThree.getTerrainTypes()).thenReturn(new int[0]); @@ -1335,7 +1335,7 @@ public void testCheckPathForHazards() { when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(2); when(mockFinalHex.depth()).thenReturn(2); when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(0); - assertEquals(25.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); + assertEquals(1000.0, testRanker.checkPathForHazards(mockPath, mockUnit, mockGame), TOLERANCE); when(mockUnit.getArmor(eq(Mech.LOC_LLEG))).thenReturn(10); when(mockFinalHex.terrainLevel(Terrains.WATER)).thenReturn(0); when(mockFinalHex.depth()).thenReturn(0);