mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 12:48:00 +00:00
Merge branch 'ai-buyback' into 'master'
Various AI updates and fixes + rewritten enhanced Buyback logic See merge request core-developers/forge!1168
This commit is contained in:
@@ -51,6 +51,7 @@ import forge.game.player.PlayerActionConfirmMode;
|
|||||||
import forge.game.replacement.ReplaceMoved;
|
import forge.game.replacement.ReplaceMoved;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.spellability.*;
|
import forge.game.spellability.*;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.trigger.WrappedAbility;
|
import forge.game.trigger.WrappedAbility;
|
||||||
@@ -670,7 +671,7 @@ 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) {
|
||||||
boolean xCost = sa.getPayCosts().hasXInAnyCostPart(sa);
|
boolean xCost = sa.getPayCosts().hasXInAnyCostPart();
|
||||||
|
|
||||||
if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) {
|
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
|
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
|
||||||
@@ -707,6 +708,13 @@ public class AiController {
|
|||||||
return canPlaySa(((WrappedAbility) sa).getWrappedAbility());
|
return canPlaySa(((WrappedAbility) sa).getWrappedAbility());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations
|
||||||
|
if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
|
||||||
|
if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) {
|
||||||
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When processing a new SA, clear the previously remembered cards that have been marked to avoid re-entry
|
// When processing a new SA, clear the previously remembered cards that have been marked to avoid re-entry
|
||||||
// which might potentially cause a stack overflow.
|
// which might potentially cause a stack overflow.
|
||||||
AiCardMemory.clearMemorySet(this, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
|
AiCardMemory.clearMemorySet(this, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
|
||||||
@@ -801,10 +809,69 @@ public class AiController {
|
|||||||
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
||||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add any other necessary logic to play a basic spell here
|
// add any other necessary logic to play a basic spell here
|
||||||
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) {
|
||||||
|
boolean wasteBuybackAllowed = false;
|
||||||
|
|
||||||
|
// About to lose game : allow
|
||||||
|
if (ComputerUtil.aiLifeInDanger(player, true, 0)) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copies = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(card.getName())).size();
|
||||||
|
// Have two copies : allow
|
||||||
|
if (copies >= 2) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int neededMana = 0;
|
||||||
|
boolean dangerousRecurringCost = false;
|
||||||
|
for (SpellAbility sa2 : GameActionUtil.getOptionalCosts(sa)) {
|
||||||
|
if (sa2.isOptionalCostPaid(OptionalCost.Buyback)) {
|
||||||
|
Cost sac = sa2.getPayCosts();
|
||||||
|
CostAdjustment.adjust(sac, sa2);
|
||||||
|
if (sac.getCostMana() != null) {
|
||||||
|
neededMana = sac.getCostMana().getMana().getCMC();
|
||||||
|
}
|
||||||
|
if (sac.hasSpecificCostType(CostPayLife.class)
|
||||||
|
|| sac.hasSpecificCostType(CostDiscard.class)
|
||||||
|
|| sac.hasSpecificCostType(CostSacrifice.class)) {
|
||||||
|
dangerousRecurringCost = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// won't be able to afford buyback any time soon
|
||||||
|
// if Buyback cost includes sacrifice, life, discard
|
||||||
|
if (dangerousRecurringCost) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory Crystal-like effects need special handling
|
||||||
|
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
for (StaticAbility s : c.getStaticAbilities()) {
|
||||||
|
if ("ReduceCost".equals(s.getParam("Mode"))
|
||||||
|
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
|
||||||
|
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (neededMana < 0) {
|
||||||
|
neededMana = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
|
||||||
|
if (hasMana < neededMana - 1) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasteBuybackAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
// not sure "playing biggest spell" matters?
|
// not sure "playing biggest spell" matters?
|
||||||
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
|
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public enum AiProps { /** */
|
|||||||
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
||||||
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
||||||
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS ("true"), /** */
|
||||||
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
||||||
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
||||||
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
||||||
|
|||||||
@@ -935,10 +935,9 @@ public class Cost implements Serializable {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasXInAnyCostPart(SpellAbility sa) {
|
public boolean hasXInAnyCostPart() {
|
||||||
boolean xCost = false;
|
boolean xCost = false;
|
||||||
if (sa.getPayCosts() != null) {
|
for (CostPart p : this.getCostParts()) {
|
||||||
for (CostPart p : sa.getPayCosts().getCostParts()) {
|
|
||||||
if (p instanceof CostPartMana) {
|
if (p instanceof CostPartMana) {
|
||||||
if (((CostPartMana) p).getAmountOfX() > 0) {
|
if (((CostPartMana) p).getAmountOfX() > 0) {
|
||||||
xCost = true;
|
xCost = true;
|
||||||
@@ -949,7 +948,6 @@ public class Cost implements Serializable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return xCost;
|
return xCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3
|
|||||||
# library to put some into the graveyard.
|
# library to put some into the graveyard.
|
||||||
INTUITION_ALTERNATIVE_LOGIC=true
|
INTUITION_ALTERNATIVE_LOGIC=true
|
||||||
|
|
||||||
|
# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not
|
||||||
|
# use them unless absolutely necessary (or unless multiple copies are in hand).
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS=true
|
||||||
|
|
||||||
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
||||||
# card on top of the library
|
# card on top of the library
|
||||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3
|
|||||||
# library to put some into the graveyard.
|
# library to put some into the graveyard.
|
||||||
INTUITION_ALTERNATIVE_LOGIC=true
|
INTUITION_ALTERNATIVE_LOGIC=true
|
||||||
|
|
||||||
|
# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not
|
||||||
|
# use them unless absolutely necessary (or unless multiple copies are in hand).
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS=true
|
||||||
|
|
||||||
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
||||||
# card on top of the library
|
# card on top of the library
|
||||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=5
|
|||||||
# library to put some into the graveyard.
|
# library to put some into the graveyard.
|
||||||
INTUITION_ALTERNATIVE_LOGIC=true
|
INTUITION_ALTERNATIVE_LOGIC=true
|
||||||
|
|
||||||
|
# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not
|
||||||
|
# use them unless absolutely necessary (or unless multiple copies are in hand).
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS=true
|
||||||
|
|
||||||
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
||||||
# card on top of the library
|
# card on top of the library
|
||||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=2
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF=3
|
|||||||
# library to put some into the graveyard.
|
# library to put some into the graveyard.
|
||||||
INTUITION_ALTERNATIVE_LOGIC=true
|
INTUITION_ALTERNATIVE_LOGIC=true
|
||||||
|
|
||||||
|
# If enabled, the AI will run some additional checks in order to try to preserve spells that have Buyback and not
|
||||||
|
# use them unless absolutely necessary (or unless multiple copies are in hand).
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS=false
|
||||||
|
|
||||||
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
# How big of a difference is allowed between the revealed card CMC and the currently castable CMC to still put the
|
||||||
# card on top of the library
|
# card on top of the library
|
||||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=1
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD=1
|
||||||
|
|||||||
Reference in New Issue
Block a user