Skip to content

Commit

Permalink
Extra cost gift rework (#6733)
Browse files Browse the repository at this point in the history
* added PromisedGift as xCount

* SpellAbilityAi: move chooseOptionalCosts

* SpellAbilityAi: chooseOptionalCosts check for invalid targets

* Give API logic access to castSA

* ~ add BaseSpell for PromiseGift

* Unearth: remove unneeded Param

* PromiseGift: uses AITgts to stop AI from using Gift when not needed

---------

Co-authored-by: tool4EvEr <[email protected]>
  • Loading branch information
Hanmac and tool4EvEr authored Jan 5, 2025
1 parent 1209f39 commit 2fcf95c
Show file tree
Hide file tree
Showing 32 changed files with 155 additions and 141 deletions.
77 changes: 45 additions & 32 deletions forge-ai/src/main/java/forge/ai/AiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -775,9 +775,15 @@ private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) {
if (currentState != null) {
host.setState(sa.getCardStateName(), false);
}
if (sa.isSpell()) {
host.setCastSA(sa);
}

AiPlayDecision decision = canPlayAndPayForFace(sa);

if (sa.isSpell()) {
host.setCastSA(null);
}
if (currentState != null) {
host.setState(currentState, false);
}
Expand Down Expand Up @@ -918,7 +924,7 @@ public AiPlayDecision canPlaySa(SpellAbility sa) {
Sentry.setExtra("Card", card.getName());
Sentry.setExtra("SA", sa.toString());

boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);

// remove added extra
Sentry.removeExtra("Card");
Expand Down Expand Up @@ -1296,9 +1302,9 @@ public AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolea
if (spell instanceof SpellApiBased) {
boolean chance = false;
if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerNoCostWithSubs(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
} else {
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
}
if (!chance) {
return AiPlayDecision.TargetingFailed;
Expand Down Expand Up @@ -1620,38 +1626,45 @@ private SpellAbility chooseSpellAbilityToPlayFromList(final List<SpellAbility> a
continue;
}

if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm")))
)) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
// skip evaluating Storm unless we reached the minimum Storm count
continue;
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm")))
)) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
// skip evaluating Storm unless we reached the minimum Storm count
continue;
}
}
}
// living end AI decks
// TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
if (useLivingEnd) {
if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) {
if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
&& !player.cantLoseForZeroOrLessLife()
&& player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
aiPlayDecision = AiPlayDecision.CantAfford;
} else {
aiPlayDecision = AiPlayDecision.WillPlay;

// living end AI decks
// TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
if (useLivingEnd) {
if (sa.isCycling() && sa.canCastTiming(player)
&& player.getCardsIn(ZoneType.Library).size() >= 10) {
if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
&& !player.cantLoseForZeroOrLessLife() && player.getLife() <= sa.getPayCosts()
.getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
aiPlayDecision = AiPlayDecision.CantAfford;
} else {
aiPlayDecision = AiPlayDecision.WillPlay;
}
}
}
} else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
if (isLifeInDanger) { //needs more tune up for certain conditions
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay;
} else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES).size() > 4) {
} else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
if (isLifeInDanger) { // needs more tune up for certain conditions
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa
: AiPlayDecision.WillPlay;
} else if (CardLists
.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES)
.size() > 4) {
if (player.getCreaturesInPlay().size() >= 4) // it's good minimum
continue;
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player)
&& ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
aiPlayDecision = AiPlayDecision.WillPlay;
// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
} else {
continue;
}
Expand Down Expand Up @@ -1726,7 +1739,7 @@ public boolean doTrigger(SpellAbility spell, boolean mandatory) {
if (spell instanceof WrappedAbility)
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
if (spell.getApi() != null)
return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true;
Expand Down
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ public static CardCollection choosePermanentsToSacrifice(final Player ai, final

// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) {
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
// AI would not run this trigger if given the chance
return sacrificed;
}
Expand Down
5 changes: 5 additions & 0 deletions forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
Expand Down Expand Up @@ -122,6 +123,10 @@ public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<Spell
boolean choseOptCost = false;
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
if (!list.isEmpty()) {
// still add base spell in case of Promise Gift
if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) {
result.add(sa);
}
list = player.getController().chooseOptionalCosts(sa, list);
if (!list.isEmpty()) {
choseOptCost = true;
Expand Down
4 changes: 2 additions & 2 deletions forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
Original file line number Diff line number Diff line change
Expand Up @@ -1491,7 +1491,7 @@ public static CardCollection getAvailableManaSources(final Player ai, final bool
AbilitySub sub = m.getSubAbility();
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
continue;
}
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
Expand Down Expand Up @@ -1571,7 +1571,7 @@ private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
continue;
}
}
Expand Down
79 changes: 10 additions & 69 deletions forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

import java.security.InvalidParameterException;
import java.util.*;
import java.util.function.Predicate;

Expand Down Expand Up @@ -352,11 +351,7 @@ public <T extends GameEntity> T chooseSingleEntityForEffect(FCollectionView<T> o
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
return SpellApiToAi.Converter.get(sa).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
}

@Override
Expand Down Expand Up @@ -398,11 +393,7 @@ public List<SpellAbility> chooseSpellAbilitiesForEffect(List<SpellAbility> spell
@Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
return SpellApiToAi.Converter.get(sa).chooseSingleSpellAbility(player, sa, spells, params);
}

@Override
Expand Down Expand Up @@ -876,11 +867,7 @@ public int chooseNumber(SpellAbility sa, String title, int min, int max) {

@Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseNumber(player, sa, min, max, params);
return SpellApiToAi.Converter.get(sa).chooseNumber(player, sa, min, max, params);
}

@Override
Expand Down Expand Up @@ -982,11 +969,7 @@ public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType k
*/
@Override
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseBinary(kindOfChoice, sa, params);
return SpellApiToAi.Converter.get(sa).chooseBinary(kindOfChoice, sa, params);
}

@Override
Expand Down Expand Up @@ -1056,11 +1039,7 @@ public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa,
if (options.size() <= 1) {
return Iterables.getFirst(options, null);
}
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCounterType(options, sa, params);
return SpellApiToAi.Converter.get(sa).chooseCounterType(options, sa, params);
}

@Override
Expand Down Expand Up @@ -1217,7 +1196,7 @@ public boolean payCombatCost(Card c, Cost cost, SpellAbility sa, String prompt)

@Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
if (SpellApiToAi.Converter.get(sa.getApi()).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
if (SpellApiToAi.Converter.get(sa).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
if (!ComputerUtilCost.canPayCost(cost, sa, player, true)) {
return false;
}
Expand Down Expand Up @@ -1397,11 +1376,7 @@ public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa

@Override
public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
return SpellApiToAi.Converter.get(sa).chooseCardName(player, sa, faces);
}

@Override
Expand Down Expand Up @@ -1506,11 +1481,7 @@ public void cancelAwaitNextInput() {

@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces);
return SpellApiToAi.Converter.get(sa).chooseCardFace(player, sa, faces);
}

@Override
Expand All @@ -1520,11 +1491,7 @@ public ICardFace chooseSingleCardFace(SpellAbility sa, String message, Predicate

@Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params);
return SpellApiToAi.Converter.get(sa).chooseCardState(player, sa, states, params);
}

@Override
Expand Down Expand Up @@ -1576,32 +1543,7 @@ public List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) {

@Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();

for (OptionalCostValue opt : optionalCostValues) {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
Cost fullCost = opt.getCost().copy().add(costSoFar);
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);

// Playability check for Kicker
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
SpellAbility kickedSaCopy = fullCostSa.copy();
kickedSaCopy.addOptionalCost(opt.getType());
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
copy.setCastSA(kickedSaCopy);
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
}
}

if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
}

return chosenOptCosts;
return SpellApiToAi.Converter.get(chosen).chooseOptionalCosts(chosen, player, optionalCostValues);
}

@Override
Expand Down Expand Up @@ -1661,5 +1603,4 @@ public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> v

return choices;
}

}
Loading

0 comments on commit 2fcf95c

Please sign in to comment.