- Allow AI to hold back removal based on chance instead of always using them when a suitable target is available

This commit is contained in:
excessum
2014-04-19 05:19:29 +00:00
parent 9654c025e6
commit a4b41c94c2
5 changed files with 123 additions and 0 deletions

View File

@@ -16,10 +16,13 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
@@ -891,5 +894,95 @@ public class ComputerUtilCard {
}
return chosen;
}
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer();
final Player opp = ai.getOpponent();
final int costRemoval = sa.getHostCard().getCMC();
final int costTarget = c.getCMC();
//burn and curse spells
float valueBurn = 0;
if (dmg > 0) {
if (sa.getDescription().contains("would die, exile it instead")) {
destination = ZoneType.Exile;
}
valueBurn = 1.0f * c.getNetDefense() / dmg;
if (sa.getTargetRestrictions().canTgtPlayer()) {
valueBurn /= 2; //preserve option to burn to the face
}
}
//evaluate tempo gain
float valueTempo = Math.max(0.2f * costTarget / costRemoval, valueBurn);
if (c.isEquipped()) {
valueTempo *= 2;
}
if (SpellAbilityAi.isSorcerySpeed(sa)) {
valueTempo *= 2; //sorceries have less usage opportunities
}
if (!c.canBeDestroyed()) {
valueTempo *= 2; //deal with annoying things
}
if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers
c.hasKeyword("Persist") || c.hasKeyword("Undying") || c.hasKeyword("Modular")) {
valueTempo *= 2;
}
if (destination.equals(ZoneType.Hand) && !c.isToken()) {
valueTempo /= 2; //bouncing non-tokens for tempo is less valuable
}
if (c.isLand()) {
valueTempo += 0.5f / opp.getLandsInPlay().size(); //set back opponent's mana
}
if (c.isEnchanted()) {
boolean myEnchants = false;
for (Card enc : c.getEnchantedBy()) {
if (enc.getOwner().equals(ai)) {
myEnchants = true;
break;
}
}
if (!myEnchants) {
valueTempo += 1; //card advantage > tempo
}
}
//evaluate threat of targeted card
float threat = 0;
if (c.isCreature()) {
Combat combat = ai.getGame().getCombat();
threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(c, opp, combat) / ai.getLife();
//TODO:add threat from triggers and other abilities (ie. Master of Cruelties)
} else {
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
//continuous buffs
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
int bonusPT = 0;
if (params.containsKey("AddPower")) {
bonusPT += Integer.valueOf(params.get("AddPower"));
}
if (params.containsKey("AddToughness")) {
bonusPT += Integer.valueOf(params.get("AddToughness"));
}
String kws = params.get("AddKeyword");
if (kws != null) {
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
}
if (bonusPT > 0) {
threat = bonusPT * (1 + opp.getCreaturesInPlay().size()) / 10.0f;
}
}
}
//TODO:add threat from triggers and other abilities (ie. Bident of Thassa)
}
if (!c.getManaAbility().isEmpty()) {
threat += 0.5f * costTarget / opp.getLandsInPlay().size(); //set back opponent's mana
}
final float chance = MyRandom.getRandom().nextFloat();
return chance < Math.max(valueTempo, threat);
}
}

View File

@@ -883,6 +883,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else {
choice = mostExpensive;
}
//option to hold removal instead only applies for single targeted removal
if (sa.isSpell() && tgt.getMaxTargets(source, sa) == 1) {
if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, destination)) {
return false;
}
}
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI.

View File

@@ -302,6 +302,12 @@ public class DamageDealAi extends DamageAiBase {
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, false);
if (c != null) {
//option to hold removal instead only applies for single targeted removal
if (sa.isSpell() && tgt.getMaxTargets(sa.getHostCard(), sa) == 1 && !divided) {
if (!ComputerUtilCard.useRemovalNow(sa, c, dmg, ZoneType.Graveyard)) {
return false;
}
}
tcs.add(c);
if (divided) {
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
@@ -347,6 +353,12 @@ public class DamageDealAi extends DamageAiBase {
} else if (tgt.canTgtCreature()) {
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, mandatory);
if (c != null) {
//option to hold removal instead only applies for single targeted removal
if (sa.isSpell() && tgt.getMaxTargets(sa.getHostCard(), sa) == 1 && !divided) {
if (!ComputerUtilCard.useRemovalNow(sa, c, dmg, ZoneType.Graveyard)) {
return false;
}
}
tcs.add(c);
if (divided) {
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);

View File

@@ -148,6 +148,12 @@ public class DestroyAi extends SpellAbilityAi {
} else {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
}
//option to hold removal instead only applies for single targeted removal
if (sa.isSpell() && abTgt.getMaxTargets(sa.getHostCard(), sa) == 1) {
if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return false;
}
}
if (choice == null) { // can't find anything left
if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa))

View File

@@ -339,6 +339,12 @@ public class PumpAi extends PumpAiBase {
}
t = ComputerUtilCard.getBestAI(list);
//option to hold removal instead only applies for single targeted removal
if (sa.isSpell() && tgt.getMaxTargets(source, sa) == 1) {
if (!ComputerUtilCard.useRemovalNow(sa, t, -defense, ZoneType.Graveyard)) {
return false;
}
}
sa.getTargets().add(t);
list.remove(t);
}