/**
* MegaMek -
* Copyright (C) 2003,2004,2005 Ben Mazur (bmazur@sev.org)
*
* 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;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;
import megamek.common.Compute;
import megamek.common.Coords;
import megamek.common.CriticalSlot;
import megamek.common.Entity;
import megamek.common.IEntityMovementType;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.LosEffects;
import megamek.common.Mech;
import megamek.common.MovePath;
import megamek.common.MoveStep;
import megamek.common.PilotingRollData;
import megamek.common.TargetRoll;
import megamek.common.Targetable;
import megamek.common.Terrains;
import megamek.common.ToHitData;
/**
* TODO: add the notion of a dependent state (at least a first pass estimate of
* worst case threat) for when psr's are made. TODO: add a notion of a blocked
* move, something that could open up after another mech moves.
*/
public class MoveOption extends MovePath {
private static final long serialVersionUID = -4517093562444861980L;
public static class WeightedComparator implements Comparator<MoveOption> {
private double utility_weight;
private double damage_weight;
public WeightedComparator(double utility, double damage) {
utility_weight = utility;
damage_weight = damage;
}
public int compare(MoveOption e0, MoveOption e1) {
if (damage_weight * e0.damage - utility_weight * e0.getUtility() > damage_weight
* e1.damage - utility_weight * e1.getUtility()) {
return -1;
}
return 1;
}
}
public static class Table extends HashMap<MovePath.Key, MoveOption> {
private static final long serialVersionUID = 5926883297848807149L;
public void put(MoveOption es) {
this.put(es.getKey(), es);
}
public MoveOption get(MoveOption es) {
return super.get(es.getKey());
}
public MoveOption remove(MoveOption es) {
return super.remove(es.getKey());
}
public ArrayList<MoveOption> getArray() {
return new ArrayList<MoveOption>(values());
}
}
public static class DistanceComparator implements Comparator<MoveOption> {
public int compare(MoveOption e0, MoveOption e1) {
return e0.getDistUtility() < e1.getDistUtility() ? -1 : 1;
}
}
public static class DamageInfo {
double threat;
double damage;
double max_threat;
double min_damage;
}
public static final DistanceComparator DISTANCE_COMPARATOR = new DistanceComparator();
public static final int ATTACK_MOD = 0;
public static final int DEFENCE_MOD = 1;
public static final int ATTACK_PC = 2;
public static final int DEFENCE_PC = 3;
boolean inDanger = false;
boolean doomed = false;
boolean isPhysical = false;
double self_threat = 0;
double movement_threat = 0;
double self_damage = 0;
double damage = 0;
double threat = 0;
private transient CEntity centity;
transient ArrayList<String> tv = new ArrayList<String>();
transient HashMap<CEntity, DamageInfo> damageInfos = new HashMap<CEntity, DamageInfo>();
private Coords pos;
private int facing;
private boolean prone;
public MoveOption(IGame game, CEntity centity) {
super(game, centity.entity);
this.centity = centity;
pos = centity.entity.getPosition();
facing = centity.entity.getFacing();
prone = centity.entity.isProne();
}
public MoveOption(MoveOption base) {
this(base.game, base.centity);
steps = new Vector<MoveStep>(base.steps);
threat = base.threat;
damage = base.damage;
movement_threat = base.movement_threat;
tv = new ArrayList<String>(base.tv);
self_threat = base.self_threat;
inDanger = base.inDanger;
doomed = base.doomed;
isPhysical = base.isPhysical;
self_damage = base.self_damage;
pos = base.pos;
facing = base.facing;
prone = base.prone;
}
@Override
public MoveOption clone() {
return new MoveOption(this);
}
public double getThreat(CEntity e) {
return getDamageInfo(e, true).threat;
}
public void setThreat(CEntity e, double value) {
getDamageInfo(e, true).threat = value;
}
public double getMinDamage(CEntity e) {
return getDamageInfo(e, true).min_damage;
}
public double getDamage(CEntity e) {
return getDamageInfo(e, true).damage;
}
public void setDamage(CEntity e, double value) {
getDamageInfo(e, true).damage = value;
}
CEntity getCEntity() {
return centity;
}
@Override
public MoveOption addStep(int step_type) {
super.addStep(step_type);
MoveStep current = getLastStep();
// running with gyro or hip hit is dangerous!
if ((current.getMovementType() == IEntityMovementType.MOVE_RUN)
&& ((entity.getBadCriticals(CriticalSlot.TYPE_SYSTEM,
Mech.SYSTEM_GYRO, Mech.LOC_CT) > 0) || entity
.hasHipCrit())) {
getStep(0).setDanger(true);
current.setDanger(true);
}
if (current.isDanger()) {
if (getCEntity().base_psr_odds < .1) {
current.setMovementType(IEntityMovementType.MOVE_ILLEGAL);
} else {
double cur_threat = getCEntity().getThreatUtility(
.2 * entity.getWeight(), ToHitData.SIDE_REAR)
* (1 - Math.pow(getCEntity().base_psr_odds, 2));
movement_threat += cur_threat;
if (centity.getTb().debug) {
tv.add(cur_threat + " Movement Threat \r\n");
}
}
}
return this;
}
public int getMovementheatBuildup() {
MoveStep last = getLastStep();
if (last == null) {
return 0;
}
int heat = last.getTotalHeat();
int move = 0;
switch (last.getMovementType()) {
case IEntityMovementType.MOVE_WALK:
case IEntityMovementType.MOVE_VTOL_WALK:
move = 1;
break;
case IEntityMovementType.MOVE_RUN:
case IEntityMovementType.MOVE_VTOL_RUN:
move = 2;
break;
case IEntityMovementType.MOVE_JUMP:
move = getEntity().getJumpHeat(last.getMpUsed());
break;
default:
move = 1000;
}
return heat + move; // illegal?
}
public boolean changeToPhysical() {
MoveStep last = getLastStep();
if (isJumping()) {
if (getEntity().canCharge()) {
return false;
}
if (!getEntity().canDFA()) {
return false;
}
} else {
if (getEntity().canDFA()) {
return false;
}
}
boolean isClan = getEntity().isClan();
if ((last == null)
|| (last.getMovementType() == IEntityMovementType.MOVE_ILLEGAL)) {
return false;
}
if ((last.getType() != STEP_FORWARDS)
|| (isClan
&& game.getOptions().booleanOption("no_clan_physical") && (getEntity()
.getSwarmAttackerId() == Entity.NONE))) {
return false;
}
Enumeration<Entity> e = game.getEntities(last.getPosition());
// TODO: this just takes the first target
while (e.hasMoreElements()) {
Entity en = e.nextElement();
if (!en.isSelectableThisTurn() && en.isEnemyOf(entity)) {
isPhysical = true;
removeLastStep();
if (isJumping()) {
addStep(MovePath.STEP_DFA, en);
} else {
addStep(MovePath.STEP_CHARGE, en);
}
return true;
}
}
return false;
}
// it would be nice to have a stand still move...
public void setState() {
entity = centity.entity;
if (steps.size() == 0) {
entity.setPosition(pos);
entity.setFacing(facing);
entity.setSecondaryFacing(facing);
entity.delta_distance = 0;
entity.setProne(prone);
} else {
entity.setPosition(getFinalCoords());
entity.setFacing(getFinalFacing());
entity.setSecondaryFacing(getFinalFacing());
entity.setProne(getFinalProne());
entity.delta_distance = getHexesMoved();
}
entity.moved = getLastStepMovementType();
}
/**
* TODO: replace with more common logic approximates the attack and
* defensive modifies assumes that set state has been called
*/
public int[] getModifiers(final Entity te) {
// set them at the appropriate positions
final Entity ae = entity;
int attHeight = ae.isProne() ? 0 : 1;
int targHeight = te.isProne() ? 0 : 1;
int attEl = 0;
int targEl = 0;
attEl = ae.getElevation() + attHeight;
targEl = te.getElevation() + targHeight;
boolean pc = false;
boolean apc = false;
// get all relevent modifiers
ToHitData toHita = new ToHitData();
ToHitData toHitd = new ToHitData();
toHita.append(Compute.getAttackerMovementModifier(game, ae.getId()));
toHita.append(Compute.getTargetMovementModifier(game, te.getId()));
toHita.append(Compute.getTargetTerrainModifier(game, te));
toHita.append(Compute.getAttackerTerrainModifier(game, ae.getId()));
toHitd.append(Compute.getAttackerMovementModifier(game, te.getId()));
toHitd.append(Compute.getTargetMovementModifier(game, ae.getId()));
if (!(isPhysical && isJumping())) {
toHitd.append(Compute.getTargetTerrainModifier(game, ae));
}
toHitd.append(Compute.getAttackerTerrainModifier(game, te.getId()));
IHex attHex = game.getBoard().getHex(ae.getPosition());
if (attHex.containsTerrain(Terrains.WATER) && (attHex.surface() > attEl)) {
toHita.addModifier(TargetRoll.IMPOSSIBLE,
"Attacker in depth 2+ water");
toHitd.addModifier(TargetRoll.IMPOSSIBLE,
"Defender in depth 2+ water");
} else if ((attHex.surface() == attEl) && (ae.height() > 0)) {
apc = true;
}
IHex targHex = game.getBoard().getHex(te.getPosition());
if (targHex.containsTerrain(Terrains.WATER)) {
if ((targHex.surface() == targEl) && (te.height() > 0)) {
pc = true;
} else if (targHex.surface() > targEl) {
toHita.addModifier(TargetRoll.IMPOSSIBLE,
"Attacker in depth 2+ water");
toHitd.addModifier(TargetRoll.IMPOSSIBLE,
"Defender in depth 2+ water");
}
}
// calc & add attacker los mods
LosEffects los = LosEffects.calculateLos(game, ae.getId(), te);
toHita.append(los.losModifiers(game));
// save variables
pc = los.isTargetCover();
apc = los.isAttackerCover();
// reverse attacker & target partial cover & calc defender los mods
int temp = los.getTargetCover();
los.setTargetCover(los.getAttackerCover());
los.setAttackerCover(temp);
toHitd.append(los.losModifiers(game));
// heatBuildup
if (ae.getHeatFiringModifier() != 0) {
toHita.addModifier(ae.getHeatFiringModifier(), "heatBuildup");
}
if (te.getHeatFiringModifier() != 0) {
toHitd.addModifier(te.getHeatFiringModifier(), "heatBuildup");
}
// target immobile
if (te.isImmobile()) {
toHita.addModifier(-4, "target immobile");
}
if (ae.isImmobile()) {
toHitd.addModifier(-4, "target immobile");
}
final int range = ae.getPosition().distance(te.getPosition());
// target prone
if (te.isProne()) {
// easier when point-blank
if (range == 1) {
toHita.addModifier(-2, "target prone and adjacent");
}
// harder at range
if (range > 1) {
toHita.addModifier(1, "target prone and at range");
}
}
if (ae.isProne()) {
// easier when point-blank
if (range == 1) {
toHitd.addModifier(-2, "target prone and adjacent");
}
// harder at range
if (range > 1) {
toHitd.addModifier(1, "target prone and at range");
}
}
return new int[] { toHita.getValue(), toHitd.getValue(), apc ? 1 : 0,
pc ? 1 : 0 };
}
/**
* TODO: the result of this calculation should be cached...
*/
public double getUtility() {
// self threat and self damage are considered transient
double temp_threat = (threat + movement_threat + self_threat + (double) getMovementheatBuildup() / 20)
/ getCEntity().strategy.attack;
double temp_damage = (damage + self_damage) * centity.strategy.attack;
if (threat + movement_threat > 4 * centity.avg_armor) {
double ratio = (threat + movement_threat)
/ (centity.avg_armor + .25 * centity.avg_iarmor);
if (ratio > 2) {
temp_threat += centity.bv / 15.0; // likely to die
doomed = true;
inDanger = true;
} else if (ratio > 1) {
temp_threat += centity.bv / 30.0; // in danger
inDanger = true;
} else {
temp_threat += centity.bv / 75.0; // in danger
inDanger = true;
}
} else if (threat + movement_threat > 30) {
temp_threat += centity.entity.getWeight();
}
double retVal = temp_threat - temp_damage;
// If the move has a chance of making MASC fail...
if (hasActiveMASC()) {
int mascTN = 0;
for (final Enumeration<MoveStep> i = getSteps(); i
.hasMoreElements();) {
MoveStep step = i.nextElement();
if (step.isUsingMASC() && (step.getTargetNumberMASC() > mascTN)) {
mascTN = step.getTargetNumberMASC();
}
}
double mascMult = Compute.oddsAbove(mascTN) / 100;
if (mascMult < 1.0) {
inDanger = true;
}
retVal *= (mascMult > 0) ? mascMult : 0.01;
}
// If getting up is difficult...
if (prone) {
PilotingRollData tmpPRD = centity.getEntity()
.checkGetUp(getStep(0));
if ((tmpPRD != null)
&& ((tmpPRD.getValue() == TargetRoll.IMPOSSIBLE) || (tmpPRD
.getValue() == TargetRoll.AUTOMATIC_FAIL))) {
retVal *= 0.01;
}
}
return retVal;
}
/**
* get maximum damage in this current state from enemy accounting for torso
* twisting and slightly for heat -- the ce passed in is supposed to be the
* enemy mech
*/
public double getMaxModifiedDamage(MoveOption enemy, int modifier, int apc) {
double max = 0;
int distance = getFinalCoords().distance(enemy.getFinalCoords());
double mod = 1;
// heat effect modifiers
if (enemy.isJumping()
|| (enemy.entity.heat + enemy.entity.heatBuildup > 4)) {
if (enemy.centity.overheat == CEntity.OVERHEAT_LOW) {
mod = .75;
} else if (enemy.centity.overheat == CEntity.OVERHEAT_HIGH) {
mod = .5;
} else {
mod = .9;
}
}
int enemy_firing_arcs[] = { 0, MovePath.STEP_TURN_LEFT,
MovePath.STEP_TURN_RIGHT };
for (int i = 0; i < enemy_firing_arcs.length; i++) {
enemy_firing_arcs[i] = CEntity.getThreatHitArc(enemy
.getFinalCoords(), MovePath.getAdjustedFacing(enemy
.getFinalFacing(), enemy_firing_arcs[i]), getFinalCoords());
}
max = enemy.centity.getModifiedDamage((apc == 1) ? CEntity.TT
: enemy_firing_arcs[0], distance, modifier);
if (enemy_firing_arcs[1] == ToHitData.SIDE_FRONT) {
max = Math.max(max, enemy.centity.getModifiedDamage(CEntity.TT,
distance, modifier));
} else {
max = Math.max(max, enemy.centity.getModifiedDamage(
enemy_firing_arcs[1], distance, modifier));
}
if (enemy_firing_arcs[2] == ToHitData.SIDE_FRONT) {
max = Math.max(max, enemy.centity.getModifiedDamage(CEntity.TT,
distance, modifier));
} else {
max = Math.max(max, enemy.centity.getModifiedDamage(
enemy_firing_arcs[2], distance, modifier));
}
// TODO this is not quite right, but good enough for now...
// ideally the pa charaterization should be in centity
max *= mod;
if (!enemy.getFinalProne() && (distance == 1)
&& (enemy_firing_arcs[0] != ToHitData.SIDE_REAR)) {
IHex h = game.getBoard().getHex(getFinalCoords());
IHex h1 = game.getBoard().getHex(enemy.getFinalCoords());
if (Math.abs(h.getElevation() - h1.getElevation()) < 2) {
max += (((h1.getElevation() - h.getElevation() == 1) || getFinalProne()) ? 5
: 1)
* ((enemy_firing_arcs[0] == ToHitData.SIDE_FRONT) ? .2
: .05)
* centity.entity.getWeight()
* Compute.oddsAbove(3 + modifier)
/ 100
+ (1 - enemy.centity.base_psr_odds)
* enemy.entity.getWeight() / 10.0;
}
}
return max;
}
public DamageInfo getDamageInfo(CEntity cen, boolean create) {
DamageInfo result = damageInfos.get(cen);
if (create && (result == null)) {
result = new DamageInfo();
damageInfos.put(cen, result);
}
return result;
}
public double getDistUtility() {
return getMpUsed() + movement_threat * 100 / centity.bv;
}
/**
* There could still be a problem here, but now it's the callers problem
*/
int getPhysicalTargetId() {
MoveStep step = getLastStep();
if (step == null) {
return -1;
}
Targetable target = step.getTarget(game);
if (target == null) {
return -1;
}
return target.getTargetId();
}
@Override
public String toString() {
return getEntity().getShortName() + " " + getEntity().getId() + " "
+ getFinalCoords() + " " + super.toString() + "\r\n Utility: "
+ getUtility() + " \r\n" + tv + "\r\n";
}
}