mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 03:08:02 +00:00
- Basic implementation for chaining two damage spells, supports damage+damage or damage+debuff.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -630,18 +630,34 @@ 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;
|
||||
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;
|
||||
@@ -656,6 +672,7 @@ public class AiController {
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a simplification, since one mana source can produce more than one mana,
|
||||
// but should work in most circumstances to ensure safety in whatever the AI is using this for.
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +220,25 @@ public class DamageDealAi extends DamageAiBase {
|
||||
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")) ||
|
||||
sourceName.equals("Crater's Claws")){
|
||||
@@ -238,6 +258,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
source.setSVar("PayX", Integer.toString(actualPay));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user