- Account for random destruction (Wild Swing)

- Some minor reorganization/refactoring.
This commit is contained in:
Michael Kamensky
2021-10-24 08:27:32 +00:00
parent a509fb643e
commit c75552f54b
3 changed files with 139 additions and 9 deletions

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.Iterator; import java.util.Iterator;
import forge.game.ability.effects.CounterEffect;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -69,6 +70,8 @@ public class CounterAi extends SpellAbilityAi {
|| ai.getAllies().contains(topSA.getActivatingPlayer())) { || ai.getAllies().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness // might as well check for player's friendliness
return false; return false;
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
return false;
} }
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided // check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided

View File

@@ -1,18 +1,12 @@
package forge.game.ability.effects; package forge.game.ability.effects;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.game.Game; import forge.game.Game;
import forge.game.GameLogEntryType; import forge.game.GameLogEntryType;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardZoneTable;
import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -20,8 +14,13 @@ import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.SpellPermanent; import forge.game.spellability.SpellPermanent;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.Zone; import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Localizer; import forge.util.Localizer;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class CounterEffect extends SpellAbilityEffect { public class CounterEffect extends SpellAbilityEffect {
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
@@ -141,6 +140,10 @@ public class CounterEffect extends SpellAbilityEffect {
continue; continue;
} }
if (sa.hasParam("ConditionWouldDestroy") && !checkForConditionWouldDestroy(sa, tgtSA)) {
continue;
}
removeFromStack(tgtSA, sa, si, table); removeFromStack(tgtSA, sa, si, table);
// Destroy Permanent may be able to be turned into a SubAbility // Destroy Permanent may be able to be turned into a SubAbility
@@ -163,6 +166,122 @@ public class CounterEffect extends SpellAbilityEffect {
table.triggerChangesZoneAll(game, sa); table.triggerChangesZoneAll(game, sa);
} // end counterResolve } // end counterResolve
public static boolean checkForConditionWouldDestroy(SpellAbility sa, SpellAbility tgtSA) {
List<SpellAbility> testChain = Lists.newArrayList();
// TODO: add anything that may be important for the test chain here
SpellAbility currentTgtSA = tgtSA;
while (currentTgtSA != null) {
testChain.add(currentTgtSA);
currentTgtSA = currentTgtSA.getSubAbility();
}
for (SpellAbility viableTgtSA : testChain) {
if (checkSingleSAForConditionWouldDestroy(sa, viableTgtSA)) {
return true;
}
}
return false;
}
private static boolean checkSingleSAForConditionWouldDestroy(SpellAbility sa, SpellAbility tgtSA) {
Game game = sa.getHostCard().getGame();
if (tgtSA.getApi() != ApiType.Destroy && tgtSA.getApi() != ApiType.DestroyAll) {
return false;
}
String wouldDestroy = sa.getParam("ConditionWouldDestroy");
CardCollectionView cardsOTB = game.getCardsIn(ZoneType.Battlefield);
// Potential candidates that our condition (ConditionWouldDestroy) is checking for
CardCollection conditionCandidates = CardLists.getValidCards(cardsOTB, wouldDestroy, sa.getActivatingPlayer(), sa.getHostCard(), sa);
// Determine which cards will be affected by the target SA
CardCollection affected = new CardCollection();
if (tgtSA.hasParam("ValidTgts") || tgtSA.hasParam("Defined")) {
affected.addAll(getDefinedCardsOrTargeted(tgtSA));
} else if (tgtSA.hasParam("ValidCards")) {
affected.addAll(CardLists.getValidCards(cardsOTB, tgtSA.getParam("ValidCards"), tgtSA.getActivatingPlayer(), tgtSA.getHostCard(), tgtSA));
}
// Determine which of the condition-specific candidates are potentially affected with the target SA
CardCollection validAffected = new CardCollection();
for (Card cand : conditionCandidates) {
if (affected.contains(cand)) {
validAffected.add(cand);
}
}
// Special case: Wild Swing random destruction - only counter if all targets are valid and each can be destroyed (100% chance
// to destroy one of the owned lands)
// TODO: this is hacky... make the detection of this ability more robust and generic?
boolean isRandomDestruction = false;
if (validAffected.isEmpty() && tgtSA.getRootAbility().getApi() == ApiType.Pump
&& tgtSA.getRootAbility().hasParam("TargetMax")
&& tgtSA.getRootAbility().getSubAbility() != null
&& tgtSA.getRootAbility().getSubAbility().getApi() == ApiType.ChooseCard
&& tgtSA.getRootAbility().getSubAbility().hasParam("AtRandom")
&& "ChosenCard".equals(tgtSA.getParam("Defined"))) {
isRandomDestruction = true;
boolean allValid = true;
affected.addAll(getDefinedCardsOrTargeted(tgtSA.getRootAbility()));
for (Card cand : conditionCandidates) {
if (affected.contains(cand)) {
validAffected.add(cand);
}
}
CardCollectionView rootTgts = tgtSA.getRootAbility().getTargets().getTargetCards();
for (Card rootTgt : rootTgts) {
if (!validAffected.contains(rootTgt)) {
allValid = false;
break;
}
}
if (!allValid) {
return false;
}
}
if (validAffected.isEmpty()) {
return false;
} else if (tgtSA.hasParam("Sacrifice")) {
return false; // Sacrifice doesn't count as Destroy
}
// Dry run Destroy on each validAffected to see if it can be destroyed at this moment
boolean willDestroyCondition = false;
final boolean noRegen = tgtSA.hasParam("NoRegen");
CardZoneTable testTable = new CardZoneTable();
Map<AbilityKey, Object> testParams = AbilityKey.newMap();
testParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
boolean willDestroyAll = true;
for (Card aff : validAffected) {
if (tgtSA.usesTargeting() && !aff.canBeTargetedBy(tgtSA)) {
willDestroyAll = false;
continue; // Should account for Protection/Hexproof/etc.
}
Card toBeDestroyed = CardFactory.copyCard(aff, true);
game.getTriggerHandler().setSuppressAllTriggers(true);
boolean destroyed = game.getAction().destroy(toBeDestroyed, tgtSA, !noRegen, testTable, testParams);
game.getTriggerHandler().setSuppressAllTriggers(false);
if (destroyed) {
willDestroyCondition = true; // this should pick up replacement effects replacing Destroy
if (!isRandomDestruction) {
break;
}
} else {
willDestroyAll = false;
}
}
return isRandomDestruction ? willDestroyAll : willDestroyCondition;
}
/** /**
* <p> * <p>
* removeFromStack. * removeFromStack.

View File

@@ -0,0 +1,8 @@
Name:Equinox
ManaCost:W
Types:Enchantment Aura
K:Enchant land
A:SP$ Attach | Cost$ W | ValidTgts$ Land | TgtPrompt$ Select target land | AILogic$ Pump
S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddAbility$ CounterDestroyLand | Description$ Enchanted land has "{T}: Counter target spell if it would destroy a land you control."
SVar:CounterDestroyLand:AB$ Counter | Cost$ T | TargetType$ Spell | ConditionWouldDestroy$ Land.YouCtrl | TgtPrompt$ Select target spell | ValidTgts$ Card | SpellDescription$ Counter target spell if it would destroy a land you control.
Oracle:Enchant land\nEnchanted land has "{T}: Counter target spell if it would destroy a land you control."