Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 73 additions & 7 deletions forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,16 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
}
}

// Can we extract this up to SPellAbilityAi for everything to have access to?
Card keycardFound = null;
for(String keyName : keyCards) {
CardCollection withKeyCard = CardLists.filter(fetchList, CardPredicates.nameEquals(keyName));
if (withKeyCard.isEmpty()) {
continue;
}
keycardFound = withKeyCard.getFirst();
}

if (sa.hasParam("AILogic")) {
String logic = sa.getParamOrDefault("AILogic", "");
if ("NeverBounceItself".equals(logic)) {
Expand All @@ -1474,6 +1484,8 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
} else if ("WorstCard".equals(logic)) {
return ComputerUtilCard.getWorstAI(fetchList);
} else if ("BestCard".equals(logic)) {
if (keycardFound != null) return keycardFound;

return ComputerUtilCard.getBestAI(fetchList); // generally also means the most expensive one or close to it
} else if ("Mairsil".equals(logic)) {
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
Expand All @@ -1491,6 +1503,12 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, sa, fetchList);
} else if (logic.equals("ConsiderRamp")) {
Card c = considerRamp(decider, sa, fetchList, keycardFound);

if (c != null) {
return c;
}
}
}
if (fetchList.isEmpty()) {
Expand All @@ -1504,6 +1522,7 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
CardLists.shuffle(fetchList);
// Save a card as a default, in case we can't find anything suitable.
Card first = fetchList.get(0);

if (ZoneType.Battlefield.equals(destination)) {
fetchList = CardLists.filter(fetchList, c1 -> {
if (c1.getType().isLegendary()) {
Expand Down Expand Up @@ -1539,13 +1558,19 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
}
}
} else if (origin.contains(ZoneType.Library) && (type.contains("Basic") || areAllBasics(type))) {
if (keycardFound != null) return keycardFound;

c = basicManaFixing(decider, fetchList);
} else if (ZoneType.Hand.equals(destination) && CardLists.getNotType(fetchList, "Creature").isEmpty()) {
if (keycardFound != null) return keycardFound;

c = chooseCreature(decider, fetchList);
} else if (ZoneType.Battlefield.equals(destination) || ZoneType.Graveyard.equals(destination)) {
if (!activator.equals(decider) && sa.hasParam("GainControl")) {
c = ComputerUtilCard.getWorstAI(fetchList);
} else {
if (keycardFound != null) return keycardFound;

c = ComputerUtilCard.getBestAI(fetchList);
}
} else {
Expand All @@ -1556,13 +1581,7 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
}

// Tutor for the first key card in the list, since the list should be in priority order
for(String keyName : keyCards) {
CardCollection withKeyCard = CardLists.filter(fetchList, CardPredicates.nameEquals(keyName));
if (withKeyCard.isEmpty()) {
continue;
}
return withKeyCard.getFirst();
}
if (keycardFound != null) return keycardFound;

// Does AI need a land?
// The logic here seems wrong if the decider isn't the same as the player
Expand Down Expand Up @@ -2038,6 +2057,53 @@ private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardColl
return null;
}

private static Card considerRamp(Player ai, SpellAbility sa, CardCollection choices, Card keycardFound) {
// For cards that might fetch a land or other things, but really might need the land right now.
// Do a rough check of available mana sources (lands on battlefield, other mana producers on battlefield,
// and lands in hand) to decide whether to prioritize fetching a land.

// Count lands already on the battlefield
int landsOnBattlefield = ai.getLandsInPlay().size();

// Count non-land permanents on the battlefield that produce mana (e.g. mana rocks, dorks)
int otherManaProducers = 0;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
if (!c.getManaAbilities().isEmpty()) {
otherManaProducers++;
}
}

// Count lands in hand (they represent future mana sources we expect to play)
int landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS).size();

int totalManaSources = landsOnBattlefield + otherManaProducers + landsInHand;

// Base threshold: below this many total mana sources we should prioritize getting a land
int threshold = 4;

// If we have a keycard target, also make sure we'll eventually have enough mana to cast it.
// If the keycard's CMC is further than one land drop away from our current sources, keep ramping.
if (keycardFound != null && keycardFound.getCMC() > totalManaSources + 1) {
threshold = Math.max(threshold, keycardFound.getCMC() - 1);
}

// If we are below the threshold, look for a land in the available choices and prefer it
if (totalManaSources < threshold) {
CardCollection landsInChoices = CardLists.filter(choices, CardPredicates.LANDS);
if (!landsInChoices.isEmpty()) {
// Prefer a land that produces mana over a purely tapped/utility land
CardCollection manaLands = CardLists.filter(landsInChoices, CardPredicates.LANDS_PRODUCING_MANA);
if (!manaLands.isEmpty()) {
return manaLands.get(0);
}
return landsInChoices.get(0);
}
}

// We have enough mana sources — let the caller use keycardFound (may be null, triggering fallthrough)
return keycardFound;
}

@Override
public boolean willPayUnlessCost(Player payer, SpellAbility sa, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card host = sa.getHostCard();
Expand Down
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/t/treefolk_harbinger.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Name:Treefolk Harbinger
ManaCost:G
Types:Creature Treefolk Druid
PT:0/3
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters, you may search your library for a Treefolk or Forest card, reveal it, then shuffle and put that card on top.
T:Mode$ ChangesZone | AILogic$ ConsiderRamp | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters, you may search your library for a Treefolk or Forest card, reveal it, then shuffle and put that card on top.
SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Card.Treefolk,Card.Forest | ChangeNum$ 1 | ShuffleNonMandatory$ True
AI:RemoveDeck:Random
Oracle:When Treefolk Harbinger enters, you may search your library for a Treefolk or Forest card, reveal it, then shuffle and put that card on top.
Loading