- Basic implementation for chaining two damage spells, supports damage+damage or damage+debuff.

This commit is contained in:
Agetian
2018-12-10 18:26:16 +03:00
parent c5b1f6cf61
commit c79ed8b609
5 changed files with 134 additions and 16 deletions

View File

@@ -51,6 +51,7 @@ public class AiCardMemory {
HELD_MANA_SOURCES_FOR_MAIN2, // These mana sources will not be used before Main 2
HELD_MANA_SOURCES_FOR_DECLBLK, // These mana sources will not be used before Combat - Declare Blockers
HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK, // These mana sources will not be used before the opponent's Combat - Declare Blockers
HELD_MANA_SOURCES_FOR_NEXT_PRIORITY, // These mana sources will not be used until the next time the AI receives priority
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
@@ -65,6 +66,7 @@ public class AiCardMemory {
private final Set<Card> memHeldManaSources;
private final Set<Card> memHeldManaSourcesForCombat;
private final Set<Card> memHeldManaSourcesForEnemyCombat;
private final Set<Card> memHeldManaSourcesForNextPriority;
private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memBouncedThisTurn;
@@ -84,6 +86,7 @@ public class AiCardMemory {
this.memTrickAttackers = new HashSet<>();
this.memChosenFogEffect = new HashSet<>();
this.memMarkedToAvoidReentry = new HashSet<>();
this.memHeldManaSourcesForNextPriority = new HashSet<>();
}
private Set<Card> getMemorySet(MemorySet set) {
@@ -98,6 +101,8 @@ public class AiCardMemory {
return memHeldManaSourcesForCombat;
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
return memHeldManaSourcesForEnemyCombat;
case HELD_MANA_SOURCES_FOR_NEXT_PRIORITY:
return memHeldManaSourcesForNextPriority;
case ATTACHED_THIS_TURN:
return memAttachedThisTurn;
case ANIMATED_THIS_TURN:

View File

@@ -630,31 +630,48 @@ public class AiController {
}
public boolean reserveManaSources(SpellAbility sa) {
return reserveManaSources(sa, PhaseType.MAIN2, false);
return reserveManaSources(sa, PhaseType.MAIN2, false, false, null);
}
public boolean reserveManaSourcesTillNextPriority(SpellAbility sa, SpellAbility exceptForSa) {
return reserveManaSources(sa, null, false, true, exceptForSa);
}
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) {
return reserveManaSources(sa, phaseType, enemy, true, null);
}
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean nextPriority, SpellAbility exceptForThisSa) {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
// used for chained spells where two spells need to be cast in succession
if (exceptForThisSa != null) {
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
}
if (manaSources.isEmpty()) {
return false;
}
AiCardMemory.MemorySet memSet;
switch (phaseType) {
case MAIN2:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
case COMBAT_DECLARE_BLOCKERS:
memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK
: AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
break;
default:
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
+ phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
if (phaseType == null && nextPriority) {
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY;
} else {
switch (phaseType) {
case MAIN2:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
case COMBAT_DECLARE_BLOCKERS:
memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK
: AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
break;
default:
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
+ phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
}
}
// This is a simplification, since one mana source can produce more than one mana,
@@ -1317,6 +1334,9 @@ public class AiController {
// re-created if needed and used for any AI logic that needs it.
predictedCombat = null;
// Reset priority mana reservation
AiCardMemory.clearMemorySet(player, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY);
if (useSimulation) {
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
}

View File

@@ -7,6 +7,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.ability.ApiType;
@@ -15,10 +16,15 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.cost.Cost;
import forge.game.cost.CostPartMana;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
@@ -182,4 +188,65 @@ public class ComputerUtilAbility {
return targeted;
}
public static Pair<SpellAbility, Integer> getDamageAfterChainingSpells(Player ai, SpellAbility sa, String damage) {
if (sa.getSubAbility() != null || sa.getParent() != null) {
// Doesn't work yet for complex decisions where damage is only a part of the decision process
return null;
}
// Try to chain damage/debuff effects
if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) {
// currently only works for predictable numeric damage
CardCollection cards = new CardCollection();
cards.addAll(ai.getCardsIn(ZoneType.Hand));
cards.addAll(ai.getCardsIn(ZoneType.Battlefield));
cards.addAll(ai.getCardsActivableInExternalZones(true));
for (Card c : cards) {
for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.equals(sa) || ab.getSubAbility() != null) { // decisions for complex SAs with subs are not supported yet
continue;
}
// currently works only with cards that don't have additional costs (only mana is supported)
if (ab.getPayCosts() != null && (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) {
String dmgDef = "0";
if (ab.getApi() == ApiType.DealDamage) {
dmgDef = ab.getParamOrDefault("NumDmg", "0");
} else if (ab.getApi() == ApiType.Pump) {
dmgDef = ab.getParamOrDefault("NumDef", "0");
if (dmgDef.startsWith("-")) {
dmgDef = dmgDef.substring(1);
} else {
continue; // not a toughness debuff
}
}
if (StringUtils.isNumeric(dmgDef) && ab.canPlay()) { // currently doesn't work for X and other dependent costs
if (sa.usesTargeting() && ab.usesTargeting()) {
// Ensure that the chained spell can target at least the same things (or more) as the current one
TargetRestrictions tgtSa = sa.getTargetRestrictions();
TargetRestrictions tgtAb = sa.getTargetRestrictions();
if (tgtSa.canTgtCreature() && !tgtAb.canTgtCreature()) {
continue;
} else if (tgtSa.canTgtPlaneswalker() && !tgtAb.canTgtPlaneswalker()) {
continue;
}
// FIXME: should it also check restrictions for targeting players?
ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above
ManaCost total = ManaCost.combine(costSa, costAb);
SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));
// can we pay both costs?
if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
//aic.reserveManaSourcesTillNextPriority(ab); // reserve mana for the second spell
return Pair.of(ab, Integer.parseInt(dmgDef));
}
}
}
}
}
}
}
return null;
}
}

View File

@@ -856,6 +856,11 @@ public class ComputerUtilMana {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
// Mana reserved for spell synchronization
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY)) {
return true;
}
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
// For combat tricks, always obey mana reservation

View File

@@ -21,6 +21,7 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import org.apache.commons.lang3.tuple.Pair;
import java.util.List;
import java.util.Map;
@@ -195,6 +196,9 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
// Try to chain damage/debuff effects
Pair<SpellAbility, Integer> chainDmg = ComputerUtilAbility.getDamageAfterChainingSpells(ai, sa, damage);
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
@@ -216,8 +220,24 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
if (!this.damageTargetAI(ai, sa, dmg, false)) {
return false;
// test what happens if we chain this to another damaging spell
if (chainDmg != null && ai.getController().isAI()) {
int extraDmg = chainDmg.getValue();
if (!this.damageTargetAI(ai, sa, dmg + extraDmg, false)) {
return false; // won't play it even in chain
} else {
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
// the second spell so we don't misplay
if (chainDmg != null && ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
aic.reserveManaSourcesTillNextPriority(chainDmg.getKey(), sa);
}
}
} else {
// simple targeting when there is no spell chaining plan
if (!this.damageTargetAI(ai, sa, dmg, false)) {
return false;
}
}
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
@@ -238,6 +258,7 @@ public class DamageDealAi extends DamageAiBase {
source.setSVar("PayX", Integer.toString(actualPay));
}
}
return true;
}