mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 03:08:02 +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.combat.Combat;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
import forge.util.Aggregates;
|
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.MutablePair;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
@@ -892,4 +895,94 @@ public class ComputerUtilCard {
|
|||||||
return chosen;
|
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 {
|
} else {
|
||||||
choice = mostExpensive;
|
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)) {
|
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
|
||||||
List<Card> nonLands = CardLists.getNotType(list, "Land");
|
List<Card> nonLands = CardLists.getNotType(list, "Land");
|
||||||
// Prefer to pull a creature, generally more useful for AI.
|
// 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);
|
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, false);
|
||||||
if (c != null) {
|
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);
|
tcs.add(c);
|
||||||
if (divided) {
|
if (divided) {
|
||||||
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
||||||
@@ -347,6 +353,12 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
} else if (tgt.canTgtCreature()) {
|
} else if (tgt.canTgtCreature()) {
|
||||||
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, mandatory);
|
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, mandatory);
|
||||||
if (c != null) {
|
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);
|
tcs.add(c);
|
||||||
if (divided) {
|
if (divided) {
|
||||||
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
||||||
|
|||||||
@@ -148,6 +148,12 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
} else {
|
} else {
|
||||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
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 (choice == null) { // can't find anything left
|
||||||
if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa))
|
if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa))
|
||||||
|
|||||||
@@ -339,6 +339,12 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t = ComputerUtilCard.getBestAI(list);
|
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);
|
sa.getTargets().add(t);
|
||||||
list.remove(t);
|
list.remove(t);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user