Skip to content

Commit

Permalink
Merge pull request #6311 from Sleet01/RFE_6295_teach_Princess_that_Le…
Browse files Browse the repository at this point in the history
…aping_is_dangerous

Rfe 6295: teach princess that leaping is dangerous
  • Loading branch information
HoneySkull authored Dec 25, 2024
2 parents 6bb37ff + b8a0d70 commit 9106f42
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 19 deletions.
6 changes: 5 additions & 1 deletion megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4823,4 +4823,8 @@ Bot.commands.herding=Herding
Bot.commands.aggression=Aggression
Bot.commands.bravery=Bravery
Bot.commands.avoid=Self-Preservation
Bot.commands.caution=Piloting Caution
Bot.commands.caution=Piloting Caution

#### TacOps movement and damage descriptions
TacOps.leaping.leg_damage=leaping (leg damage)
TacOps.leaping.fall_damage=leaping (fall)
11 changes: 6 additions & 5 deletions megamek/src/megamek/client/bot/princess/BasicPathRanker.java
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,14 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
double successProbability = getMovePathSuccessProbability(pathCopy, formula);
double utility = -calculateFallMod(successProbability, formula);

// Worry about how badly we can damage ourselves on this path!
double expectedDamageTaken = calculateMovePathPSRDamage(movingUnit, pathCopy, formula);
expectedDamageTaken += checkPathForHazards(pathCopy, movingUnit, game);
expectedDamageTaken += MinefieldUtil.checkPathForMinefieldHazards(pathCopy);

// 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) {
Expand Down Expand Up @@ -589,7 +590,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
// 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);
double braveryMod = (successProbability * (maximumDamageDone * braveryValue)) - expectedDamageTaken;
formula.append(" + braveryMod [")
.append(LOG_DECIMAL.format(braveryMod)).append(" = ")
.append(LOG_PERCENT.format(successProbability))
Expand Down
36 changes: 36 additions & 0 deletions megamek/src/megamek/client/bot/princess/PathEnumerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,40 @@ private boolean worryAboutBridges() {

return mapHasBridges.get();
}

/**
* Find paths with a similar direction and step count to the provided path, within the selected unit's
* already-computed unit paths.
* @param moverId
* @param prunedPath
* @return
*/
protected List<MovePath> getSimilarUnitPaths(int moverId, BulldozerMovePath prunedPath) {
int mpDelta = 2;
int distanceDelta = 2;

List<MovePath> paths = new ArrayList<>();
if (!getUnitPaths().containsKey(moverId)) {
return paths;
}
List<MovePath> unitPaths = getUnitPaths().get(moverId);

Coords target = prunedPath.getDestination();
int prunedDistance = target.distance(prunedPath.getFinalCoords());

for (MovePath movePath: unitPaths) {
// We want unit paths that use similar amounts of MP to get similarly close to the BMP's destination
if (Math.abs((target.distance(movePath.getFinalCoords()) - prunedDistance)) > distanceDelta) {
continue;
}

if (Math.abs(movePath.getMpUsed() - prunedPath.getMpUsed()) > mpDelta ) {
continue;
}

paths.add(movePath);
}

return paths;
}
}
36 changes: 36 additions & 0 deletions megamek/src/megamek/client/bot/princess/PathRanker.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Enumeration;
import java.util.List;

import megamek.client.ui.Messages;
import org.apache.logging.log4j.Level;

import megamek.client.bot.princess.UnitBehavior.BehaviorType;
Expand All @@ -38,6 +39,9 @@
import megamek.common.options.OptionsConstants;
import megamek.logging.MMLogger;

import static megamek.client.ui.SharedUtility.predictLeapDamage;
import static megamek.client.ui.SharedUtility.predictLeapFallDamage;

public abstract class PathRanker implements IPathRanker {
private final static MMLogger logger = MMLogger.create(PathRanker.class);

Expand Down Expand Up @@ -372,6 +376,38 @@ protected double getMovePathSuccessProbability(MovePath movePath, StringBuilder
return successProbability;
}

/**
* Estimates the most expected damage that a path could cause, given the pilot skill of the path ranker
* and various conditions.
*
* XXX Sleet01: add fall pilot damage, skid damage, and low-gravity overspeed damage calcs
*
* @param movingEntity
* @param path
* @param msg
* @return
*/
protected double calculateMovePathPSRDamage(Entity movingEntity, MovePath path, StringBuilder msg) {
double damage = 0.0;

List<TargetRoll> pilotingRolls = getPSRList(path);
for (TargetRoll roll : pilotingRolls) {
// Have to use if/else as switch/case won't allow runtime loading of strings without SDK 17 LTS support
String description = roll.getLastPlainDesc().toLowerCase();
if (
description.contains(Messages.getString("TacOps.leaping.leg_damage"))
) {
damage += predictLeapDamage(movingEntity, roll, msg);
} else if (
description.contains(Messages.getString("TacOps.leaping.fall_damage"))
) {
damage += predictLeapFallDamage(movingEntity, roll, msg);
}
}

return damage;
}

protected List<TargetRoll> getPSRList(MovePath path) {
return SharedUtility.getPSRList(path);
}
Expand Down
2 changes: 2 additions & 0 deletions megamek/src/megamek/client/bot/princess/Princess.java
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,8 @@ public List<MovePath> getMovePathsAndSetNecessaryTargets(Entity mover, boolean f
// also return some paths that go a little slower than max speed
// in case the faster path would force an unwanted PSR or MASC check
prunedPaths.addAll(PathDecorator.decoratePath(prunedPath));
// Return some of the already-computed unit paths as well.
prunedPaths.addAll(getPrecognition().getPathEnumerator().getSimilarUnitPaths(mover.getId(), prunedPath));
}
return prunedPaths;
}
Expand Down
74 changes: 71 additions & 3 deletions megamek/src/megamek/client/ui/SharedUtility.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
import megamek.common.options.OptionsConstants;
import megamek.server.totalwarfare.TWGameManager;

import static java.util.List.of;

public class SharedUtility {

private static int CRIT_VALUE = 100;

public static String doPSRCheck(MovePath md) {
return (String) doPSRCheck(md, true);
}
Expand All @@ -46,7 +50,7 @@ public static List<TargetRoll> getPSRList(MovePath md) {

/**
* Function that carries out PSR checks specific only to airborne aero units
*
*
* @param md The path to check
* @param stringResult Whether to return the report as a string
* @return Collection of PSRs that will be required for this activity
Expand Down Expand Up @@ -256,11 +260,22 @@ private static Object doPSRCheck(MovePath md, boolean stringResult) {
if (leapDistance > 2) {
rollTarget = entity.getBasePilotingRoll(moveType);
entity.addPilotingModifierForTerrain(rollTarget, curPos);
rollTarget.append(new PilotingRollData(entity.getId(), 2 * leapDistance, "leaping (leg damage)"));
rollTarget.append(
new PilotingRollData(
entity.getId(),
2 * leapDistance,
Messages.getString("TacOps.leaping.leg_damage")
)
);
SharedUtility.checkNag(rollTarget, nagReport, psrList);
rollTarget = entity.getBasePilotingRoll(moveType);
entity.addPilotingModifierForTerrain(rollTarget, curPos);
rollTarget.append(new PilotingRollData(entity.getId(), leapDistance, "leaping (fall)"));
rollTarget.append(
new PilotingRollData(
entity.getId(),
leapDistance,
Messages.getString("TacOps.leaping.fall_damage"))
);
SharedUtility.checkNag(rollTarget, nagReport, psrList);
}
}
Expand Down Expand Up @@ -928,4 +943,57 @@ public static String[] getDisplayArray(List<? extends Targetable> entities) {
return targets.stream().filter(t -> chosenDisplayName.equals(t.getDisplayName())).findAny().orElse(null);
}
}

public static double predictLeapFallDamage(Entity movingEntity, TargetRoll data, StringBuilder msg) {
// Rough guess based on normal pilots
double odds = Compute.oddsAbove(data.getValue(), false) / 100d;
int fallHeight = data.getModifiers().get(data.getModifiers().size()-1).getValue();
double fallDamage = Math.round(movingEntity.getWeight() / 10.0)
* (fallHeight + 1);
msg.append("\nPredicting expected Leap fall damage:")
.append(String.format("\n\tFall height: %d", fallHeight))
.append(String.format("\n\tChance of taking damage: %.2f", ((1-odds)*100d))).append('%')
.append(String.format("\n\tExpected total damage from fall: %.2f", fallDamage * (1 - odds) ));
return fallDamage * (1 - odds);
}

/** Per TacOps p 20, a leap carries the following risks:
* 1. risk of damaging each leg by distance leaped (3 or more per leg); mod is 2 x distance leaped.
* 1.a 1 critical roll _per leg_.
* 1.b 1 _additional_ critical per leg that takes internal structure damage due to leaping damage.
* 2. risk of falling; mod is distance leaped.
* @param movingEntity
* @param data
* @param msg
* @return
*/
public static double predictLeapDamage(Entity movingEntity, TargetRoll data, StringBuilder msg) {
int legMultiplier = (movingEntity.isQuadMek()) ? 4 : 2;
double odds = Compute.oddsAbove(data.getValue(), false) / 100d;
int fallHeight = data.getModifiers().get(data.getModifiers().size()-1).getValue() / 2;
double legDamage = fallHeight * (legMultiplier);
msg.append("\nPredicting expected Leap damage:")
.append(String.format("\n\tFall height: %d", fallHeight))
.append(String.format("\n\tChance of taking damage: %.2f", ((1-odds)*100d))).append('%');

int[] legLocations = {BipedMek.LOC_LLEG, BipedMek.LOC_RLEG, QuadMek.LOC_LARM, QuadMek.LOC_RARM};

// Add required crits; say the effective leg "damage" from a crit is 20 for now.
legDamage += legMultiplier * CRIT_VALUE;
msg.append(
String.format("\n\tAdding %d leg critical chances as %d additional damage", legMultiplier, legMultiplier * CRIT_VALUE)
);

// Add additional crits for each leg that would take internal damage
for (int i=0;i<legMultiplier; i++) {
if (movingEntity.getArmor(legLocations[i]) < fallHeight) {
msg.append("\n\tAdditional critical due to internal structure damage...");
legDamage += CRIT_VALUE;
}
}

// Calculate odds of receiving this damage and return
return legDamage * (1 - odds);
}

}
14 changes: 12 additions & 2 deletions megamek/src/megamek/common/BulldozerMovePath.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* An extension of the MovePath class that stores information about terrain that
* needs
* to be destroyed in order to move along the specified route.
*
*
* @author NickAragua
*/
public class BulldozerMovePath extends MovePath {
Expand All @@ -41,6 +41,7 @@ public class BulldozerMovePath extends MovePath {
Map<Coords, Integer> additionalCosts = new HashMap<>();
List<Coords> coordsToLevel = new ArrayList<>();
double maxPointBlankDamage = -1;
Coords destination = null;

public BulldozerMovePath(Game game, Entity entity) {
super(game, entity);
Expand Down Expand Up @@ -174,6 +175,7 @@ public BulldozerMovePath clone() {
copy.additionalCosts = new HashMap<>(additionalCosts);
copy.coordsToLevel = new ArrayList<>(coordsToLevel);
copy.maxPointBlankDamage = maxPointBlankDamage;
copy.destination = (destination == null) ? destination : new Coords(destination.getX(), destination.getY());
return copy;
}

Expand Down Expand Up @@ -288,6 +290,14 @@ public List<Coords> getCoordsToLevel() {
return coordsToLevel;
}

public Coords getDestination() {
return destination;
}

public void setDestination(Coords d) {
destination = d;
}

@Override
public String toString() {
return super.toString() + " Leveling Cost: " + getLevelingCost() + " Additional Cost: " + getAdditionalCost();
Expand All @@ -297,7 +307,7 @@ public String toString() {
* Comparator implementation useful in comparing two bulldozer move paths by
* how many MP it'll take to accomplish that path, including time wasted
* leveling any obstacles
*
*
* @author NickAragua
*
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public BulldozerMovePath findPathToCoords(Entity entity, Set<Coords> destination
if (destinationCoords.contains(currentPath.getFinalCoords()) &&
((bestPath == null) || (movePathComparator.compare(bestPath, currentPath) > 0))) {
bestPath = currentPath;
// Keep a record of where this path is headed
bestPath.setDestination(closest);
maximumCost = bestPath.getMpUsed() + bestPath.getLevelingCost();
}
}
Expand All @@ -124,8 +126,8 @@ public BulldozerMovePath findPathToCoords(Entity entity, Set<Coords> destination
}

/**
* Calculates the closest coordinates to the given entity Coordinates which you
* have to blow up to get into are considered to be further
* Calculates the closest coordinates to the given entity
* Coordinates which you have to blow up to get into are considered to be further
*/
public static Coords getClosestCoords(Set<Coords> destinationRegion, Entity entity) {
Coords bestCoords = null;
Expand Down
5 changes: 3 additions & 2 deletions megamek/src/megamek/server/totalwarfare/MovePathHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.stream.Collectors;

import megamek.MMConstants;
import megamek.client.ui.Messages;
import megamek.common.*;
import megamek.common.actions.AirMekRamAttackAction;
import megamek.common.actions.AttackAction;
Expand Down Expand Up @@ -2275,7 +2276,7 @@ && getGame().getOptions().booleanOption(OptionsConstants.ADVGRNDMOV_TACOPS_LEAPI
rollTarget = entity.getBasePilotingRoll(stepMoveType);
entity.addPilotingModifierForTerrain(rollTarget, curPos);
rollTarget.append(new PilotingRollData(entity.getId(),
2 * leapDistance, "leaping (leg damage)"));
2 * leapDistance, Messages.getString("TacOps.leaping.leg_damage")));
if (0 < gameManager.doSkillCheckWhileMoving(entity, lastElevation,
lastPos, curPos, rollTarget, false)) {
// do leg damage
Expand All @@ -2298,7 +2299,7 @@ && getGame().getOptions().booleanOption(OptionsConstants.ADVGRNDMOV_TACOPS_LEAPI
rollTarget = entity.getBasePilotingRoll(stepMoveType);
entity.addPilotingModifierForTerrain(rollTarget, curPos);
rollTarget.append(new PilotingRollData(entity.getId(),
leapDistance, "leaping (fall)"));
leapDistance, Messages.getString("TacOps.leaping.fall_damage")));
if (0 < gameManager.doSkillCheckWhileMoving(entity, lastElevation,
lastPos, curPos, rollTarget, false)) {
entity.setElevation(lastElevation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@ void testRankPath() {
when(mockPath.getLastStep()).thenReturn(mockLastStep);
when(mockPath.getStepVector()).thenReturn(new Vector<>());

final TargetRoll mockTargetRoll = MockGenerators.mockTargetRoll(8);
final TargetRoll mockTargetRollTwo = MockGenerators.mockTargetRoll(5);
final List<TargetRoll> testRollList = List.of(mockTargetRoll, mockTargetRollTwo);

doReturn(testRollList).when(testRanker).getPSRList(eq(mockPath));

final Board mockBoard = mock(Board.class);
when(mockBoard.contains(any(Coords.class))).thenReturn(true);
final Coords boardCenter = spy(new Coords(8, 8));
Expand All @@ -461,6 +467,7 @@ void testRankPath() {
when(mockGame.getArtilleryAttacks()).thenReturn(Collections.emptyEnumeration());
when(mockGame.getPlanetaryConditions()).thenReturn(mockPC);
when(mockPrincess.getGame()).thenReturn(mockGame);
when(mockMover.getGame()).thenReturn(mockGame);

final List<Entity> testEnemies = new ArrayList<>();

Expand Down Expand Up @@ -529,10 +536,10 @@ void testRankPath() {
doReturn(0.5)
.when(testRanker)
.getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class));
expected = new RankedPath(-298.125, mockPath, "Calculation: {fall mod ["
expected = new RankedPath(-318.125, mockPath, "Calculation: {fall mod ["
+ LOG_DECIMAL.format(250) + " = " + LOG_DECIMAL.format(0.5) + " * "
+ LOG_DECIMAL.format(500) + "] + braveryMod ["
+ LOG_DECIMAL.format(-3.12) + " = " + LOG_PERCENT.format(0.5)
+ LOG_DECIMAL.format(-23.12) + " = " + LOG_PERCENT.format(0.5)
+ " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5)
+ ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod ["
+ LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * "
Expand All @@ -550,10 +557,10 @@ void testRankPath() {
doReturn(0.75)
.when(testRanker)
.getMovePathSuccessProbability(any(MovePath.class), any(StringBuilder.class));
expected = new RankedPath(-174.6875, mockPath, "Calculation: {fall mod ["
expected = new RankedPath(-184.6875, mockPath, "Calculation: {fall mod ["
+ LOG_DECIMAL.format(125) + " = " + LOG_DECIMAL.format(0.25) + " * "
+ LOG_DECIMAL.format(500) + "] + braveryMod ["
+ LOG_DECIMAL.format(-4.69) + " = " + LOG_PERCENT.format(0.75)
+ LOG_DECIMAL.format(-14.69) + " = " + LOG_PERCENT.format(0.75)
+ " * ((" + LOG_DECIMAL.format(22.5) + " * " + LOG_DECIMAL.format(1.5)
+ ") - " + LOG_DECIMAL.format(40) + "] - aggressionMod ["
+ LOG_DECIMAL.format(30) + " = " + LOG_DECIMAL.format(12) + " * "
Expand Down
Loading

0 comments on commit 9106f42

Please sign in to comment.