AI framework to improve sacrificing endangered cards + several AI hints (Stoneforge Mystic, Atog, others) and improvements. (#4014)

* - Implement a fallback mechanism in case getting a card by name and edition fails for whatever reason.

* - Patch up pulling a card without filters in All Card Variants mode.

* - Sacrifice creatures when they're endangered (currently works for
AF LifeGain, LifeLose, and any AFs that do not have phase-based AI restrictions or other factors that will prevent instant speed activation)

* - Tweaks to the requirements for the AI.
- Some AI enablement.

* - Account for non-creature endangered objects

* - Flag Wall of Limbs as RemAIDeck for now.

* - Support for AF PutCounter.

* - Clean up.

* - Logic fix for AF PutCounter.

* - Clean up.

* - Logic tweak/fix for AF Pump.

* - Another slight tweak.

* - Some AI hint fixes/additions.

* - Some AI hint fixes/additions.

* - Improve timing for AF LifeGain/LifeLose.

* - AI profile option for default SacCost AI preference.

* - Default Sacrifice AI preference master toggle.

* - Stoneforge Mystic AI hint.

* - For now, keep the default pref SacCost toggle to the Experimental AI and at minimum values (too extreme for general use).

* - AI hint: Cryptbreaker

* - Don't auto-sac creatures that evaluate above a given threshold, sac smaller CMC first

* - Lower the priority of cards that have a self-sacrifice activated ability

* - Revert the evaluation modification until a better solution is found.

* - AI hint for Hallowed Moonlight.

* - AI hint for Winds of Abandon (AI casts the non-overloaded version in Main 1, so cast the other one in Main 1 as well to be able to prioritize/choose)

* - AI logic for The One Ring.

* - Some logic tweaks/fixes.

* - Winds of Abandon: use the AI logic hint like other similar non-permanent spells.

* - Fix logic for default sacrifice priorities.
- Mark P9 Mox, Black Lotus, and Lotus Petal cards as bad for AI sacrifice.

* - More logic fixes.

* - One more logic fix.

* - Revert the AIDontSacrifice hint for now.

* - Revert Tinker as well

* - Limit LifeLoseAi sac logic to threatened cards.

* - Logic tweak.

* - Logic tweak.

* - Simplify check (part already checked above).

* - Some more minor cleanup.

* - AI shouldn't sacrifice things mid-combat in presence of Trample or Banding because of altered combat rules (likely to backfire/result in a misplay)
- Minor cleanup.

* - A [hacky] way to make the AI understand Anticognition and Bring the Ending.

* - Fix imports.

* - Avoid a crash by ensuring that the AI parameter indeed points to an AI player (and not e.g. predicting/simulating human decisions at the moment)

* - Do not try to sacrifice a card in an attempt to regenerate it

* - Clean up for AiController mustRespond

* - Suppress recursive checkSacrificeCost when called from the predictive code.
- Trample only matters for the attacking side when checking for threatened card SacCost requirements

* - Naming convention.

* - NPE guard.

* - Recommended tweaks and fixes.

* - Don't override X payment for a triggered ability (e.g. Spiteful Banditry)

* - A better attempt at handling X inside trigger code.

* - Process AI logic for EffectAi from triggered abilities.

* - Improve Black Lotus AI by handling it as if it were a Mana Ritual card when processing ManaEffectAi.

* - AI property guarded check + meaningful default for potential non-AI calls
This commit is contained in:
Agetian
2023-11-05 22:21:11 +03:00
committed by GitHub
parent baaf8ab4c5
commit 8e0bc63a8b
43 changed files with 344 additions and 91 deletions

View File

@@ -1537,7 +1537,14 @@ public class AiController {
top = game.getStack().peekAbility();
}
final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player);
final boolean mustRespond = top != null && top.hasParam("AIRespondsToOwnAbility");
// Must respond: cases where the AI should respond to its own triggers or other abilities (need to add negative stuff to be countered here)
boolean mustRespond = false;
if (top != null) {
mustRespond = top.hasParam("AIRespondsToOwnAbility"); // Forced combos (currently defined for Sensei's Divining Top)
mustRespond |= top.isTrigger() && top.getTrigger().getKeyword() != null
&& top.getTrigger().getKeyword().getKeyword() == Keyword.EVOKE; // Evoke sacrifice trigger
}
if (topOwnedByAI) {
// AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it

View File

@@ -134,7 +134,12 @@ public enum AiProps { /** */
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"),
BLINK_RELOAD_PLANESWALKER_CHANCE("30"), /** */
BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY("2"), /** */
BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"); /** */
BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"),
SACRIFICE_DEFAULT_PREF_ENABLE("true"),
SACRIFICE_DEFAULT_PREF_MIN_CMC("0"),
SACRIFICE_DEFAULT_PREF_MAX_CMC("2"),
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS("true"),
SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL("135");
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->

View File

@@ -25,6 +25,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import forge.game.cost.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
@@ -65,13 +66,6 @@ import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostExile;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -433,12 +427,41 @@ public class ComputerUtil {
final CardCollection sacMeList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority;
return (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority)
|| (priority == 1 && shouldSacrificeThreatenedCard(ai, c, sa));
}
});
if (!sacMeList.isEmpty()) {
CardLists.shuffle(sacMeList);
return sacMeList.get(0);
return sacMeList.getFirst();
} else {
// empty sacMeList, so get some viable average preference if the option is enabled
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
boolean enableDefaultPref = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
if (enableDefaultPref) {
int minCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
int maxCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
int maxCreatureEval = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
boolean allowTokens = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
List<String> dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
CardCollection allowList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
return false;
}
return (allowTokens && card.isToken())
|| (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
}
});
if (!allowList.isEmpty()) {
CardLists.sortByCmcDesc(allowList);
return allowList.getLast();
}
}
}
}
}
@@ -1435,6 +1458,8 @@ public class ComputerUtil {
if (type.equals("CARDNAME")) {
if (source.getSVar("SacMe").equals("6")) {
return true;
} else if (shouldSacrificeThreatenedCard(ai, source, sa)) {
return true;
}
continue;
}
@@ -1444,6 +1469,8 @@ public class ComputerUtil {
for (Card c : typeList) {
if (c.getSVar("SacMe").equals("6")) {
return true;
} else if (shouldSacrificeThreatenedCard(ai, c, sa)) {
return true;
}
}
}
@@ -1810,6 +1837,13 @@ public class ComputerUtil {
}
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
if (saviour.usesTargeting() && !saviour.canTarget(c)) {
continue;
} else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class)
&& (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) {
continue;
}
boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false);
if ((!topStack.usesTargeting() && !grantIndestructible && !canSave)
|| (!grantIndestructible && !grantShroud && !canSave)) {
@@ -1818,6 +1852,13 @@ public class ComputerUtil {
}
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
if (saviour.usesTargeting() && !saviour.canTarget(c)) {
continue;
} else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class)
&& (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) {
continue;
}
boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false);
if (!canSave) {
continue;
@@ -2038,25 +2079,35 @@ public class ComputerUtil {
* @return true if the creature dies according to current board position.
*/
public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa) {
return predictCreatureWillDieThisTurn(ai, creature, excludeSa, false);
}
public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa, final boolean nonCombatOnly) {
final Game game = ai.getGame();
// a creature will [hopefully] die from a spell on stack
boolean willDieFromSpell = false;
boolean noStackCheck = false;
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility sa = si.getSpellAbility();
if (sa.getApi() == ApiType.Counter) {
noStackCheck = true;
break;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility sa = si.getSpellAbility();
if (sa.getApi() == ApiType.Counter) {
noStackCheck = true;
break;
}
}
}
}
willDieFromSpell = !noStackCheck && predictThreatenedObjects(creature.getController(), excludeSa).contains(creature);
if (nonCombatOnly) {
return willDieFromSpell;
}
// a creature will die as a result of combat
boolean willDieInCombat = !willDieFromSpell && game.getPhaseHandler().inCombat()
&& ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat());
@@ -3257,5 +3308,20 @@ public class ComputerUtil {
List<ReplacementEffect> list = c.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.CantHappen);
return !list.isEmpty();
}
public static boolean shouldSacrificeThreatenedCard(Player ai, Card c, SpellAbility sa) {
if (!ai.getController().isAI()) {
return false; // only makes sense for actual AI decisions
} else if (sa != null && sa.getApi() == ApiType.Regenerate && sa.getHostCard().equals(c)) {
return false; // no use in sacrificing a card in an attempt to regenerate it
}
ComputerUtilCost.setSuppressRecursiveSacCostCheck(true);
Game game = ai.getGame();
Combat combat = game.getCombat();
boolean isThreatened = (c.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, c, sa, false)
&& (!ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, c, combat) && !ComputerUtilCombat.isDangerousToSacInCombat(ai, c, combat)))
|| (!c.isCreature() && ComputerUtil.predictThreatenedObjects(ai, sa).contains(c));
ComputerUtilCost.setSuppressRecursiveSacCostCheck(false);
return isThreatened;
}
}

View File

@@ -2583,4 +2583,43 @@ public class ComputerUtilCombat {
}
return totalLifeLinkDamage;
}
public static boolean willOpposingCreatureDieInCombat(final Player ai, final Card combatant, final Combat combat) {
if (combat != null) {
if (combat.isBlocking(combatant)) {
for (Card atk : combat.getAttackersBlockedBy(combatant)) {
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, atk, combat)) {
return true;
}
}
} else if (combat.isBlocked(combatant)) {
for (Card blk : combat.getBlockers(combatant)) {
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, blk, combat)) {
return true;
}
}
}
}
return false;
}
public static boolean isDangerousToSacInCombat(final Player ai, final Card combatant, final Combat combat) {
if (combat != null) {
if (combat.isBlocking(combatant)) {
if (combatant.hasKeyword(Keyword.BANDING)) {
return true;
}
for (Card atk : combat.getAttackersBlockedBy(combatant)) {
if (atk.hasKeyword(Keyword.TRAMPLE)) {
return true;
}
}
} else if (combat.isBlocked(combatant)) {
if (combatant.hasKeyword(Keyword.BANDING)) {
return true;
}
}
}
return false;
}
}

View File

@@ -35,6 +35,10 @@ import java.util.Set;
public class ComputerUtilCost {
private static boolean suppressRecursiveSacCostCheck = false;
public static void setSuppressRecursiveSacCostCheck(boolean shouldSuppress) {
suppressRecursiveSacCostCheck = shouldSuppress;
}
/**
* Check add m1 m1 counter cost.
@@ -344,6 +348,10 @@ public class ComputerUtilCost {
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if (suppressRecursiveSacCostCheck) {
return false;
}
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
@@ -358,11 +366,13 @@ public class ComputerUtilCost {
}
if (source.isCreature()) {
// e.g. Sakura Tribe-Elder
final Combat combat = ai.getGame().getCombat();
final boolean beforeNextTurn = ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn().equals(ai);
final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility);
final int lifeThreshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility, false)
&& !ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, source, combat);
final int lifeThreshold = ai.getController().isAI() ? (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)) : 4;
final boolean aiInDanger = ai.getLife() <= lifeThreshold && ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife();
if (creatureInDanger) {
if (creatureInDanger && !ComputerUtilCombat.isDangerousToSacInCombat(ai, source, combat)) {
return true;
} else if (aiInDanger || !beforeNextTurn) {
return false;

View File

@@ -1610,6 +1610,22 @@ public class SpecialCardAi {
}
}
// The One Ring
public static class TheOneRing {
public static boolean consider(final Player ai, final SpellAbility sa) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
return true;
}
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
}
}
// The Scarab God
public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) {

View File

@@ -1,13 +1,11 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -15,6 +13,9 @@ import forge.game.combat.Combat;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.util.Expressions;
import java.util.Map;
public class BranchAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -25,6 +26,37 @@ public class BranchAi extends SpellAbilityAi {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GrislySigil".equals(aiLogic)) {
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
} else if ("BranchCounter".equals(aiLogic)) {
// TODO: this might need expanding/tweaking if more cards are added with different SA setups
SpellAbility top = ComputerUtilAbility.getTopSpellAbilityOnStack(aiPlayer.getGame(), sa);
if (top == null || !sa.canTarget(top)) {
return false;
}
Card host = sa.getHostCard();
// pre-target the object to calculate the branch condition SVar, then clean up before running the real check
sa.getTargets().add(top);
int value = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("BranchConditionSVar"), sa);
sa.resetTargets();
String branchCompare = sa.getParamOrDefault("BranchConditionSVarCompare", "GE1");
String operator = branchCompare.substring(0, 2);
String operand = branchCompare.substring(2);
final int operandValue = AbilityUtils.calculateAmount(host, operand, sa);
boolean conditionMet = Expressions.compare(value, operator, operandValue);
SpellAbility falseSub = sa.getAdditionalAbility("FalseSubAbility"); // this ability has the UnlessCost part
boolean willPlay = false;
if (!conditionMet && falseSub.hasParam("UnlessCost")) {
// FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
// This is hacky, but it works. Perhaps a cleaner way exists?
sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(aiPlayer, sa);
sa.getMapParams().remove("UnlessCost");
} else {
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(aiPlayer, sa);
}
return willPlay;
} else if ("TgtAttacker".equals(aiLogic)) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null || combat.getAttackingPlayer() != aiPlayer) {

View File

@@ -768,6 +768,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
return true;
}
if (sa.isHidden()) {

View File

@@ -44,6 +44,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
final Game game = ai.getGame();
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
final String aiLogic = sa.getParamOrDefault("AILogic" ,"");
if (abCost != null) {
// AI currently disabled for these costs
@@ -52,7 +53,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
return false;
@@ -86,16 +87,16 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
if ("LivingDeath".equals(aiLogic)) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa);
} else if ("Timetwister".equals(sa.getParam("AILogic"))) {
} else if ("Timetwister".equals(aiLogic)) {
// Timetwister AI
return SpecialCardAi.Timetwister.consider(ai, sa);
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
// e.g. Shadow of the Grave
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
} else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
} else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
@@ -105,7 +106,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
}
return false;
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
} else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
PlayerCollection players = ai.getOpponents();
players.add(ai);
int maxSize = 1;
@@ -217,7 +218,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
// Don't cast during main1?
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai)) {
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
return false;
}
} else if (origin.equals(ZoneType.Graveyard)) {
@@ -245,15 +246,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
&& !ComputerUtil.isPlayingReanimator(ai);
}
} else if (origin.equals(ZoneType.Exile)) {
String logic = sa.getParam("AILogic");
if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) {
if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
boolean noDiscard = logic.contains(".noDiscard");
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
boolean noDiscard = aiLogic.contains(".noDiscard");
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {

View File

@@ -308,8 +308,10 @@ public class CountersPutAi extends CountersAi {
} else if (logic.equals("ChargeToBestCMC")) {
return doChargeToCMCLogic(ai, sa);
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
return doChargeToOppCtrlCMCLogic(ai, sa);
}
return doChargeToOppCtrlCMCLogic(ai, sa);
} else if (logic.equals("TheOneRing")) {
return SpecialCardAi.TheOneRing.consider(ai, sa);
}
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false;
@@ -440,6 +442,9 @@ public class CountersPutAi extends CountersAi {
}
}
final boolean hasSacCost = abCost.hasSpecificCostType(CostSacrifice.class);
final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
if (sa.usesTargeting()) {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
@@ -453,15 +458,15 @@ public class CountersPutAi extends CountersAi {
sa.addDividedAllocation(c, amount);
return true;
} else {
return false;
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return false;
}
}
}
}
sa.resetTargets();
final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
@@ -474,6 +479,8 @@ public class CountersPutAi extends CountersAi {
// don't put the counter on the dead creature
if (sacSelf && c.equals(source)) {
return false;
} else if (hasSacCost && !ComputerUtil.shouldSacrificeThreatenedCard(ai, c, sa)) {
return false;
}
if ("NoCounterOfType".equals(sa.getParam("AILogic"))) {
for (String ctrType : types) {
@@ -628,7 +635,7 @@ public class CountersPutAi extends CountersAi {
}
// Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
if (!(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable
}
}

View File

@@ -274,7 +274,8 @@ public class DamageAllAi extends SpellAbilityAi {
final String damage = sa.getParam("NumDmg");
int dmg;
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")
&& sa.getPayCosts() != null && sa.getPayCosts().hasXInAnyCostPart()) {
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai, true);
sa.setXManaCostPaid(dmg);

View File

@@ -752,7 +752,7 @@ public class DamageDealAi extends DamageAiBase {
int minTgts = tgt.getMinTargets(source, sa);
if (tcs.size() < minTgts || tcs.size() == 0) {
if (mandatory) {
// Sanity check: if there are any legal non-owned targets after the check (which may happen for complex cards like Searing Blaze),
// Sanity check: if there are any legal non-owned targets after the check (which may happen for complex cards like Rift Bolt),
// choose a random opponent's target before forcing targeting of own stuff
List<GameEntity> allTgtEntities = sa.getTargetRestrictions().getAllCandidates(sa, true);
for (GameEntity ent : allTgtEntities) {

View File

@@ -35,11 +35,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayLife;
import forge.game.cost.PaymentDecision;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -148,9 +144,15 @@ public class DrawAi extends SpellAbilityAi {
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
}
// Sacrificing a creature in response to something dangerous is generally good in any phase
boolean isSacCost = false;
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
isSacCost = true;
}
// Don't use draw abilities before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
&& !ComputerUtil.castSpellInMain1(ai, sa) && !isSacCost) {
return false;
}

View File

@@ -396,10 +396,17 @@ public class EffectAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (sa.hasParam("AILogic")) {
if (canPlayAI(aiPlayer, sa)) {
return true; // if false, fall through further to do the mandatory stuff
}
}
// E.g. Nova Pentacle
if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer()) {
// try to target the opponent's best targetable permanent, if able
CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
oppPerms = CardLists.filter(oppPerms, card -> sa.canTarget(card));
if (!oppPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
@@ -409,6 +416,7 @@ public class EffectAi extends SpellAbilityAi {
if (mandatory) {
// try to target the AI's worst targetable permanent, if able
CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
aiPerms = CardLists.filter(aiPerms, card -> sa.canTarget(card));
if (!aiPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));

View File

@@ -81,6 +81,7 @@ public class LifeGainAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Game game = ai.getGame();
final int life = ai.getLife();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
boolean lifeCritical = life <= 5;
@@ -103,9 +104,15 @@ public class LifeGainAi extends SpellAbilityAi {
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; }
}
// Sacrificing in response to something dangerous is generally good in any phase
boolean isSacCost = false;
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
isSacCost = true;
}
// Don't use lifegain before main 2 if possible
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
&& !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false;
}
@@ -124,6 +131,7 @@ public class LifeGainAi extends SpellAbilityAi {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", "");
final int life = ai.getLife();
final String amountStr = sa.getParam("LifeAmount");
@@ -185,7 +193,11 @@ public class LifeGainAi extends SpellAbilityAi {
|| sa.getSubAbility() != null || playReusable(ai, sa)) {
return true;
}
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
return true; // sac costs should be performed at Instant speed when able
}
// Save instant-speed life-gain unless it is really worth it
final float value = 0.9f * lifeAmount / life;
if (value < 0.2f) {

View File

@@ -10,6 +10,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
@@ -96,6 +97,7 @@ public class LifeLoseAi extends SpellAbilityAi {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount");
final String aiLogic = sa.getParamOrDefault("AILogic", "");
int amount = 0;
if (sa.usesTargeting()) {
@@ -133,9 +135,15 @@ public class LifeLoseAi extends SpellAbilityAi {
return true;
}
// Sacrificing a creature in response to something dangerous is generally good in any phase
boolean isSacCost = false;
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
isSacCost = true;
}
// Don't use loselife before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa) && !"AnyPhase".equals(sa.getParam("AILogic"))) {
&& !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false;
}

View File

@@ -6,14 +6,7 @@ import java.util.List;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
@@ -50,7 +43,7 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
if (aiLogic.startsWith("ManaRitual")) {
if (aiLogic.startsWith("ManaRitual") || aiLogic.startsWith("BlackLotus")) {
return doManaRitualLogic(ai, sa, false);
} else if ("Always".equals(aiLogic)) {
return true;

View File

@@ -299,3 +299,16 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50
# attempt this either in its upkeep or its draw phase or main 1).
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=15
# Master toggle for the following options setting the default AIPreference:SacCost handling.
SACRIFICE_DEFAULT_PREF_ENABLE=false
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum
SACRIFICE_DEFAULT_PREF_MIN_CMC=0
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum
SACRIFICE_DEFAULT_PREF_MAX_CMC=1
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card is a token
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
# A creature should evaluate to no more than this much to be considered for default SacCost preference
SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135

View File

@@ -299,3 +299,17 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50
# The chance that the AI will activate Jhoira's copy random instant ability (per phase, the AI will generally
# attempt this either in its upkeep or its draw phase or main 1).
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20
# Master toggle for the following options setting the default AIPreference:SacCost handling.
SACRIFICE_DEFAULT_PREF_ENABLE=false
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum
SACRIFICE_DEFAULT_PREF_MIN_CMC=0
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum
SACRIFICE_DEFAULT_PREF_MAX_CMC=2
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card is a token
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
# A creature should evaluate to no more than this much to be considered for default SacCost preference
SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135

View File

@@ -300,8 +300,22 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50
# attempt this either in its upkeep or its draw phase or main 1).
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20
# Master toggle for the following options setting the default AIPreference:SacCost handling.
SACRIFICE_DEFAULT_PREF_ENABLE=true
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum
SACRIFICE_DEFAULT_PREF_MIN_CMC=0
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum
SACRIFICE_DEFAULT_PREF_MAX_CMC=1
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card is a token
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
# A creature should evaluate to no more than this much to be considered for default SacCost preference
SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135
# -- Experimental feature toggles which only exist until the testing procedure for the relevant --
# -- features is over. These toggles will be removed later, or may be reintroduced under a --
# -- different name if necessary --
# <-- there are no experimental options here at the moment -->
# <-- there are no experimental options here at the moment -->

View File

@@ -300,3 +300,16 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50
# attempt this either in its upkeep or its draw phase or main 1).
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20
# Master toggle for the following options setting the default AIPreference:SacCost handling.
SACRIFICE_DEFAULT_PREF_ENABLE=false
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum
SACRIFICE_DEFAULT_PREF_MIN_CMC=0
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum
SACRIFICE_DEFAULT_PREF_MAX_CMC=3
# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still
# consider the sacrifice of a matching card is a token
SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true
# A creature should evaluate to no more than this much to be considered for default SacCost preference
SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135

View File

@@ -6,6 +6,5 @@ A:SP$ Attach | Cost$ 2 W | ValidTgts$ Land | AILogic$ Pump
S:Mode$ Continuous | Affected$ Land.AttachedBy | AddAbility$ GainLife | AddSVar$ AnimalBoneyardX | Description$ Enchanted land has "{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness."
SVar:GainLife:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ AnimalBoneyardX | SpellDescription$ You gain life equal to the sacrificed creature's toughness.
SVar:AnimalBoneyardX:Sacrificed$CardToughness
AI:RemoveDeck:All
SVar:NonStackingAttachEffect:True
Oracle:Enchant land\nEnchanted land has "{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness."

View File

@@ -1,8 +1,7 @@
Name:Anticognition
ManaCost:1 U
Types:Instant
A:SP$ Pump | Cost$ 1 U | IsCurse$ True | TargetType$ Spell | TgtZone$ Stack | TgtPrompt$ Select target creature or planeswalker spell | ValidTgts$ Creature,Planeswalker | SubAbility$ DBBranch | StackDescription$ SpellDescription | SpellDescription$ Counter target creature or planeswalker spell unless its controller pays {2}. If an opponent has eight or more cards in their graveyard, instead counter that spell, then scry 2.
SVar:DBBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE8 | TrueSubAbility$ CounterScry | FalseSubAbility$ CounterUnless | StackDescription$ None
A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Creature,Planeswalker | BranchConditionSVarCompare$ GE8 | TrueSubAbility$ CounterScry | FalseSubAbility$ CounterUnless | AILogic$ BranchCounter | SpellDescription$ Counter target creature or planeswalker spell unless its controller pays {2}. If an opponent has eight or more cards in their graveyard, instead counter that spell, then scry 2.
SVar:CounterUnless:DB$ Counter | Defined$ Targeted | UnlessCost$ 2
SVar:CounterScry:DB$ Counter | Defined$ Targeted | SubAbility$ DBScry
SVar:DBScry:DB$ Scry | ScryNum$ 2

View File

@@ -4,7 +4,7 @@ Types:Legendary Enchantment
A:AB$ Draw | Cost$ 1 B PayLife<2> | SpellDescription$ Draw a card.
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ DBTransform | LifeTotal$ You | LifeAmount$ LE5 | TriggerDescription$ At the beginning of your upkeep, if you have 5 or less life, you may transform CARDNAME.
SVar:DBTransform:DB$ SetState | Defined$ Self | Mode$ Transform
AI:RemoveDeck:All
AI:RemoveDeck:Random
AlternateMode:DoubleFaced
Oracle:{1}{B}, Pay 2 life: Draw a card.\nAt the beginning of your upkeep, if you have 5 or less life, you may transform Arguel's Blood Fast.

View File

@@ -5,4 +5,5 @@ PT:1/2
A:AB$ Pump | Cost$ Sac<1/Artifact> | Defined$ Self | NumAtt$ 2 | NumDef$ 2 | SpellDescription$ CARDNAME gets +2/+2 until end of turn.
DeckNeeds:Type$Artifact
DeckHas:Ability$Sacrifice
SVar:AIPreference:SacCost$Artifact.token,Artifact.cmcEQ0+nonLegendary+notnamedBlack Lotus,Artifact.cmcEQ1,Artifact.cmcEQ2,Artifact.cmcEQ3
Oracle:Sacrifice an artifact: Atog gets +2/+2 until end of turn.

View File

@@ -2,6 +2,6 @@ Name:Bone Splinters
ManaCost:B
Types:Sorcery
A:SP$ Destroy | Cost$ B Sac<1/Creature> | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Destroy target creature.
SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2
SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2
AI:RemoveDeck:Random
Oracle:As an additional cost to cast this spell, sacrifice a creature.\nDestroy target creature.

View File

@@ -1,7 +1,7 @@
Name:Bring the Ending
ManaCost:1 U
Types:Instant
A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Card | BranchConditionSVarCompare$ GE3 | TrueSubAbility$ Counter | FalseSubAbility$ CounterUnless | SpellDescription$ Counter target spell unless its controller pays {2}. Corrupted — Counter that spell instead if its controller has three or more poison counters.
A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Card | BranchConditionSVarCompare$ GE3 | TrueSubAbility$ Counter | FalseSubAbility$ CounterUnless | AILogic$ BranchCounter | SpellDescription$ Counter target spell unless its controller pays {2}. Corrupted — Counter that spell instead if its controller has three or more poison counters.
SVar:CounterUnless:DB$ Counter | Defined$ Targeted | UnlessCost$ 2
SVar:Counter:DB$ Counter | Defined$ Targeted
SVar:X:TargetedController$PoisonCounters

View File

@@ -3,6 +3,6 @@ ManaCost:4 B B
Types:Creature Phyrexian Horror
PT:6/3
A:AB$ Regenerate | Cost$ B Sac<1/Creature> | SpellDescription$ Regenerate CARDNAME.
SVar:AIPreferences:SacCost$Creature.token,Creature.cmcLE5+powerLE3+toughnessLE4
SVar:AIPreference:SacCost$Creature.token,Creature.cmcLE5+powerLE3+toughnessLE4
AI:RemoveDeck:Random
Oracle:{B}, Sacrifice a creature: Regenerate Corrupted Harvester.

View File

@@ -3,7 +3,7 @@ ManaCost:B
Types:Creature Zombie
PT:1/1
A:AB$ Token | Cost$ 1 B T Discard<1/Card> | TokenAmount$ 1 | TokenScript$ b_2_2_zombie | TokenOwner$ You | SpellDescription$ Create a 2/2 black Zombie creature token.
A:AB$ Draw | Cost$ tapXType<3/Zombie> | NumCards$ 1 | SpellDescription$ You draw a card and you lose 1 life. | SubAbility$ DBLoseLife
A:AB$ Draw | Cost$ tapXType<3/Zombie> | NumCards$ 1 | AILogic$ AtOppEOT | SpellDescription$ You draw a card and you lose 1 life. | SubAbility$ DBLoseLife
SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1
SVar:AIPreference:DiscardCost$Card
DeckNeeds:Type$Zombie

View File

@@ -4,5 +4,4 @@ Types:Land
A:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness.
SVar:X:Sacrificed$CardToughness
DeckHas:Ability$Sacrifice
AI:RemoveDeck:All
Oracle:{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness.

View File

@@ -4,5 +4,4 @@ Types:Creature Human Cleric
PT:1/1
A:AB$ GainLife | Cost$ 1 Sac<1/Creature> | Defined$ You | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness.
SVar:X:Sacrificed$CardToughness
AI:RemoveDeck:All
Oracle:{1}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness.

View File

@@ -1,9 +1,8 @@
Name:Hallowed Moonlight
ManaCost:1 W
Types:Instant
A:SP$ Effect | Cost$ 1 W | ReplacementEffects$ ReplaceExile | SubAbility$ DBDraw | SpellDescription$ Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead. Draw a card.
A:SP$ Effect | Cost$ 1 W | ReplacementEffects$ ReplaceExile | SubAbility$ DBDraw | AILogic$ NonCastCreature | SpellDescription$ Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead. Draw a card.
SVar:ReplaceExile:Event$ Moved | ActiveZones$ Command | Destination$ Battlefield | ValidCard$ Creature.wasNotCast | ReplaceWith$ DBExile | Description$ If a creature would enter the battlefield and it wasn't cast, exile it instead.
SVar:DBExile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard
SVar:DBDraw:DB$ Draw | NumCards$ 1
AI:RemoveDeck:All
Oracle:Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead.\nDraw a card.

View File

@@ -3,5 +3,4 @@ ManaCost:no cost
Types:Land
A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}.
A:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ 1 | SpellDescription$ You gain 1 life.
AI:RemoveDeck:All
Oracle:{T}: Add {C}.\n{T}, Sacrifice a creature: You gain 1 life.

View File

@@ -3,6 +3,6 @@ ManaCost:3 R R
Types:Creature Cyclops
PT:5/4
A:AB$ DealDamage | Cost$ 1 Sac<1/Creature.Other/another creature> | ValidTgts$ Any | NumDmg$ 1 | SpellDescription$ CARDNAME deals 1 damage to any target.
SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2
SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2
AI:RemoveDeck:Random
Oracle:{1}, Sacrifice another creature: Hurler Cyclops deals 1 damage to any target.

View File

@@ -6,5 +6,4 @@ K:Defender
A:AB$ GainLife | Cost$ 1 G Sac<1/Creature.Other/another creature> | LifeAmount$ X | SubAbility$ DBCleanup | SpellDescription$ You gain life equal to the sacrificed creature's toughness.
SVar:X:Sacrificed$CardToughness
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
Oracle:Defender\n{1}{G}, Sacrifice another creature: You gain life equal to the sacrificed creature's toughness.

View File

@@ -4,5 +4,4 @@ Types:Legendary Land
A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}.
A:AB$ GainLife | Cost$ 3 T Sac<1/Creature> | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness.
SVar:X:Sacrificed$CardToughness
AI:RemoveDeck:All
Oracle:{T}: Add {C}.\n{3}, {T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness.

View File

@@ -3,5 +3,5 @@ ManaCost:B
Types:Sorcery
S:Mode$ Continuous | CharacteristicDefining$ True | AddKeyword$ Flash | IsPresent$ Permanent.YouCtrl+hasKeywordFlash | Description$ CARDNAME has flash as long as you control a permanent with flash.
A:SP$ Destroy | Cost$ B Sac<1/Creature> | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Destroy target creature.
SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2
SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2
Oracle:This spell has flash as long as you control a permanent with flash.\nAs an additional cost to cast this spell, sacrifice a creature.\nDestroy target creature.

View File

@@ -6,7 +6,7 @@ T:Mode$ ChangesZone | ValidCard$ Card.wasCastByYou+Self | Destination$ Battlefie
SVar:TrigPump:DB$ Pump | Defined$ You | Duration$ UntilYourNextTurn | KW$ Protection from everything
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigLoseLife | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, you lose 1 life for each burden counter on CARDNAME.
SVar:TrigLoseLife:DB$ LoseLife | LifeAmount$ X
A:AB$ PutCounter | Cost$ 1 T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME.
A:AB$ PutCounter | Cost$ 1 T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | AILogic$ TheOneRing | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME.
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X
SVar:X:Count$CardCounters.BURDEN
Oracle:Indestructible\nWhen The One Ring enters the battlefield, if you cast it, you gain protection from everything until your next turn.\nAt the beginning of your upkeep, you lose 1 life for each burden counter on The One Ring.\n{1}, {T}: Put a burden counter on The One Ring, then draw a card for each burden counter on The One Ring.

View File

@@ -4,5 +4,5 @@ Types:Creature Kor Artificer
PT:1/2
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters the battlefield, you may search your library for an Equipment card, reveal it, put it into your hand, then shuffle.
SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Card.Equipment | ChangeNum$ 1 | ShuffleNonMandatory$ True
A:AB$ ChangeZone | Cost$ 1 W T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Equipment | ChangeNum$ 1 | SpellDescription$ You may put an Equipment card from your hand onto the battlefield.
A:AB$ ChangeZone | Cost$ 1 W T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Equipment | ChangeNum$ 1 | AILogic$ Main1 | SpellDescription$ You may put an Equipment card from your hand onto the battlefield.
Oracle:When Stoneforge Mystic enters the battlefield, you may search your library for an Equipment card, reveal it, put it into your hand, then shuffle.\n{1}{W}, {T}: You may put an Equipment card from your hand onto the battlefield.

View File

@@ -6,7 +6,7 @@ T:Mode$ ChangesZone | ValidCard$ Card.wasCastByYou+Self | Destination$ Battlefie
SVar:TrigPump:DB$ Pump | Defined$ You | Duration$ UntilYourNextTurn | KW$ Protection from everything
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigLoseLife | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, you lose 1 life for each burden counter on CARDNAME.
SVar:TrigLoseLife:DB$ LoseLife | LifeAmount$ X
A:AB$ PutCounter | Cost$ T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME.
A:AB$ PutCounter | Cost$ T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | AILogic$ TheOneRing | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME.
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X
SVar:X:Count$CardCounters.BURDEN
Oracle:Indestructible\nWhen The One Ring enters the battlefield, if you cast it, you gain protection from everything until your next turn.\nAt the beginning of your upkeep, you lose 1 life for each burden counter on The One Ring.\n{T}: Put a burden counter on The One Ring, then draw a card for each burden counter on The One Ring.

View File

@@ -4,5 +4,4 @@ Types:Enchantment
A:AB$ GainLife | Cost$ 1 B Sac<1/Creature> | Defined$ You | LifeAmount$ 1 | SubAbility$ DBDraw | SpellDescription$ You gain 1 life and draw a card.
SVar:DBDraw:DB$ Draw | NumCards$ 1
SVar:NonStackingEffect:True
AI:RemoveDeck:All
Oracle:{1}{B}, Sacrifice a creature: You gain 1 life and draw a card.

View File

@@ -7,6 +7,6 @@ T:Mode$ LifeGained | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ Tri
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1
A:AB$ LoseLife | Cost$ 5 B B Sac<1/CARDNAME> | LifeAmount$ X | ValidTgts$ Player | TgtPrompt$ Select a player | SpellDescription$ Target player loses X life, where X is CARDNAME's power.
SVar:X:Sacrificed$CardPower
AI:RemoveDeck:All
AI:RemoveDeck:Random
AI:RemoveDeck:All
Oracle:Defender (This creature can't attack.)\nWhenever you gain life, put a +1/+1 counter on Wall of Limbs.\n{5}{B}{B}, Sacrifice Wall of Limbs: Target player loses X life, where X is Wall of Limbs's power.

View File

@@ -2,7 +2,7 @@ Name:Winds of Abandon
ManaCost:1 W
Types:Sorcery
A:SP$ ChangeZone | Cost$ 1 W | Origin$ Battlefield | Destination$ Exile | ValidTgts$ Creature.YouDontCtrl | TgtPrompt$ Select target creature you don't control | SubAbility$ DBGetLandsAll | RememberLKI$ True | SpellDescription$ Exile target creature you don't control. For each creature exiled this way, its controller searches their library for a basic land card. Those players put those cards onto the battlefield tapped, then shuffle.
A:SP$ ChangeZoneAll | Cost$ 4 W W | ChangeType$ Creature.YouDontCtrl | Origin$ Battlefield | Destination$ Exile | RememberLKI$ True | SubAbility$ DBGetLandsAll | PrecostDesc$ Overload | CostDesc$ {4}{W}{W} | NonBasicSpell$ True | SpellDescription$ (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of "target" with "each.")
A:SP$ ChangeZoneAll | Cost$ 4 W W | ChangeType$ Creature.YouDontCtrl | Origin$ Battlefield | Destination$ Exile | RememberLKI$ True | SubAbility$ DBGetLandsAll | PrecostDesc$ Overload | CostDesc$ {4}{W}{W} | NonBasicSpell$ True | AILogic$ Main1 | SpellDescription$ (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of "target" with "each.")
SVar:DBGetLandsAll:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBGetLandsOne | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:DBGetLandsOne:DB$ ChangeZone | Optional$ True | Origin$ Library | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.Basic | ChangeNum$ X | DefinedPlayer$ Player.IsRemembered | ShuffleNonMandatory$ False | ConditionCheckSVar$ X | ConditionSVarCompare$ GE1