diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index be8b0a8062d..57b67da5c4d 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -81,6 +81,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) { GameRules currentRules = origGame.getRules(); Match newMatch = new Match(currentRules, newPlayers, origGame.getView().getTitle()); Game newGame = new Game(newPlayers, currentRules, newMatch); + newGame.dangerouslySetTimestamp(origGame.getTimestamp()); for (int i = 0; i < origGame.getPlayers().size(); i++) { Player origPlayer = origGame.getPlayers().get(i); diff --git a/forge-game/pom.xml b/forge-game/pom.xml index 1810d23cc36..0afe552afea 100644 --- a/forge-game/pom.xml +++ b/forge-game/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -34,5 +35,10 @@ sentry-logback 7.15.0 + + org.jgrapht + jgrapht-core + 1.5.2 + diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 8453fe16ca3..8e86887e6ef 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -45,6 +45,7 @@ import forge.game.spellability.TargetRestrictions; import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbilityCantAttackBlock; +import forge.game.staticability.StaticAbilityContinuous; import forge.game.staticability.StaticAbilityLayer; import forge.game.trigger.TriggerType; import forge.game.zone.PlayerZone; @@ -56,6 +57,9 @@ import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; import java.util.*; @@ -70,6 +74,9 @@ public class GameAction { private boolean holdCheckingStaticAbilities = false; + private final static Comparator effectOrder = Comparator.comparing(StaticAbility::isCharacteristicDefining).reversed() + .thenComparing(s -> s.getHostCard().getLayerTimestamp()); + public GameAction(Game game0) { game = game0; } @@ -1105,16 +1112,25 @@ public boolean visit(final Card c) { } }, true); - final Comparator comp = (a, b) -> ComparisonChain.start() - .compareTrueFirst(a.isCharacteristicDefining(), b.isCharacteristicDefining()) - .compare(a.getHostCard().getLayerTimestamp(), b.getHostCard().getLayerTimestamp()) - .result(); - staticAbilities.sort(comp); + staticAbilities.sort(effectOrder); final Map affectedPerAbility = Maps.newHashMap(); for (final StaticAbilityLayer layer : StaticAbilityLayer.CONTINUOUS_LAYERS) { List toAdd = Lists.newArrayList(); - for (final StaticAbility stAb : staticAbilities) { + List staticsForLayer = Lists.newArrayList(); + for (StaticAbility stAb : staticAbilities) { + if (stAb.getLayers().contains(layer)) { + staticsForLayer.add(stAb); + } + } + + while (!staticsForLayer.isEmpty()) { + StaticAbility stAb = staticsForLayer.get(0); + // dependency with CDA seems unlikely + if (!stAb.isCharacteristicDefining()) { + stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility); + } + staticsForLayer.remove(stAb); final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb); final CardCollectionView affectedHere; if (previouslyAffected == null) { @@ -1123,6 +1139,9 @@ public boolean visit(final Card c) { affectedPerAbility.put(stAb, affectedHere); } } else { + // 613.6 If an effect starts to apply in one layer and/or sublayer, it will continue to be applied + // to the same set of objects in each other applicable layer and/or sublayer, + // even if the ability generating the effect is removed during this process. affectedHere = previouslyAffected; stAb.applyContinuousAbility(layer, previouslyAffected); } @@ -1136,6 +1155,9 @@ public boolean visit(final Card c) { } } } + // 613.8c. After each effect is applied, the order of remaining effects is reevaluated + // and may change if an effect that has not yet been applied becomes + // dependent on or independent of one or more other effects that have not yet been applied. } staticAbilities.addAll(toAdd); for (Player p : game.getPlayers()) { @@ -1211,6 +1233,115 @@ public boolean visit(final Card c) { game.getTracker().unfreeze(); } + private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List staticsForLayer, CardCollectionView preList, Map affectedPerAbility) { + if (staticsForLayer.size() == 1) { + return staticsForLayer.get(0); + } + if (!StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY.contains(layer)) { + return staticsForLayer.get(0); + } + + DefaultDirectedGraph dependencyGraph = new DefaultDirectedGraph<>(DefaultEdge.class); + + for (StaticAbility stAb : staticsForLayer) { + dependencyGraph.addVertex(stAb); + + boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb); + boolean compareAffected = true; + CardCollectionView affectedHere = affectedPerAbility.get(stAb); + if (affectedHere == null) { + affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList); + } else { + compareAffected = false; + } + List effectResults = generateStaticAbilityResult(layer, stAb); + + for (StaticAbility otherStAb : staticsForLayer) { + if (stAb == otherStAb) { + continue; + } + + boolean removeFull = true; + CardCollectionView affectedOther = affectedPerAbility.get(otherStAb); + if (affectedOther == null) { + affectedOther = otherStAb.applyContinuousAbilityBefore(layer, preList); + if (affectedOther == null) { + // ability was removed + continue; + } + } else { + removeFull = false; + otherStAb.applyContinuousAbility(layer, affectedOther); + } + + // 613.8a. An effect is said to "depend on" another if + // (b) applying the other would change the text or the existence of the first effect... + boolean dependency = exists != stAb.getHostCard().getStaticAbilities().contains(stAb); + // ...what it applies to... + if (!dependency && compareAffected) { + CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList); + if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) { + dependency = true; + } + } + // ...or what it does to any of the things it applies to + if (!dependency) { + List effectResultsAfterOther = generateStaticAbilityResult(layer, stAb); + if (!effectResults.equals(effectResultsAfterOther)) { + dependency = true; + } + } + + if (dependency) { + dependencyGraph.addVertex(otherStAb); + dependencyGraph.addEdge(stAb, otherStAb); + } + + // undo changes and check next pair + game.getStaticEffects().removeStaticEffect(otherStAb, layer, removeFull); + } + // when lucky the effect with the earliest timestamp has no dependency + // then we can safely return it - otherwise we need to build the whole graph + // because it might still be part of a loop + if (dependencyGraph.edgeSet().isEmpty() && stAb == staticsForLayer.get(0)) { + return stAb; + } + } + + // 613.8b. If several dependent effects form a dependency loop, then this rule is ignored + List> cycles = new SzwarcfiterLauerSimpleCycles<>(dependencyGraph).findSimpleCycles(); + for (List cyc : cycles) { + for (int i = 0 ; i < cyc.size() - 1 ; i++) { + dependencyGraph.removeEdge(cyc.get(i), cyc.get(i + 1)); + } + // remove final edge + dependencyGraph.removeEdge(cyc.get(cyc.size() - 1), cyc.get(0)); + } + + // remove all effects that are still dependent on another + Set toRemove = Sets.newHashSet(); + for (StaticAbility stAb : dependencyGraph.vertexSet()) { + if (dependencyGraph.outDegreeOf(stAb) > 0) { + toRemove.add(stAb); + } + } + dependencyGraph.removeAllVertices(toRemove); + + // now the earlist one left is the correct choice + List statics = Lists.newArrayList(dependencyGraph.vertexSet()); + statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp())); + + return statics.get(0); + } + + private List generateStaticAbilityResult(StaticAbilityLayer layer, StaticAbility stAb) { + List results = Lists.newArrayList(); + if (layer == StaticAbilityLayer.CONTROL) { + results.addAll(AbilityUtils.getDefinedPlayers(stAb.getHostCard(), stAb.getParam("GainControl"), stAb)); + } + return results; + } + public final boolean checkStateEffects(final boolean runEvents) { return checkStateEffects(runEvents, Sets.newHashSet()); } diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java index d5890e5ff6c..30715222f57 100644 --- a/forge-game/src/main/java/forge/game/StaticEffect.java +++ b/forge-game/src/main/java/forge/game/StaticEffect.java @@ -28,6 +28,7 @@ import forge.game.card.CardCollectionView; import forge.game.player.Player; import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityLayer; /** *

@@ -52,7 +53,7 @@ public class StaticEffect { } StaticEffect(final StaticAbility ability) { - this(ability.getHostCard()); + this(ability.getHostCard()); this.ability = ability; } @@ -171,128 +172,145 @@ public String getParam(final String key) { * @return a {@link CardCollectionView} of all affected cards. */ final CardCollectionView remove() { + return remove(StaticAbilityLayer.CONTINUOUS_LAYERS); + } + final CardCollectionView remove(List layers) { final CardCollectionView affectedCards = getAffectedCards(); final List affectedPlayers = getAffectedPlayers(); - boolean removeMayPlay = false; - - if (hasParam("MayPlay")) { - removeMayPlay = true; - } - - if (hasParam("IgnoreEffectCost")) { - getSource().removeChangedCardTraits(getTimestamp(), ability.getId()); + if (layers.contains(StaticAbilityLayer.RULES)) { + if (hasParam("IgnoreEffectCost")) { + getSource().removeChangedCardTraits(getTimestamp(), ability.getId()); + } } // modify players for (final Player p : affectedPlayers) { - p.setUnlimitedHandSize(false); - p.setMaxHandSize(p.getStartingHandSize()); - p.removeChangedKeywords(getTimestamp(), ability.getId()); - - p.removeMaxLandPlays(getTimestamp()); - p.removeMaxLandPlaysInfinite(getTimestamp()); + if (layers.contains(StaticAbilityLayer.RULES)) { + p.setUnlimitedHandSize(false); + p.setMaxHandSize(p.getStartingHandSize()); - p.removeControlledWhileSearching(getTimestamp()); - p.removeControlVote(getTimestamp()); - p.removeAdditionalVote(getTimestamp()); - p.removeAdditionalOptionalVote(getTimestamp()); - p.removeAdditionalVillainousChoices(getTimestamp()); + p.removeMaxLandPlays(getTimestamp()); + p.removeMaxLandPlaysInfinite(getTimestamp()); - p.removeDeclaresAttackers(getTimestamp()); - p.removeDeclaresBlockers(getTimestamp()); - } + p.removeControlledWhileSearching(getTimestamp()); + p.removeControlVote(getTimestamp()); + p.removeAdditionalVote(getTimestamp()); + p.removeAdditionalOptionalVote(getTimestamp()); + p.removeAdditionalVillainousChoices(getTimestamp()); - // modify the affected card - for (final Card affectedCard : affectedCards) { - // Gain control - if (hasParam("GainControl")) { - affectedCard.removeTempController(getTimestamp()); + p.removeDeclaresAttackers(getTimestamp()); + p.removeDeclaresBlockers(getTimestamp()); } - // Revert changed color words - affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId()); - - // remove set P/T - if (hasParam("SetPower") || hasParam("SetToughness")) { - affectedCard.removeNewPT(getTimestamp(), ability.getId()); + if (layers.contains(StaticAbilityLayer.ABILITIES)) { + p.removeChangedKeywords(getTimestamp(), ability.getId()); } - // remove P/T bonus - affectedCard.removePTBoost(getTimestamp(), ability.getId()); - - // remove keywords - // (Although nothing uses it at this time) - if (hasParam("AddKeyword") || hasParam("RemoveKeyword") || hasParam("RemoveLandTypes") - || hasParam("ShareRememberedKeywords") || hasParam("RemoveAllAbilities")) { - affectedCard.removeChangedCardKeywords(getTimestamp(), ability.getId(), false); - } - - if (hasParam("CantHaveKeyword")) { - affectedCard.removeCantHaveKeyword(getTimestamp()); - } - - if (hasParam("AddHiddenKeyword")) { - affectedCard.removeHiddenExtrinsicKeywords(timestamp, ability.getId()); - } + } - // remove abilities - if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf") - || hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf") - || hasParam("AddTrigger") || hasParam("AddStaticAbility") - || hasParam("AddReplacementEffects") || hasParam("RemoveAllAbilities") - || hasParam("RemoveLandTypes")) { - affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId()); + // modify the affected card + for (final Card affectedCard : affectedCards) { + if (layers.contains(StaticAbilityLayer.CONTROL)) { + if (hasParam("GainControl")) { + affectedCard.removeTempController(getTimestamp()); + } } - // remove Types - if (hasParam("AddType") || hasParam("AddAllCreatureTypes") || hasParam("RemoveType") || hasParam("RemoveLandTypes")) { - // the view is updated in GameAction#checkStaticAbilities to avoid flickering - affectedCard.removeChangedCardTypes(getTimestamp(), ability.getId(), false); + if (layers.contains(StaticAbilityLayer.TEXT)) { + // Revert changed color words + affectedCard.removeChangedTextColorWord(getTimestamp(), ability.getId()); + + // remove changed name + if (hasParam("SetName") || hasParam("AddNames")) { + affectedCard.removeChangedName(timestamp, ability.getId()); + } + + if (hasParam("GainTextOf")) { + affectedCard.removeChangedName(getTimestamp(), ability.getId()); + affectedCard.removeChangedManaCost(getTimestamp(), ability.getId()); + affectedCard.removeColor(getTimestamp(), ability.getId()); + affectedCard.removeChangedCardTypes(getTimestamp(), ability.getId()); + affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId()); + affectedCard.removeChangedCardKeywords(getTimestamp(), ability.getId()); + affectedCard.removeNewPT(getTimestamp(), ability.getId()); + + affectedCard.updateChangedText(); + } } - // remove colors - if (hasParam("AddColor") || hasParam("SetColor")) { - affectedCard.removeColor(getTimestamp(), ability.getId()); + if (layers.contains(StaticAbilityLayer.TYPE)) { + // remove Types + if (hasParam("AddType") || hasParam("AddAllCreatureTypes") || hasParam("RemoveType") || hasParam("RemoveLandTypes")) { + // the view is updated in GameAction#checkStaticAbilities to avoid flickering + affectedCard.removeChangedCardTypes(getTimestamp(), ability.getId(), false); + } } - // remove changed name - if (hasParam("SetName") || hasParam("AddNames")) { - affectedCard.removeChangedName(timestamp, ability.getId()); + if (layers.contains(StaticAbilityLayer.COLOR)) { + // remove colors + if (hasParam("AddColor") || hasParam("SetColor")) { + affectedCard.removeColor(getTimestamp(), ability.getId()); + } } - // remove may look at - if (hasParam("MayLookAt")) { - affectedCard.removeMayLookAt(getTimestamp()); - } - if (removeMayPlay) { - affectedCard.removeMayPlay(ability); + if (layers.contains(StaticAbilityLayer.ABILITIES)) { + // remove keywords + if (hasParam("AddKeyword") || hasParam("RemoveKeyword") || hasParam("RemoveLandTypes") + || hasParam("ShareRememberedKeywords") || hasParam("RemoveAllAbilities")) { + affectedCard.removeChangedCardKeywords(getTimestamp(), ability.getId(), false); + } + + // remove abilities + if (hasParam("AddAbility") || hasParam("GainsAbilitiesOf") + || hasParam("GainsAbilitiesOfDefined") || hasParam("GainsTriggerAbsOf") + || hasParam("AddTrigger") || hasParam("AddStaticAbility") + || hasParam("AddReplacementEffects") || hasParam("RemoveAllAbilities") + || hasParam("RemoveLandTypes")) { + affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId()); + } + + if (hasParam("CantHaveKeyword")) { + affectedCard.removeCantHaveKeyword(getTimestamp()); + } + + affectedCard.removeChangedSVars(getTimestamp(), ability.getId()); } - if (hasParam("GainTextOf")) { - affectedCard.removeChangedName(getTimestamp(), ability.getId()); - affectedCard.removeChangedManaCost(getTimestamp(), ability.getId()); - affectedCard.removeColor(getTimestamp(), ability.getId()); - affectedCard.removeChangedCardTypes(getTimestamp(), ability.getId()); - affectedCard.removeChangedCardTraits(getTimestamp(), ability.getId()); - affectedCard.removeChangedCardKeywords(getTimestamp(), ability.getId()); - affectedCard.removeNewPT(getTimestamp(), ability.getId()); - - affectedCard.updateChangedText(); + if (layers.contains(StaticAbilityLayer.SETPT)) { + if (hasParam("SetPower") || hasParam("SetToughness")) { + affectedCard.removeNewPT(getTimestamp(), ability.getId()); + } } - if (hasParam("Goad")) { - affectedCard.removeGoad(getTimestamp()); + if (layers.contains(StaticAbilityLayer.MODIFYPT)) { + affectedCard.removePTBoost(getTimestamp(), ability.getId()); } - if (hasParam("CanBlockAny")) { - affectedCard.removeCanBlockAny(getTimestamp()); + if (layers.contains(StaticAbilityLayer.RULES)) { + if (hasParam("AddHiddenKeyword")) { + affectedCard.removeHiddenExtrinsicKeywords(timestamp, ability.getId()); + } + + // remove may look at + if (hasParam("MayLookAt")) { + affectedCard.removeMayLookAt(getTimestamp()); + } + if (hasParam("MayPlay")) { + affectedCard.removeMayPlay(ability); + } + + if (hasParam("Goad")) { + affectedCard.removeGoad(getTimestamp()); + } + + if (hasParam("CanBlockAny")) { + affectedCard.removeCanBlockAny(getTimestamp()); + } + if (hasParam("CanBlockAmount")) { + affectedCard.removeCanBlockAdditional(getTimestamp()); + } } - if (hasParam("CanBlockAmount")) { - affectedCard.removeCanBlockAdditional(getTimestamp()); - } - - affectedCard.removeChangedSVars(getTimestamp(), ability.getId()); affectedCard.updateAbilityTextForView(); // need to update keyword cache for clean reapply } diff --git a/forge-game/src/main/java/forge/game/StaticEffects.java b/forge-game/src/main/java/forge/game/StaticEffects.java index 75cdf8982a0..3f83c56b56f 100644 --- a/forge-game/src/main/java/forge/game/StaticEffects.java +++ b/forge-game/src/main/java/forge/game/StaticEffects.java @@ -20,10 +20,12 @@ import java.util.Map; import java.util.Set; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.game.card.Card; import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityLayer; /** *

@@ -67,12 +69,17 @@ public Iterable getEffects() { return staticEffects.values(); } - public boolean removeStaticEffect(final StaticAbility staticAbility) { - final StaticEffect currentEffect = staticEffects.remove(staticAbility); + public boolean removeStaticEffect(final StaticAbility staticAbility, final StaticAbilityLayer layer, final boolean removeFull) { + final StaticEffect currentEffect; + if (removeFull) { + currentEffect = staticEffects.remove(staticAbility); + } else { + currentEffect = staticEffects.get(staticAbility); + } if (currentEffect == null) { return false; } - currentEffect.remove(); + currentEffect.remove(Lists.newArrayList(layer)); return true; } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index 11a1007f6ef..5a04eeabd10 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -1016,7 +1016,7 @@ private static List getAffectedPlayers(final StaticAbility stAb) { return players; } - private static CardCollectionView getAffectedCards(final StaticAbility stAb, final CardCollectionView preList) { + public static CardCollectionView getAffectedCards(final StaticAbility stAb, final CardCollectionView preList) { final Card hostCard = stAb.getHostCard(); final Game game = hostCard.getGame(); final Player controller = hostCard.getController(); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityLayer.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityLayer.java index 48e9a75e05e..83f762bdac4 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityLayer.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityLayer.java @@ -35,4 +35,6 @@ public enum StaticAbilityLayer { public final static ImmutableList CONTINUOUS_LAYERS = ImmutableList.of(COPY, CONTROL, TEXT, TYPE, COLOR, ABILITIES, CHARACTERISTIC, SETPT, MODIFYPT, RULES); + public final static ImmutableList CONTINUOUS_LAYERS_WITH_DEPENDENCY = + ImmutableList.of(COPY, CONTROL, TEXT, TYPE, ABILITIES, CHARACTERISTIC, SETPT); } diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java index d5cb4cac1d3..9757f7df9ab 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java @@ -2550,6 +2550,66 @@ public void testBasicSpellFizzling() { AssertJUnit.assertEquals(0, game.getPlayers().get(0).getCardsIn(ZoneType.Hand).size()); } + @Test + public void testControlLayerDependency() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + Player opp = game.getPlayers().get(1); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + + Card bear = addCard("Bear Cub", p); + addCards("Island", 6, p); + addCards("Island", 5, opp); + addCard("Vedalken Orrery", opp); + Card control = addCardToZone("Mind Control", opp, ZoneType.Hand); + + GameSimulator sim = createSimulator(game, opp); + game = sim.getSimulatedGameState(); + + SpellAbility controlSA = control.getFirstSpellAbility(); + controlSA.getTargets().add(bear); + sim.simulateSpellAbility(controlSA); + + p = game.getPlayers().get(0); + Card confiscate = addCardToZone("Confiscate", p, ZoneType.Hand); + control = findCardWithName(game, "Mind Control"); + SpellAbility confiscateSA = confiscate.getFirstSpellAbility(); + confiscateSA.getTargets().add(control); + + sim = createSimulator(game, p); + game = sim.getSimulatedGameState(); + bear = findCardWithName(game, "Bear Cub"); + + AssertJUnit.assertTrue(bear.getController().equals(opp)); + sim.simulateSpellAbility(confiscateSA); + AssertJUnit.assertTrue(bear.getController().equals(p)); + } + + @Test + public void testTypeLayerDependency() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + + Card nonBasicForest = addCard("Breeding Pool", p); + addCard("Life and Limb", p); + addCard("Blood Moon", p); + + game.getAction().checkStaticAbilities(); + + // Blood Moon will be applied first because Life and Limb depends on it + AssertJUnit.assertFalse(nonBasicForest.isCreature()); + AssertJUnit.assertTrue(nonBasicForest.getType().hasSubtype("Mountain")); + + // adding Saproling causes dependency loop, so Life and Limb gets applied first instead + addCard("Shroofus Sproutsire", p); + + game.getAction().checkStaticAbilities(); + + AssertJUnit.assertTrue(nonBasicForest.isCreature()); + AssertJUnit.assertTrue(nonBasicForest.getType().hasSubtype("Mountain")); + } + /** * Test for "Volo's Journal" usage by the AI. This test checks if the AI correctly * adds the correct types to the "Volo's Journal" when casting the spells in order