mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 11:18:01 +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_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_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_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
|
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
|
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||||
BOUNCED_THIS_TURN, // These cards were bounced 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> memHeldManaSources;
|
||||||
private final Set<Card> memHeldManaSourcesForCombat;
|
private final Set<Card> memHeldManaSourcesForCombat;
|
||||||
private final Set<Card> memHeldManaSourcesForEnemyCombat;
|
private final Set<Card> memHeldManaSourcesForEnemyCombat;
|
||||||
|
private final Set<Card> memHeldManaSourcesForNextPriority;
|
||||||
private final Set<Card> memAttachedThisTurn;
|
private final Set<Card> memAttachedThisTurn;
|
||||||
private final Set<Card> memAnimatedThisTurn;
|
private final Set<Card> memAnimatedThisTurn;
|
||||||
private final Set<Card> memBouncedThisTurn;
|
private final Set<Card> memBouncedThisTurn;
|
||||||
@@ -84,6 +86,7 @@ public class AiCardMemory {
|
|||||||
this.memTrickAttackers = new HashSet<>();
|
this.memTrickAttackers = new HashSet<>();
|
||||||
this.memChosenFogEffect = new HashSet<>();
|
this.memChosenFogEffect = new HashSet<>();
|
||||||
this.memMarkedToAvoidReentry = new HashSet<>();
|
this.memMarkedToAvoidReentry = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForNextPriority = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Card> getMemorySet(MemorySet set) {
|
private Set<Card> getMemorySet(MemorySet set) {
|
||||||
@@ -98,6 +101,8 @@ public class AiCardMemory {
|
|||||||
return memHeldManaSourcesForCombat;
|
return memHeldManaSourcesForCombat;
|
||||||
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
||||||
return memHeldManaSourcesForEnemyCombat;
|
return memHeldManaSourcesForEnemyCombat;
|
||||||
|
case HELD_MANA_SOURCES_FOR_NEXT_PRIORITY:
|
||||||
|
return memHeldManaSourcesForNextPriority;
|
||||||
case ATTACHED_THIS_TURN:
|
case ATTACHED_THIS_TURN:
|
||||||
return memAttachedThisTurn;
|
return memAttachedThisTurn;
|
||||||
case ANIMATED_THIS_TURN:
|
case ANIMATED_THIS_TURN:
|
||||||
|
|||||||
@@ -630,18 +630,34 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean reserveManaSources(SpellAbility sa) {
|
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) {
|
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);
|
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
||||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
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()) {
|
if (manaSources.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AiCardMemory.MemorySet memSet;
|
AiCardMemory.MemorySet memSet;
|
||||||
|
if (phaseType == null && nextPriority) {
|
||||||
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY;
|
||||||
|
} else {
|
||||||
switch (phaseType) {
|
switch (phaseType) {
|
||||||
case MAIN2:
|
case MAIN2:
|
||||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||||
@@ -656,6 +672,7 @@ public class AiController {
|
|||||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is a simplification, since one mana source can produce more than one mana,
|
// 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.
|
// 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.
|
// re-created if needed and used for any AI logic that needs it.
|
||||||
predictedCombat = null;
|
predictedCombat = null;
|
||||||
|
|
||||||
|
// Reset priority mana reservation
|
||||||
|
AiCardMemory.clearMemorySet(player, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_PRIORITY);
|
||||||
|
|
||||||
if (useSimulation) {
|
if (useSimulation) {
|
||||||
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
|
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameActionUtil;
|
import forge.game.GameActionUtil;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
@@ -15,10 +16,15 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostPartMana;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
public class ComputerUtilAbility {
|
public class ComputerUtilAbility {
|
||||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||||
@@ -182,4 +188,65 @@ public class ComputerUtilAbility {
|
|||||||
|
|
||||||
return targeted;
|
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();
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
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();
|
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||||
|
|
||||||
// For combat tricks, always obey mana reservation
|
// For combat tricks, always obey mana reservation
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import forge.game.spellability.TargetChoices;
|
|||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -195,6 +196,9 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to chain damage/debuff effects
|
||||||
|
Pair<SpellAbility, Integer> chainDmg = ComputerUtilAbility.getDamageAfterChainingSpells(ai, sa, damage);
|
||||||
|
|
||||||
// temporarily disabled until better AI
|
// temporarily disabled until better AI
|
||||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -216,9 +220,25 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return 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)) {
|
if (!this.damageTargetAI(ai, sa, dmg, false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
|
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
|
||||||
sourceName.equals("Crater's Claws")){
|
sourceName.equals("Crater's Claws")){
|
||||||
@@ -238,6 +258,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
source.setSVar("PayX", Integer.toString(actualPay));
|
source.setSVar("PayX", Integer.toString(actualPay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user