mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 01:38:13 +00:00
- Basic AI logic support for the Cemetery cycle (VOW)
This commit is contained in:
@@ -1,32 +1,12 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import forge.game.card.*;
|
||||
import forge.game.keyword.Keyword;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiCardMemory;
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialAiLogic;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardType;
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
@@ -35,12 +15,14 @@ import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -50,8 +32,12 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ChangeZoneAi extends SpellAbilityAi {
|
||||
/*
|
||||
@@ -1540,11 +1526,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if ("DeathgorgeScavenger".equals(logic)) {
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
}
|
||||
if ("ExtraplanarLens".equals(logic)) {
|
||||
} else if ("ExtraplanarLens".equals(logic)) {
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
}
|
||||
if ("ExileCombatThreat".equals(logic)) {
|
||||
} else if ("ExileCombatThreat".equals(logic)) {
|
||||
return doExileCombatThreatLogic(ai, sa);
|
||||
}
|
||||
|
||||
@@ -1570,7 +1554,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("NeverBounceItself".equals(logic)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) {
|
||||
@@ -1593,6 +1577,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
multipleCardsToChoose.remove(0);
|
||||
return choice;
|
||||
}
|
||||
} else if (logic.startsWith("ExilePreference")) {
|
||||
return doExilePreferenceLogic(decider, sa, fetchList);
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -2029,6 +2015,121 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {
|
||||
if (fetchList.isEmpty()) {
|
||||
return null; // there was nothing to choose at all
|
||||
}
|
||||
|
||||
final Card host = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final String valid = logic.split(":")[1];
|
||||
final boolean isCurse = logic.contains("Curse");
|
||||
final boolean isOwnOnly = logic.contains("OwnOnly");
|
||||
final boolean isWorstChoice = logic.contains("Worst");
|
||||
final boolean isRandomChoice = logic.contains("Random");
|
||||
|
||||
if (logic.endsWith("HighestCMC")) {
|
||||
return ComputerUtilCard.getMostExpensivePermanentAI(fetchList);
|
||||
} else if (logic.contains("MostProminent")) {
|
||||
CardCollection scanList = new CardCollection();
|
||||
if (logic.endsWith("OwnType")) {
|
||||
scanList.addAll(aiPlayer.getCardsIn(ZoneType.Library));
|
||||
scanList.addAll(aiPlayer.getCardsIn(ZoneType.Hand));
|
||||
} else if (logic.endsWith("OppType")) {
|
||||
// this assumes that the deck list is known to the AI before the match starts,
|
||||
// so it's possible to figure out what remains in library/hand if you know what's
|
||||
// in graveyard, exile, etc.
|
||||
scanList.addAll(aiPlayer.getOpponents().getCardsIn(ZoneType.Library));
|
||||
scanList.addAll(aiPlayer.getOpponents().getCardsIn(ZoneType.Hand));
|
||||
}
|
||||
|
||||
if (logic.contains("NonLand")) {
|
||||
scanList = CardLists.filter(scanList, Predicates.not(Presets.LANDS));
|
||||
}
|
||||
|
||||
if (logic.contains("NonExiled")) {
|
||||
scanList = CardLists.filter(scanList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
CardCollectionView imprinted = host.getImprintedCards();
|
||||
if (imprinted.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (Card c : imprinted) {
|
||||
return !c.getType().sharesCardTypeWith(card.getType());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Map<CardType.CoreType, Integer> typesInDeck = Maps.newHashMap();
|
||||
for (final Card c : scanList) {
|
||||
for (CardType.CoreType ct : c.getType().getCoreTypes()) {
|
||||
Integer count = typesInDeck.get(ct);
|
||||
if (count == null) {
|
||||
count = 0;
|
||||
}
|
||||
typesInDeck.put(ct, count + 1);
|
||||
}
|
||||
}
|
||||
int max = 0;
|
||||
CardType.CoreType maxType = CardType.CoreType.Land;
|
||||
|
||||
for (final Map.Entry<CardType.CoreType, Integer> entry : typesInDeck.entrySet()) {
|
||||
final CardType.CoreType type = entry.getKey();
|
||||
|
||||
if (max < entry.getValue()) {
|
||||
max = entry.getValue();
|
||||
maxType = type;
|
||||
}
|
||||
}
|
||||
|
||||
final CardType.CoreType determinedMaxType = maxType;
|
||||
CardCollection preferredList = CardLists.filter(fetchList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return card.getType().hasType(determinedMaxType);
|
||||
}
|
||||
});
|
||||
|
||||
return preferredList.isEmpty() ? Aggregates.random(fetchList) : Aggregates.random(preferredList);
|
||||
}
|
||||
|
||||
// Filter by preference. If nothing is preferred, choose the best/worst/random target for the opponent
|
||||
// or for the AI depending on the settings. This logic must choose at least something if at all possible,
|
||||
// since it's called from chooseSingleCard.
|
||||
CardCollection preferredList = CardLists.filter(fetchList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
boolean playerPref = true;
|
||||
if (isCurse) {
|
||||
playerPref = card.getController().isOpponentOf(aiPlayer);
|
||||
} else if (isOwnOnly) {
|
||||
playerPref = card.getController().equals(aiPlayer) || !card.getController().isOpponentOf(aiPlayer);
|
||||
}
|
||||
|
||||
if (!playerPref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return card.isValid(valid, aiPlayer, host, sa); // for things like ExilePreference:Land.Basic
|
||||
}
|
||||
});
|
||||
|
||||
if (!preferredList.isEmpty()) {
|
||||
if (isRandomChoice) {
|
||||
return Aggregates.random(preferredList);
|
||||
}
|
||||
return isWorstChoice ? ComputerUtilCard.getWorstAI(preferredList) : ComputerUtilCard.getBestAI(preferredList);
|
||||
} else {
|
||||
if (isRandomChoice) {
|
||||
return Aggregates.random(preferredList);
|
||||
}
|
||||
return isWorstChoice ? ComputerUtilCard.getWorstAI(fetchList) : ComputerUtilCard.getBestAI(fetchList);
|
||||
}
|
||||
}
|
||||
|
||||
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
|
||||
// Determines if the controller of each potential target can negate the ChangeZone effect
|
||||
// by paying the Unless cost. Returns the list of targets that can be saved that way.
|
||||
|
||||
@@ -4,7 +4,7 @@ Types:Creature Zombie
|
||||
PT:4/4
|
||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard.
|
||||
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigExile | Secondary$ True | TriggerDescription$ When CARDNAME enters the battlefield or dies, exile another card from a graveyard.
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Hidden$ True | RememberChanged$ True | ChangeType$ Card.Other | ChangeNum$ 1 | Mandatory$ True | SubAbility$ DBImmediateTrigger
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Hidden$ True | RememberChanged$ True | ChangeType$ Card.Other | ChangeNum$ 1 | Mandatory$ True | AILogic$ ExilePreference:HighestCMC | SubAbility$ DBImmediateTrigger
|
||||
SVar:DBImmediateTrigger:DB$ ImmediateTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card | Execute$ TrigCharm | TriggerDescription$ When you do, ABILITY
|
||||
SVar:TrigCharm:DB$ Charm | Choices$ DBRemoveCounter,DBPump
|
||||
SVar:DBRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | TgtPrompt$ Select target permanent | CounterType$ Any | CounterNum$ X | SubAbility$ DBCleanup | SpellDescription$ Remove X counters from target permanent, where X is the mana value of the exiled card.
|
||||
|
||||
@@ -4,8 +4,9 @@ Types:Creature Vampire
|
||||
PT:2/1
|
||||
K:First strike
|
||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield, exile a card from a graveyard.
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True | AILogic$ ExilePreference:MostProminentOppType
|
||||
T:Mode$ LandPlayed | ValidCard$ Land.sharesCardTypeWith Imprinted.ExiledWithSource | TriggerZones$ Battlefield | Execute$ TrigDamage | TriggerDescription$ Whenever a player plays a land or casts a spell, if it shares a card type with the exiled card, Cemetery Gatekeeper deals 2 damage to that player.
|
||||
T:Mode$ SpellCast | ValidCard$ Card.sharesCardTypeWith Imprinted.ExiledWithSource | ValidActivatingPlayer$ Player | TriggerZones$ Battlefield | Execute$ TrigDamage | Secondary$ True | TriggerDescription$ Whenever a player plays a land or casts a spell, if it shares a card type with the exiled card, Cemetery Gatekeeper deals 2 damage to that player.
|
||||
SVar:TrigDamage:DB$ DealDamage | NumDmg$ 2 | Defined$ TriggeredCardController
|
||||
SVar:AICastPreference:NeverCastIfLifeBelow$ 8
|
||||
Oracle:First strike\nWhen Cemetery Gatekeeper enters the battlefield, exile a card from a graveyard.\nWhenever a player plays a land or casts a spell, if it shares a card type with the exiled card, Cemetery Gatekeeper deals 2 damage to that player.
|
||||
|
||||
@@ -5,7 +5,7 @@ PT:2/3
|
||||
K:Flying
|
||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield or attacks, exile a card from a graveyard.
|
||||
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigExile | Secondary$ True | TriggerDescription$ Whenever CARDNAME enters the battlefield or attacks, exile a card from a graveyard.
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True | AILogic$ ExilePreference:MostProminentOwnType
|
||||
S:Mode$ Continuous | Affected$ Card.TopLibrary+YouCtrl | AffectedZone$ Library | MayLookAt$ You | Description$ You may look at the top card of your library any time.
|
||||
S:Mode$ Continuous | EffectZone$ Battlefield | Affected$ Card.nonLand+TopLibrary+YouCtrl+sharesCardTypeWith Imprinted.ExiledWithSource | AffectedZone$ Library | MayPlay$ True | MayPlayLimit$ 1 | Description$ Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with CARDNAME.
|
||||
DeckHas:Ability$Graveyard
|
||||
|
||||
@@ -4,7 +4,7 @@ Types:Creature Human Soldier
|
||||
PT:3/4
|
||||
K:Flash
|
||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield, exile a card from a graveyard.
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True | AILogic$ ExilePreference:Land
|
||||
T:Mode$ LandPlayed | ValidCard$ Land.YouOwn+sharesCardTypeWith Imprinted.ExiledWithSource | Execute$ TrigToken | TriggerZones$ Battlefield | TriggerDescription$ Whenever you play a land or cast a spell, if it shares a card type with the exiled card, create a 1/1 white Human creature token.
|
||||
T:Mode$ SpellCast | ValidCard$ Card.sharesCardTypeWith Imprinted.ExiledWithSource | ValidActivatingPlayer$ You | Execute$ TrigToken | TriggerZones$ Battlefield | Secondary$ True | TriggerDescription$ Whenever you play a land or cast a spell, if it shares a card type with the exiled card, create a 1/1 white Human creature token.
|
||||
SVar:TrigToken:DB$ Token | TokenScript$ w_1_1_human
|
||||
|
||||
@@ -5,7 +5,7 @@ PT:3/4
|
||||
K:Vigilance
|
||||
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters the battlefield or attacks, exile a card from a graveyard.
|
||||
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigExile | Secondary$ True | TriggerDescription$ Whenever CARDNAME enters the battlefield or attacks, exile a card from a graveyard.
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True
|
||||
SVar:TrigExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | ChangeType$ Card | ChangeNum$ 1 | SelectPrompt$ Select a card from a graveyard | Mandatory$ True | Hidden$ True | Imprint$ True | AILogic$ ExilePreference:MostProminentNonLandNonExiledOwnType
|
||||
S:Mode$ ReduceCost | ValidCard$ Card | Type$ Spell | Amount$ AffectedX | Activator$ You | Description$ Spells you cast cost {1} less to cast for each card type they share with cards exiled with CARDNAME.
|
||||
SVar:AffectedX:Count$TypesSharedWith Imprinted.ExiledWithSource
|
||||
T:Mode$ ChangesZone | Origin$ Battlefield | ValidCard$ Card.Self | Destination$ Any | Execute$ DBCleanup | Static$ True
|
||||
|
||||
Reference in New Issue
Block a user