Skip to content

Commit

Permalink
Merge pull request #6486 from Scoppio/feat/princess-new-sliders
Browse files Browse the repository at this point in the history
AI Toy - Piracy, new behaviors, and a new test platform to build upon
  • Loading branch information
HammerGS authored Feb 6, 2025
2 parents 4d96b89 + 2220f96 commit dd7dd68
Show file tree
Hide file tree
Showing 33 changed files with 3,157 additions and 259 deletions.
11 changes: 11 additions & 0 deletions megamek/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ task assembleDist(overwrite: true) {
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
jvmArgs = mmJvmOptions
}

jacocoTestReport {
Expand Down Expand Up @@ -418,6 +419,16 @@ task unitFileMigrationTool(type: JavaExec, dependsOn: jar) {
mainClass = 'megamek.utilities.UnitFileMigrationTool'
}

task testAi(type: JavaExec, dependsOn: jar) {
description = 'Test AI'
group = 'utility'
classpath = sourceSets.main.runtimeClasspath
mainClass = 'megamek.utilities.QuickGameRunner'
args(project.hasProperty("testAiArgs") ? project.property("testAiArgs").split(' ') : "")

jvmArgs = mmJvmOptions
}

tasks.withType(Checkstyle) {
minHeapSize = "200m"
maxHeapSize = "1g"
Expand Down
22 changes: 21 additions & 1 deletion megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ CommonMenuBar.viewToggleHexCoords=Hex Coords
CommonMenuBar.viewLabels=Change Unit Label Display
CommonMenuBar.viewToggleFovHighlight=Highlight within field of view
CommonMenuBar.viewToggleFovDarken=Darken outside field of view
CommonMenuBar.viewToggleFovDarkenTooltip=<html>Darkens hexes with no LOS to the currently selected hex. <br> The height in the source hex is based upon the selected unit or LOS settings.<br>The height in the destination hex is based on the tallest unit in that hex or LoS Settings when none are present.</html>
CommonMenuBar.viewToggleFovDarkenTooltip=Darkens hexes with no LOS to the currently selected hex. The height in the source hex is based upon the selected unit or LOS settings. The height in the destination hex is based on the tallest unit in that hex or LoS Settings when none are present.
CommonMenuBar.viewToggleIsometric=Isometric View
CommonMenuBar.viewToggleFieldOfFire=Field of Fire
CommonMenuBar.viewToggleFieldOfFireToolTip=Outline firing arcs for selected weapons
Expand Down Expand Up @@ -1264,6 +1264,8 @@ CommonSettingsDialog.colors.myUnitColor=My Unit Color
CommonSettingsDialog.colors.allyUnitColor=Ally Unit Color
CommonSettingsDialog.colors.enemyUnitColor=Enemy Unit Color
CommonSettingsDialog.darkenMapAtNight=Darken Map At Night
CommonSettingsDialog.enableExperimentalBotFeatures=Enable experimental bot features.
CommonSettingsDialog.enableExperimentalBotFeatures.tooltip=If enabled, Princess will have access to experimental systems. These systems may be unbalanced, broken or otherwise unfit for normal gameplay. Use at your own risk.
CommonSettingsDialog.defaultAutoejectDisabled=Disable automatic ejection by default for units added in the lobby.
CommonSettingsDialog.defaultWeaponSortOrder.tooltip=<html>Sets the default weapon sort order for chassis/models without a specified default. <br>Changing this setting won't affect games already in progress.</html>
CommonSettingsDialog.defaultWeaponSortOrder=Default Weapon Sort Order:
Expand Down Expand Up @@ -4535,6 +4537,11 @@ BotConfigDialog.autoFleeCheck=Auto Flee Board
BotConfigDialog.autoFleeTooltip=If true, Princess will flee her units off her home board edge even if they are not required to by Forced Withdrawal.
BotConfigDialog.retreatEdgeLabel=to:
BotConfigDialog.retreatEdgeTooltip=The edge to which the bot's crippled units will attempt to retreat if under Forced Withdrawal. NEAREST will simply run to the closest edge.
BotConfigDialog.iAmAPirateCheck=I am a Pirate
BotConfigDialog.iAmAPirateCheckToolTip=If enabled, Princess will ignore civilian status and Forced Withdrawal rules, it will kill anything that moves
BotConfigDialog.experimentalCheck=Experimental features
BotConfigDialog.experimentalCheckToolTip=If enabled, Princess will have access to experimental systems.<br/>These systems may be unbalanced, broken or otherwise unfit for normal gameplay.<br/>Use at your own risk.

BotConfigDialog.homeEdgeLabel=to:
BotConfigDialog.homeEdgeTooltip=The edge to which the bot will attempt to move her units, unless under Forced Withdrawal.
BotConfigDialog.northEdge=NORTH
Expand All @@ -4559,6 +4566,19 @@ BotConfigDialog.fallShameSliderMin=Highway Menace
BotConfigDialog.fallShameSliderMax=Never takes a risk.
BotConfigDialog.fallShameSliderTitle=Piloting Caution
BotConfigDialog.fallShameToolTip=<EM>Piloting Caution:</EM> How much do I want to avoid failed Piloting Rolls?

BotConfigDialog.antiCrowdingSliderMin=Disabled
BotConfigDialog.antiCrowdingSliderMax=Disperse
BotConfigDialog.antiCrowdingTitle=Anti-Crowding
BotConfigDialog.antiCrowdingToolTip=<EM>Anti-Crowding</EM> Keeps a distance between your units. Try to keep enemies at around 60% of the unit max distance.

BotConfigDialog.favorHigherTMMSliderMin=Disabled
BotConfigDialog.favorHigherTMMSliderMax=Sprinter
BotConfigDialog.favorHigherTMMTitle=Favor Higher TMM
BotConfigDialog.favorHigherTMMToolTip=<EM>Favor Higher TMM:</EM> Prefers to move longer distances or in modes which grants it a higher TMM.



BotConfigDialog.braverySliderMax=I fear nothing!
BotConfigDialog.braverySliderMin=Run Away!
BotConfigDialog.braverySliderTitle=Bravery
Expand Down
95 changes: 75 additions & 20 deletions megamek/src/megamek/client/bot/princess/BasicPathRanker.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import megamek.client.bot.princess.BotGeometry.CoordFacingCombo;
import megamek.client.bot.princess.BotGeometry.HexLine;
import megamek.client.bot.princess.UnitBehavior.BehaviorType;
import megamek.codeUtilities.MathUtility;
import megamek.common.*;
import megamek.common.options.OptionsConstants;
import megamek.common.planetaryconditions.PlanetaryConditions;
Expand Down Expand Up @@ -385,7 +386,7 @@ protected double calculateHerdingMod(Coords friendsCoords, MovePath path) {
return herdingMod;
}

private double calculateFacingMod(Entity movingUnit, Game game, final MovePath path) {
protected double calculateFacingMod(Entity movingUnit, Game game, final MovePath path) {
int facingDiff = getFacingDiff(movingUnit, game, path);
double facingMod = Math.max(0.0, 50 * (facingDiff - 1));
logger.trace("facing mod [-{} = max(0, 50 * ({}) - 1)]", facingMod, facingDiff);
Expand Down Expand Up @@ -586,16 +587,18 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
// The further I am from my teammates, the lower this path
// ranks (weighted by Herd Mentality).
double herdingMod = isNotAirborne ? calculateHerdingMod(friendsCoords, pathCopy) : 0;

// Movement is good, it gives defense and extends a player power in the game.
if (movingUnit.getPosition() != null && friendsCoords != null) {
scores.put("friendsDistance", (double) friendsCoords.distance(movingUnit.getPosition()));
}
scores.put("herdingValue", getOwner().getBehaviorSettings().getHerdMentalityValue());
scores.put("herdingIndex", (double) getOwner().getBehaviorSettings().getHerdMentalityIndex());
scores.put("herdingMod", herdingMod);


var movementModFormula = new StringBuilder(64);
// Movement is good, it gives defense and extends a player power in the game.
double movementMod = calculateMovementMod(pathCopy, game, enemies);
double movementMod = calculateMovementMod(pathCopy, game, enemies, movementModFormula);
scores.put("enemyHotSpotCount", (double) getOwner().getEnemyHotSpots().size());
scores.put("herdingValue", getOwner().getBehaviorSettings().getSelfPreservationValue());
scores.put("herdingIndex", (double) getOwner().getBehaviorSettings().getSelfPreservationIndex());
Expand All @@ -610,7 +613,8 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
if (facingMod <= -10000) {
return new RankedPath(facingMod, pathCopy, "Calculation {facing mod[<= -10000]}");
}

var crowdingToleranceFormula = new StringBuilder(64);
double crowdingTolerance = calculateCrowdingTolerance(pathCopy, enemies, crowdingToleranceFormula);
// If I need to flee the board, I want to get closer to my home edge.
double selfPreservationMod= calculateSelfPreservationMod(movingUnit, pathCopy, game);
double offBoardMod = calculateOffBoardMod(pathCopy);
Expand All @@ -622,6 +626,7 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
utility -= aggressionMod;
utility -= herdingMod;
utility += movementMod;
utility -= crowdingTolerance;
utility -= facingMod;
utility -= selfPreservationMod;
utility -= utility * offBoardMod;
Expand Down Expand Up @@ -656,25 +661,29 @@ protected RankedPath rankPath(MovePath path, Game game, int maxRange, double fal
} else {
formula.append("0 no friends");
}
formula
.append("] + movementMod [")
.append(movementMod)
.append("] - facingMod [")
formula.append("]");
if (movementMod != 0.0) {
formula.append(" + ").append(movementModFormula);
}
if (crowdingTolerance != 0.0) {
formula.append(" - ").append(crowdingToleranceFormula);
}

formula.append(" - facingMod [")
.append(LOG_DECIMAL.format(facingMod))
.append(" = max(0, 50 * {")
.append(getFacingDiff(movingUnit, game, pathCopy))
.append(" - 1})]");

logger.trace("utility [{} = - fallMod({}) - offBoard*utility({}) - selfPreservation({}) - facingMod({}) + bravery({}) + movement({}) - aggression({}) - herding({})]",
utility, fallMod, utility * offBoardMod, selfPreservationMod, facingMod, braveryMod, movementMod, aggressionMod, herdingMod);
logger.trace("{}", formula);

RankedPath rankedPath = new RankedPath(utility, pathCopy, formula.toString());
rankedPath.setExpectedDamage(damageEstimate.getMaximumDamageEstimate());
rankedPath.getScores().putAll(scores);
return rankedPath;
}

private double getBraveryMod(double successProbability, FiringPhysicalDamage damageEstimate, double expectedDamageTaken) {
protected double getBraveryMod(double successProbability, FiringPhysicalDamage damageEstimate, double expectedDamageTaken) {
double maximumDamageDone = damageEstimate.getMaximumDamageEstimate();
// My bravery modifier is based on my chance of getting to the
// firing position (successProbability), how much damage I can do
Expand All @@ -687,17 +696,63 @@ private double getBraveryMod(double successProbability, FiringPhysicalDamage dam
}

// Only forces unit to move if there are no units around
private double calculateMovementMod(MovePath pathCopy, Game game, List<Entity> enemies) {
if (!enemies.isEmpty() || !getOwner().getEnemyHotSpots().isEmpty()) {
protected double calculateMovementMod(MovePath pathCopy, Game game, List<Entity> enemies, StringBuilder formula) {
var favorHigherTMM = getOwner().getBehaviorSettings().getFavorHigherTMM();
boolean noEnemiesInSight = enemies.isEmpty() && getOwner().getEnemyHotSpots().isEmpty();
boolean disabledFavorHigherTMM = favorHigherTMM == 0;
if (noEnemiesInSight || !disabledFavorHigherTMM) {
var tmm = Compute.getTargetMovementModifier(pathCopy.getHexesMoved(), pathCopy.isJumping(), pathCopy.isAirborne(), game);
double selfPreservation = getOwner().getBehaviorSettings().getSelfPreservationValue();
var tmmValue = tmm.getValue();
var movementFactor = tmmValue * (selfPreservation + favorHigherTMM);
formula.append("movementMod [").append(movementFactor).append(" = ").append(tmmValue).append(" * (")
.append(selfPreservation).append(" + ").append(favorHigherTMM).append(")]");
logger.trace("movement mod [{} = {} * ({} + {})]", movementFactor, tmmValue, selfPreservation, favorHigherTMM);
return movementFactor;
}
return 0.0;
}

protected double calculateCrowdingTolerance(MovePath movePath, List<Entity> enemies, StringBuilder formula) {
var self = movePath.getEntity();
formula.append(" crowdingTolerance ");
if (!(self instanceof Mek) && !(self instanceof Tank)) {
formula.append("[0 not a Mek or Tank]}");
return 0.0;
}
var distanceMoved = pathCopy.getDistanceTravelled();
var tmm = Compute.getTargetMovementModifier(distanceMoved, pathCopy.isJumping(), pathCopy.isAirborne(), game);
double selfPreservation = getOwner().getBehaviorSettings().getSelfPreservationValue();
var tmmValue = tmm.getValue();
var movementFactor = tmmValue * selfPreservation;
logger.trace("movement mod [{} = {} * {})]", movementFactor, tmmValue, selfPreservation);
return movementFactor;

var antiCrowding = getOwner().getBehaviorSettings().getAntiCrowding();
if (antiCrowding == 0) {
formula.append("[0 antiCrowding is disabled]}");
return 0;
}

var antiCrowdingFactor = (10.0 / (11 - antiCrowding));
final double herdingDistance = Math.ceil(antiCrowding * 1.3);
final double closingDistance = Math.ceil(Math.max(3.0, self.getMaxWeaponRange() * 0.6));

var crowdingFriends = getOwner().getFriendEntities().stream()
.filter(e -> e instanceof Mek || e instanceof Tank)
.filter(Entity::isDeployed)
.map(Entity::getPosition)
.filter(Objects::nonNull)
.filter(c -> c.distance(movePath.getFinalCoords()) <= herdingDistance)
.count();

var crowdingEnemies = enemies.stream()
.filter(e -> e instanceof Mek || e instanceof Tank)
.filter(Entity::isDeployed)
.map(Entity::getPosition)
.filter(Objects::nonNull)
.filter(c -> c.distance(movePath.getFinalCoords()) <= closingDistance)
.count();

double friendsCrowdingTolerance = antiCrowdingFactor * crowdingFriends;
double enemiesCrowdingTolerance = antiCrowdingFactor * crowdingEnemies;
formula.append("[").append(friendsCrowdingTolerance + enemiesCrowdingTolerance).append(" = (")
.append(antiCrowdingFactor).append(" * ").append(crowdingFriends).append(" friends) + (")
.append(antiCrowdingFactor).append(" * ").append(crowdingEnemies).append(" enemies)]");
return friendsCrowdingTolerance + enemiesCrowdingTolerance;
}

/**
Expand Down
Loading

0 comments on commit dd7dd68

Please sign in to comment.