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

Princess - improved AMS handling #5617

Merged
merged 5 commits into from
Jun 21, 2024
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
10 changes: 6 additions & 4 deletions megamek/src/megamek/client/bot/princess/FireControl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1646,8 +1646,9 @@ FiringPlan guessFullFiringPlan(final Entity shooter,
// cycle through my weapons
for (final WeaponMounted weapon : shooter.getWeaponList()) {
// respect restriction on manual AMS firing.
if (!game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS) &&
weapon.getType().hasFlag(WeaponType.F_AMS)) {
if (weapon.getType().hasFlag(WeaponType.F_AMS) &&
(!game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS) ||
!weapon.curMode().equals(Weapon.MODE_AMS_MANUAL))) {
continue;
}

Expand Down Expand Up @@ -1969,8 +1970,9 @@ FiringPlan getFullFiringPlan(final Entity shooter,
}

// respect restriction on manual AMS firing.
if (!game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS) &&
weapon.getType().hasFlag(WeaponType.F_AMS)) {
if (weapon.getType().hasFlag(WeaponType.F_AMS) &&
(!game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS) ||
!weapon.curMode().equals(Weapon.MODE_AMS_MANUAL))) {
continue;
}

Expand Down
181 changes: 181 additions & 0 deletions megamek/src/megamek/client/bot/princess/Princess.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import megamek.common.util.StringUtil;
import megamek.common.weapons.AmmoWeapon;
import megamek.common.weapons.StopSwarmAttack;
import megamek.common.weapons.Weapon;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -50,11 +51,14 @@
import java.text.NumberFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class Princess extends BotClient {
private static final char PLUS = '+';
private static final char MINUS = '-';

private static final int MAX_OVERHEAT_AMS = 14;

private final IHonorUtil honorUtil = new HonorUtil();

private boolean initialized = false;
Expand Down Expand Up @@ -88,6 +92,9 @@ public class Princess extends BotClient {
private final Set<Integer> attackedWhileFleeing = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<Integer> crippledUnits = new HashSet<>();

// Track entities that fired an AMS manually this round
private List<Integer> manualAMSIds;

/**
* Returns a new Princess Bot with the given behavior and name, configured for the given
* host and port. The new Princess Bot outputs its settings to its own logger.
Expand Down Expand Up @@ -627,6 +634,13 @@ protected void calculateFiringTurn() {
double newDamage = existingTargetDamage + shot.getExpectedDamage();
damageMap.put(targetId, newDamage);

// Track manual AMS use each round
if (shot.getWeapon().getType().hasFlag(Weapon.F_AMS)) {
if (shot.getWeapon().curMode().equals(Weapon.MODE_AMS_MANUAL)) {
flagManualAMSUse(shooter.getId());
}
}

if (shot.getUpdatedFiringMode() != null) {
super.sendModeChange(shooter.getId(), shooter.getEquipmentNum(shot.getWeapon()), shot.getUpdatedFiringMode());
}
Expand Down Expand Up @@ -1908,6 +1922,7 @@ public void endOfTurnProcessing() {
// refreshCrippledUnits should happen after checkForDishonoredEnemies, since checkForDishoneredEnemies
// wants to examine the units that were considered crippled at the *beginning* of the turn and were attacked.
refreshCrippledUnits();
setAMSModes();
}

@Override
Expand Down Expand Up @@ -2157,6 +2172,172 @@ private void launchFighters(MovePath path) {
}
}

/**
* Sets the mode for AMS on each unit this bot controls. This may be on or off, and possibly
* manual fire if the game options allow it, with any change taking effect next round.
* Normal setting is to have the AMS active/automatic. It may be turned off to conserve
* ammo on relatively undamaged units, and laser AMS may be turned off to help reduce
* overheating. Manual use is reserved as an emergency anti-infantry measure.
*
*/
private void setAMSModes() {

// Get conventional infantry if manual mode is available
List<Entity> enemyInfantry = new ArrayList<>();
if (game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS)) {
for (Entity curEnemy : this.getEnemyEntities().stream().filter(Entity::isDeployed).collect(Collectors.toSet())) {
if (curEnemy.getPosition() != null && curEnemy.isVisibleToEnemy()) {

if (curEnemy instanceof Infantry && !(curEnemy instanceof EjectedCrew)) {
enemyInfantry.add(curEnemy);
}

}
}
}

for (Entity curEntity : this.getEntitiesOwned()) {
if (!curEntity.isDeployed() || curEntity.getPosition() == null) {
continue;
}

List<WeaponMounted> activeAMS = curEntity.
getWeaponList().
stream().
filter(w -> w.getType().hasFlag(AmmoWeapon.F_AMS) && w.hasModes()).
collect(Collectors.toList());

if (!activeAMS.isEmpty()) {

// Set default to on/automatic and test to see if it should be off or manual instead
EquipmentMode newAMSMode = EquipmentMode.getMode(Weapon.MODE_AMS_ON);

boolean isOverheating = (curEntity instanceof Mech) && (curEntity.getHeat() >= MAX_OVERHEAT_AMS);

// If there are enough nearby enemy infantry (only counted if the game option is
// set), choose manual fire
if (!enemyInfantry.isEmpty() && !curEntity.isAirborne()) {
int infantryRange = enemyInfantry.stream().mapToInt(e -> Compute.effectiveDistance(game, curEntity, e)).min().getAsInt();
if (infantryRange <= 3) {
newAMSMode = EquipmentMode.getMode(Weapon.MODE_AMS_MANUAL);
}
}

// If AMS was used manually this round, chances are it will be needed next round too
if (usedManualAMS(curEntity.getId())) {
newAMSMode = EquipmentMode.getMode(Weapon.MODE_AMS_MANUAL);
}

for (WeaponMounted curAMS : activeAMS) {

EquipmentMode curMode = curAMS.curMode();

// Turn off laser AMS to help with overheating problems
if (curAMS.getType().hasFlag(WeaponType.F_ENERGY)) {
if (isOverheating) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As above. Also, while we apply stricter overheating checks to ASF than to 'mechs in other sections of the Princess code, it looks like isOverheating would ignore ASFs, causing them to be less likely to disable their LAMS units.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Deliberate choice - fighters are more vulnerable to damage so having AMS active, even when overheating, is considered useful. I considered included them at first but decided just to do Mechs and keep the complexity down. Can look into adding something if warranted, though.

newAMSMode = EquipmentMode.getMode(Weapon.MODE_AMS_OFF);
}
} else {

// Determine if ammo needs to be conserved
boolean conserveAmmo = curAMS.getLinkedAmmo().getUsableShotsLeft() <= (int) Math.floor(curAMS.getOriginalShots() *
behaviorSettings.getSelfPreservationValue() / 100.0);
Comment on lines +2243 to +2244
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this be wrapped into the existing ammo conservation code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe, but there's a distinct difference. The existing ammo conservation code is a shoot/no shoot determination. For AMS it is actually a mode change that prevents automatic operation in the following turn. Keeps the existing code about determining whether to fire, and this on whether to turn this specific system off or on.


// Consider turning off AMS to conserve ammo unless it's needed for infantry
if (conserveAmmo && !newAMSMode.equals(Weapon.MODE_AMS_MANUAL)) {

int ammoTN = 12 - behaviorSettings.getBraveryIndex();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Likewise; this could probably be incorporated into the existing conservation / threshold calculation code.


// Fighting a missile boat is more likely to require an active AMS
int lastTargetID = curEntity.getLastTarget();
if (lastTargetID >= 0) {
Entity lastTarget = game.getEntity(lastTargetID);
if (lastTarget != null && lastTarget.getRole() == UnitRole.MISSILE_BOAT) {
ammoTN += 4;
}
}

// Heavily damaged units are more likely to require an active AMS than
// lightly damaged ones
switch (curEntity.getDamageLevel()) {
case Entity.DMG_NONE:
ammoTN -= 4;
break;
case Entity.DMG_LIGHT:
ammoTN -= 2;
break;
case Entity.DMG_MODERATE:
ammoTN += 1;
break;
case Entity.DMG_HEAVY:
ammoTN += 4;
case Entity.DMG_CRIPPLED:
ammoTN += 8;
break;
default:
break;
}

if (ammoTN < 10) {
if (Compute.d6(2) >= ammoTN) {
newAMSMode = EquipmentMode.getMode(Weapon.MODE_AMS_OFF);
}
}
Comment on lines +2281 to +2285
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is the choice to change mode randomized rather than deterministic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

While it's good to have the bulk of the behavior calculated rather than random, I'm not entirely sold on a fully deterministic Princess; for one, it is too easy to predict. There will be circumstances like this, where there is no clear 'good' outcome that can be easily calculated vs the impact the decision will have. Sometimes it's best all 'round to have it to do a simple gut check and hope for the best.


}

}

// Set the mode for the AMS to get the new mode number, and register the change
// with the server
if (!curMode.equals(newAMSMode)) {
int modeNumber = curAMS.setMode(newAMSMode.getName());
if (modeNumber != -1) {
sendModeChange(curEntity.getId(), curEntity.getEquipmentNum(curAMS), modeNumber);
}
}

}

}

}

// Clear the manual AMS tracking list for next round
clearManualAMSIds();
}

/**
* Flag an entity as having used manual AMS this round
* @param id
*/
public void flagManualAMSUse (int id) {
if (manualAMSIds == null) {
manualAMSIds = new ArrayList<>();
}
if (!manualAMSIds.contains(id)) {
manualAMSIds.add(id);
}
}

public boolean usedManualAMS (int id) {
if (manualAMSIds == null) {
manualAMSIds = new ArrayList<>();
return false;
}
return manualAMSIds.contains(id);
}

/**
* Clear the manual AMS tracking list
*/
public void clearManualAMSIds () {
if (manualAMSIds == null) {
manualAMSIds = new ArrayList<>();
}
manualAMSIds.clear();
}

public void sendChat(final String message, final Level logLevel) {
if (LogManager.getLogger().getLevel().isLessSpecificThan(logLevel)) {
super.sendChat(message);
Expand Down
Loading