Merge branch 'ai-updates' into 'master'

Fixed an issue with the AI not playing spells like Repeal anymore, tweaked AI damage prediction to account for static effects a little better.

See merge request core-developers/forge!1166
This commit is contained in:
Michael Kamensky
2018-12-04 08:59:30 +00:00
12 changed files with 72 additions and 16 deletions

View File

@@ -670,7 +670,11 @@ public class AiController {
// This is for playing spells regularly (no Cascade/Ripple etc.) // This is for playing spells regularly (no Cascade/Ripple etc.)
private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) {
if (!ComputerUtilCost.canPayCost(sa, player)) { boolean xCost = ComputerUtilMana.hasXInAnyCostPart(sa);
if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) {
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
return AiPlayDecision.CantAfford; return AiPlayDecision.CantAfford;
} }
@@ -678,7 +682,20 @@ public class AiController {
return AiPlayDecision.CantPlaySa; return AiPlayDecision.CantPlaySa;
} }
return canPlaySa(sa); AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
if (canPlay != AiPlayDecision.WillPlay) {
return canPlay;
}
if (xCost && !ComputerUtilCost.canPayCost(sa, player)) {
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
return AiPlayDecision.CantAfford;
}
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
// are willing to play the SA
return AiPlayDecision.WillPlay;
} }
public AiPlayDecision canPlaySa(SpellAbility sa) { public AiPlayDecision canPlaySa(SpellAbility sa) {

View File

@@ -787,6 +787,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
final String sVar = ability.getSVar(amount); final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) { if (sVar.equals("XChoice")) {
c = AbilityUtils.calculateAmount(source, "ChosenX", ability); c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
source.setSVar("ChosenX", "Number$" + String.valueOf(c));
} else if (amount.equals("All")) { } else if (amount.equals("All")) {
c = source.getCounters(cost.counter); c = source.getCounters(cost.counter);
} else if (sVar.equals("Targeted$CardManaCost")) { } else if (sVar.equals("Targeted$CardManaCost")) {

View File

@@ -2104,6 +2104,16 @@ public class ComputerUtilCombat {
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true); defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true); attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
// Damage prevention might come from a static effect
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
if (isCombatDamagePrevented(attacker, blocker, attackerDamage)) {
attackerDamage = 0;
}
if (isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
defenderDamage = 0;
}
}
if (combat != null) { if (combat != null) {
for (Card atkr : combat.getAttackersBlockedBy(blocker)) { for (Card atkr : combat.getAttackersBlockedBy(blocker)) {
if (!atkr.equals(attacker)) { if (!atkr.equals(attacker)) {

View File

@@ -92,7 +92,7 @@ public class ComputerUtilCost {
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic) // value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) { if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount()); final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice")) { if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type))); sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
} }
} }

View File

@@ -16,11 +16,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.*;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayment;
import forge.game.mana.Mana; import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.mana.ManaPool; import forge.game.mana.ManaPool;
@@ -1560,6 +1556,24 @@ public class ComputerUtilMana {
return convoke; return convoke;
} }
public static boolean hasXInAnyCostPart(SpellAbility sa) {
boolean xCost = false;
if (sa.getPayCosts() != null) {
for (CostPart p : sa.getPayCosts().getCostParts()) {
if (p instanceof CostPartMana) {
if (((CostPartMana) p).getAmountOfX() > 0) {
xCost = true;
break;
}
} else if (p.getAmount().equals("X")) {
xCost = true;
break;
}
}
}
return xCost;
}
public static int determineMaxAffordableX(Player ai, SpellAbility sa) { public static int determineMaxAffordableX(Player ai, SpellAbility sa) {
if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) { if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) {
return -1; return -1;

View File

@@ -91,6 +91,8 @@ public class AttachAi extends SpellAbilityAi {
} }
// Don't try to attach an aura to a card which will have protection from the relevant color // Don't try to attach an aura to a card which will have protection from the relevant color
// TODO: Fix this not to be dependent on "Protection from Color" wording and to be flexible to account for
// other possibilities like "protection from all colors" etc.
Card targeted = sa.getTargets().getFirstTargetedCard(); Card targeted = sa.getTargets().getFirstTargetedCard();
if (targeted != null && !targeted.getZone().is(ZoneType.Battlefield)) { if (targeted != null && !targeted.getZone().is(ZoneType.Battlefield)) {
byte color = sa.getTargets().getFirstTargetedCard().getCurrentState().getColor(); byte color = sa.getTargets().getFirstTargetedCard().getCurrentState().getColor();

View File

@@ -15,6 +15,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -29,7 +30,7 @@ public class ManaEffectAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) { protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
if ("ManaRitual".equals(aiLogic)) { if (aiLogic.startsWith("ManaRitual")) {
return doManaRitualLogic(ai, sa); return doManaRitualLogic(ai, sa);
} else if ("Always".equals(aiLogic)) { } else if ("Always".equals(aiLogic)) {
return true; return true;
@@ -117,7 +118,8 @@ public class ManaEffectAi extends SpellAbilityAi {
String produced = sa.getParam("Produced"); String produced = sa.getParam("Produced");
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced); byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
if ("ChosenX".equals(sa.getParam("Amount")) int numCounters = 0;
if ("XChoice".equals(host.getSVar("X"))
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterType.KI; // Petalmane Baku CounterType ctrType = CounterType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) { for (CostPart part : sa.getPayCosts().getCostParts()) {
@@ -126,7 +128,11 @@ public class ManaEffectAi extends SpellAbilityAi {
break; break;
} }
} }
manaReceived = host.getCounters(ctrType); numCounters = host.getCounters(ctrType);
manaReceived = numCounters;
if ("ManaRitualBattery".equals(sa.getParam("AILogic"))) {
manaReceived++; // adds an extra mana even if no counters removed
}
} }
int searchCMC = numManaSrcs - selfCost + manaReceived; int searchCMC = numManaSrcs - selfCost + manaReceived;
@@ -196,6 +202,12 @@ public class ManaEffectAi extends SpellAbilityAi {
CardPredicates.lessCMC(searchCMC), CardPredicates.lessCMC(searchCMC),
Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor)))); Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor))));
if ("ManaRitualBattery".equals(sa.getParam("AILogic"))) {
// Don't remove more counters than would be needed to cast everything we want to cast
int maxCtrs = Aggregates.sum(castableSpells, CardPredicates.Accessors.fnGetCmc);
sa.setSVar("ChosenX", "Number$" + Math.min(numCounters, maxCtrs));
}
// TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material. // TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material.
return castableSpells.size() > 0; return castableSpells.size() > 0;
} }

View File

@@ -2,7 +2,7 @@ Name:Black Mana Battery
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.
A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | References$ X,Y | Produced$ B | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {B}, then add an additional {B} for each charge counter removed this way. A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | References$ X,Y | Produced$ B | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {B}, then add an additional {B} for each charge counter removed this way.
SVar:Y:Number$1/Plus.ChosenX SVar:Y:Number$1/Plus.ChosenX
SVar:X:XChoice SVar:X:XChoice
#ChosenX SVar created by Cost payment #ChosenX SVar created by Cost payment

View File

@@ -2,7 +2,7 @@ Name:Blue Mana Battery
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.
A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ U | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | SpellDescription$ Add {U}, then add an additional {U} for each charge counter removed this way. A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ U | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {U}, then add an additional {U} for each charge counter removed this way.
SVar:Y:Number$1/Plus.ChosenX SVar:Y:Number$1/Plus.ChosenX
SVar:X:XChoice SVar:X:XChoice
#ChosenX SVar created by Cost payment #ChosenX SVar created by Cost payment

View File

@@ -2,7 +2,7 @@ Name:Green Mana Battery
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.
A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ G | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | SpellDescription$ Add {G}, then add an additional {G} for each charge counter removed this way. A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ G | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {G}, then add an additional {G} for each charge counter removed this way.
SVar:Y:Number$1/Plus.ChosenX SVar:Y:Number$1/Plus.ChosenX
SVar:X:XChoice SVar:X:XChoice
#ChosenX SVar created by Cost payment #ChosenX SVar created by Cost payment

View File

@@ -2,7 +2,7 @@ Name:Red Mana Battery
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.
A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ R | Amount$ Y | References$ X,Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {R}, then add an additional {R} for each charge counter removed this way. A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ R | Amount$ Y | References$ X,Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {R}, then add an additional {R} for each charge counter removed this way.
SVar:Y:Number$1/Plus.ChosenX SVar:Y:Number$1/Plus.ChosenX
SVar:X:XChoice SVar:X:XChoice
#ChosenX SVar created by Cost payment #ChosenX SVar created by Cost payment

View File

@@ -2,7 +2,7 @@ Name:White Mana Battery
ManaCost:4 ManaCost:4
Types:Artifact Types:Artifact
A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME.
A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ W | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {W}, then add an additional {W} for each charge counter removed this way. A:AB$ Mana | Cost$ T SubCounter<X/CHARGE> | Produced$ W | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {W}, then add an additional {W} for each charge counter removed this way.
SVar:Y:Number$1/Plus.ChosenX SVar:Y:Number$1/Plus.ChosenX
SVar:X:XChoice SVar:X:XChoice
#ChosenX SVar created by Cost payment #ChosenX SVar created by Cost payment