Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first crack at bot minefield avoidance #2898

Merged
merged 5 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion megamek/src/megamek/client/bot/princess/BasicPathRanker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
100 changes: 100 additions & 0 deletions megamek/src/megamek/client/bot/princess/MinefieldUtil.java
Original file line number Diff line number Diff line change
@@ -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())) {
sixlettervariables marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
6 changes: 5 additions & 1 deletion megamek/src/megamek/client/bot/princess/PathEnumerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand All @@ -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());
Expand Down
9 changes: 9 additions & 0 deletions megamek/src/megamek/common/BulldozerMovePath.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions megamek/src/megamek/common/EntityMovementMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions megamek/src/megamek/common/Minefield.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 12 additions & 0 deletions megamek/src/megamek/common/MovePath.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions megamek/src/megamek/common/pathfinder/LongestPathFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Comment on lines +129 to +132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ternary return candidate

}
}
}

/**
* Relaxer for longest path movement. Current implementation needs
Expand Down
2 changes: 2 additions & 0 deletions megamek/src/megamek/common/pathfinder/PathDecorator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,6 +156,7 @@ public static List<MovePath> 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());

Expand Down
2 changes: 1 addition & 1 deletion megamek/src/megamek/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MoveStep>());

final IBoard mockBoard = Mockito.mock(IBoard.class);
Mockito.when(mockBoard.contains(Mockito.any(Coords.class))).thenReturn(true);
Expand Down