diff --git a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java index fef6b06b018..63ccacde6e6 100644 --- a/megamek/src/megamek/client/bot/princess/BasicPathRanker.java +++ b/megamek/src/megamek/client/bot/princess/BasicPathRanker.java @@ -540,6 +540,9 @@ protected RankedPath rankPath(MovePath path, IGame game, int maxRange, double expectedDamageTaken = checkPathForHazards(pathCopy, movingUnit, game); + + expectedDamageTaken += MinefieldUtil.checkPathForMinefieldHazards(pathCopy); + boolean extremeRange = game.getOptions() .booleanOption( OptionsConstants.ADVCOMBAT_TACOPS_RANGE); @@ -931,12 +934,13 @@ private double checkHexForHazards(IHex hex, Entity movingUnit, 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, IBoard board, diff --git a/megamek/src/megamek/client/bot/princess/MinefieldUtil.java b/megamek/src/megamek/client/bot/princess/MinefieldUtil.java new file mode 100644 index 00000000000..54b75d96153 --- /dev/null +++ b/megamek/src/megamek/client/bot/princess/MinefieldUtil.java @@ -0,0 +1,100 @@ +/* +* MegaMek - +* Copyright (C) 2021 The MegaMek Team +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License as published by the Free Software +* Foundation; either version 2 of the License, or (at your option) any later +* version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +*/ + +package megamek.client.bot.princess; + +import megamek.common.Compute; +import megamek.common.Entity; +import megamek.common.EntityMovementMode; +import megamek.common.Mech; +import megamek.common.Minefield; +import megamek.common.MovePath; +import megamek.common.MoveStep; +import megamek.common.annotations.Nullable; + +/** + * This class contains logic to evaluate the damage a unit could sustain from + * moving a long a move path containing minefields + * @author NickAragua + */ +public class MinefieldUtil { + /** + * Calculate how much damage we'll take from stepping on mines over a particular path + */ + public static double checkPathForMinefieldHazards(MovePath path) { + double hazardAccumulator = 0; + + for (MoveStep step : path.getStepVector()) { + hazardAccumulator += calcMinefieldHazardForHex(step, path.getEntity(), + path.isJumping(), step.equals(path.getLastStep())); + } + + //TODO: Teach bot to activate minesweepers + + return hazardAccumulator; + } + + /** + * Calculate how much damage we'll take from stepping on mines in a particular hex + */ + public static double calcMinefieldHazardForHex(@Nullable MoveStep step, Entity movingUnit, + boolean isJumping, boolean lastStep) { + // if we're not actually taking a step, no minefield hazard + if ((step == null) || !step.getType().entersNewHex()) { + return 0; + } + + // if our movement mode does not result in minefield detonation, no minefield hazard + if (!movingUnit.getMovementMode().detonatesGroundMinefields()) { + return 0; + } + + double hazardAccumulator = 0; + // hovercraft and WIGEs grinding along the ground detonate minefields on a 12 + boolean hoverMovement = (movingUnit.getMovementMode() == EntityMovementMode.HOVER) || + ((movingUnit.getMovementMode() == EntityMovementMode.WIGE) && (movingUnit.getElevation() == 0)); + double hoverMovementMultiplier = hoverMovement ? + Compute.oddsAbove(Minefield.HOVER_WIGE_DETONATION_TARGET) : 1; + + // only mechs interact with vibrabombs + boolean unitIsMech = movingUnit instanceof Mech; + + for (Minefield minefield : movingUnit.getGame().getMinefields(step.getPosition())) { + switch (minefield.getType()) { + case Minefield.TYPE_CONVENTIONAL: + case Minefield.TYPE_INFERNO: + // if we're either not jumping or it's the last step + if (!isJumping || lastStep) { + hazardAccumulator += minefield.getDensity() * hoverMovementMultiplier; + } + break; + case Minefield.TYPE_ACTIVE: + hazardAccumulator += minefield.getDensity() * hoverMovementMultiplier; + break; + case Minefield.TYPE_VIBRABOMB: + // mechs >10 tons the "setting" will set off vibrabombs before they + // get to them so we don't particularly care + if (unitIsMech && (!isJumping || lastStep) && + (movingUnit.getWeight() >= minefield.getSetting()) && + (movingUnit.getWeight() <= minefield.getSetting() + 10)) { + hazardAccumulator += minefield.getDensity(); + } + break; + } + } + + return hazardAccumulator; + } +} diff --git a/megamek/src/megamek/client/bot/princess/PathEnumerator.java b/megamek/src/megamek/client/bot/princess/PathEnumerator.java index cc7019986a2..9f186548bdd 100644 --- a/megamek/src/megamek/client/bot/princess/PathEnumerator.java +++ b/megamek/src/megamek/client/bot/princess/PathEnumerator.java @@ -42,6 +42,7 @@ import megamek.common.pathfinder.AbstractPathFinder.Filter; import megamek.common.pathfinder.AeroGroundPathFinder; import megamek.common.pathfinder.AeroGroundPathFinder.AeroGroundOffBoardFilter; +import megamek.common.pathfinder.LongestPathFinder.MovePathMinefieldAvoidanceMinMPMaxDistanceComparator; import megamek.common.util.BoardUtilities; import megamek.common.pathfinder.AeroLowAltitudePathFinder; import megamek.common.pathfinder.AeroSpacePathFinder; @@ -269,12 +270,14 @@ public boolean shouldStay(MovePath movePath) { LongestPathFinder lpf = LongestPathFinder .newInstanceOfLongestPath(mover.getRunMPwithoutMASC(), MoveStepType.FORWARDS, getGame()); + lpf.setComparator(new MovePathMinefieldAvoidanceMinMPMaxDistanceComparator()); lpf.run(new MovePath(game, mover)); paths.addAll(lpf.getLongestComputedPaths()); //add walking moves lpf = LongestPathFinder.newInstanceOfLongestPath( mover.getWalkMP(), MoveStepType.BACKWARDS, getGame()); + lpf.setComparator(new MovePathMinefieldAvoidanceMinMPMaxDistanceComparator()); lpf.run(new MovePath(getGame(), mover)); paths.addAll(lpf.getLongestComputedPaths()); @@ -285,9 +288,10 @@ public boolean shouldStay(MovePath movePath) { //add jumping moves if (mover.getJumpMP() > 0) { - ShortestPathFinder spf = ShortestPathFinder + ShortestPathFinder spf = ShortestPathFinder .newInstanceOfOneToAll(mover.getJumpMP(), MoveStepType.FORWARDS, getGame()); + spf.setComparator(new MovePathMinefieldAvoidanceMinMPMaxDistanceComparator()); spf.run((new MovePath(game, mover)) .addStep(MoveStepType.START_JUMP)); paths.addAll(spf.getAllComputedPathsUncategorized()); diff --git a/megamek/src/megamek/common/BulldozerMovePath.java b/megamek/src/megamek/common/BulldozerMovePath.java index ab40f146f33..0675284a0ad 100644 --- a/megamek/src/megamek/common/BulldozerMovePath.java +++ b/megamek/src/megamek/common/BulldozerMovePath.java @@ -22,6 +22,7 @@ import java.util.Map; import megamek.client.bot.princess.FireControl; +import megamek.client.bot.princess.MinefieldUtil; import megamek.common.pathfinder.BoardClusterTracker.MovementType; /** @@ -117,6 +118,14 @@ public MovePath addStep(final MoveStepType type) { } } } + + // we want to discourage running over minefields + double minefieldFactor = MinefieldUtil.calcMinefieldHazardForHex(mp.getLastStep(), mp.getEntity(), + mp.isJumping(), false); + + if (minefieldFactor > 0) { + additionalCosts.put(mp.getFinalCoords(), (int) Math.ceil(minefieldFactor)); + } return mp; } diff --git a/megamek/src/megamek/common/EntityMovementMode.java b/megamek/src/megamek/common/EntityMovementMode.java index 32bf4598fcb..6137882dbf1 100644 --- a/megamek/src/megamek/common/EntityMovementMode.java +++ b/megamek/src/megamek/common/EntityMovementMode.java @@ -77,4 +77,21 @@ public static String token(EntityMovementMode t) { return t.name(); } + + /** + * Whether this movement mode is capable of detonating minefields. + */ + public boolean detonatesGroundMinefields() { + return (this == BIPED) || + (this == TRIPOD) || + (this == QUAD) || + (this == TRACKED) || + (this == WHEELED) || + (this == HOVER) || // a lot less likely, but... + (this == INF_LEG) || + (this == INF_MOTORIZED) || + (this == INF_JUMP) || + (this == RAIL) || + (this == MAGLEV); + } } diff --git a/megamek/src/megamek/common/Minefield.java b/megamek/src/megamek/common/Minefield.java index 363e6c5eaa8..5ac7c2bcd61 100644 --- a/megamek/src/megamek/common/Minefield.java +++ b/megamek/src/megamek/common/Minefield.java @@ -44,6 +44,8 @@ public class Minefield implements Serializable, Cloneable { public static final int TO_HIT_SIDE = ToHitData.SIDE_FRONT; public static final int TO_HIT_TABLE = ToHitData.HIT_KICK; + + public static final int HOVER_WIGE_DETONATION_TARGET = 12; public static final int MAX_DAMAGE = 30; diff --git a/megamek/src/megamek/common/MovePath.java b/megamek/src/megamek/common/MovePath.java index 9cafde78eb9..720e1782ca2 100644 --- a/megamek/src/megamek/common/MovePath.java +++ b/megamek/src/megamek/common/MovePath.java @@ -70,6 +70,18 @@ public enum MoveStepType { SHUTDOWN, STARTUP, SELF_DESTRUCT, ACCN, DECN, ROLL, OFF, RETURN, LAUNCH, THRUST, YAW, CRASH, RECOVER, RAM, HOVER, MANEUVER, LOOP, CAREFUL_STAND, JOIN, DROP, VLAND, MOUNT, UNDOCK, TAKE_COVER, CONVERT_MODE, BOOTLEGGER, TOW, DISCONNECT; + + /** + * Whether this move step type will result in the unit entering a new hex + */ + public boolean entersNewHex() { + return this == FORWARDS || + this == BACKWARDS || + this == LATERAL_LEFT || + this == LATERAL_RIGHT || + this == LATERAL_LEFT_BACKWARDS || + this == LATERAL_RIGHT_BACKWARDS; + } } public static class Key { diff --git a/megamek/src/megamek/common/pathfinder/LongestPathFinder.java b/megamek/src/megamek/common/pathfinder/LongestPathFinder.java index 5919fedf909..23568e472a7 100644 --- a/megamek/src/megamek/common/pathfinder/LongestPathFinder.java +++ b/megamek/src/megamek/common/pathfinder/LongestPathFinder.java @@ -23,6 +23,7 @@ import java.util.Deque; import java.util.List; +import megamek.client.bot.princess.MinefieldUtil; import megamek.common.Coords; import megamek.common.IGame; import megamek.common.Infantry; @@ -108,6 +109,30 @@ public int compare(MovePath first, MovePath second) { } } } + + /** + * Comparator that sorts MovePaths based on, in order, the following criteria: + * Minefield hazard (stepping on less mines is better) + * Least MP used + * Most distance moved + */ + public static class MovePathMinefieldAvoidanceMinMPMaxDistanceComparator extends MovePathMinMPMaxDistanceComparator { + @Override + public int compare(MovePath first, MovePath second) { + Double firstMinefieldScore = MinefieldUtil.calcMinefieldHazardForHex(first.getLastStep(), + first.getEntity(), first.isJumping(), false); + Double secondMinefieldScore = MinefieldUtil.calcMinefieldHazardForHex(second.getLastStep(), + second.getEntity(), second.isJumping(), false); + + int s = secondMinefieldScore.compareTo(firstMinefieldScore); + + if (s == 0) { + return super.compare(first, second); + } else { + return s; + } + } + } /** * Relaxer for longest path movement. Current implementation needs diff --git a/megamek/src/megamek/common/pathfinder/PathDecorator.java b/megamek/src/megamek/common/pathfinder/PathDecorator.java index 0d64b9bcbdd..7540bfd89a4 100644 --- a/megamek/src/megamek/common/pathfinder/PathDecorator.java +++ b/megamek/src/megamek/common/pathfinder/PathDecorator.java @@ -27,6 +27,7 @@ import megamek.common.MovePath; import megamek.common.Terrains; import megamek.common.MovePath.MoveStepType; +import megamek.common.pathfinder.LongestPathFinder.MovePathMinefieldAvoidanceMinMPMaxDistanceComparator; /** * This class contains functionality that takes a given path @@ -155,6 +156,7 @@ public static List generatePossiblePaths(MovePath source, int desiredM LongestPathFinder lpf = LongestPathFinder .newInstanceOfLongestPath(desiredMP, MoveStepType.FORWARDS, source.getGame()); + lpf.setComparator(new MovePathMinefieldAvoidanceMinMPMaxDistanceComparator()); lpf.run(source); turnPaths.addAll(lpf.getLongestComputedPaths()); diff --git a/megamek/src/megamek/server/Server.java b/megamek/src/megamek/server/Server.java index 3987d017074..d5d96b56625 100644 --- a/megamek/src/megamek/server/Server.java +++ b/megamek/src/megamek/server/Server.java @@ -11135,7 +11135,7 @@ private boolean enterMinefield(Entity entity, Coords c, int curElev, boolean isO } if ((entity.getMovementMode() == EntityMovementMode.HOVER) || (entity.getMovementMode() == EntityMovementMode.WIGE)) { - target = 12; + target = Minefield.HOVER_WIGE_DETONATION_TARGET; } } diff --git a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java index 0c4784376c5..76ab14bc5fd 100644 --- a/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java +++ b/megamek/unittests/megamek/client/bot/princess/BasicPathRankerTest.java @@ -406,6 +406,7 @@ public void testRankPath() { Mockito.when(mockPath.toString()).thenReturn("F F F"); Mockito.when(mockPath.clone()).thenReturn(mockPath); Mockito.when(mockPath.getLastStep()).thenReturn(mockLastStep); + Mockito.when(mockPath.getStepVector()).thenReturn(new Vector()); final IBoard mockBoard = Mockito.mock(IBoard.class); Mockito.when(mockBoard.contains(Mockito.any(Coords.class))).thenReturn(true);