mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 11:18:01 +00:00
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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")) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user