mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 10:48:00 +00:00
- 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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user