diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index d3fc038f720..c2528aaa343 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -898,7 +898,7 @@ public class ComputerPlayer6 extends ComputerPlayer { return; } - CombatUtil.sortByPower(attackers, false); + CombatUtil.sortByPower(attackers, false); // most powerfull go to first CombatInfo combatInfo = CombatUtil.blockWithGoodTrade2(game, attackers, possibleBlockers); Player player = game.getPlayer(playerId); @@ -909,6 +909,7 @@ public class ComputerPlayer6 extends ComputerPlayer { List blockers = entry.getValue(); if (blockers != null) { for (Permanent blocker : blockers) { + // TODO: buggy or miss on multi blocker requirements?! player.declareBlocker(player.getId(), blocker.getId(), attackerId, game); blocked = true; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java index 2859a047ca3..5a3f5bc8d98 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java @@ -40,7 +40,7 @@ public final class CombatUtil { } } - sortByPower(blockableAttackers, true); + sortByPower(blockableAttackers, false); // most powerfull go to first // imagine that most powerful will be blocked as 1-vs-1 List attackersThatWontBeBlocked = new ArrayList<>(blockableAttackers); @@ -83,28 +83,17 @@ public final class CombatUtil { } public static void sortByPower(List permanents, final boolean ascending) { - Collections.sort(permanents, new Comparator() { - @Override - public int compare(Permanent o1, Permanent o2) { - if (ascending) { - return o2.getPower().getValue() - o1.getPower().getValue(); - } else { - return o1.getPower().getValue() - o2.getPower().getValue(); - } - } - }); + permanents.sort(Comparator.comparingInt(p -> p.getPower().getValue())); + if (!ascending) { + Collections.reverse(permanents); + } } public static Permanent getWorstCreature(List creatures) { if (creatures.isEmpty()) { return null; } - Collections.sort(creatures, new Comparator() { - @Override - public int compare(Permanent o1, Permanent o2) { - return o2.getPower().getValue() - o1.getPower().getValue(); - } - }); + creatures.sort(Comparator.comparingInt(p -> p.getPower().getValue())); return creatures.get(0); } diff --git a/Mage.Sets/src/mage/cards/b/BeckoningWillOWisp.java b/Mage.Sets/src/mage/cards/b/BeckoningWillOWisp.java index 73fbd1b422b..363b90e2a52 100644 --- a/Mage.Sets/src/mage/cards/b/BeckoningWillOWisp.java +++ b/Mage.Sets/src/mage/cards/b/BeckoningWillOWisp.java @@ -70,7 +70,7 @@ enum BeckoningWillOWispPredicate implements ObjectSourcePlayerPredicate input, Game game) { UUID playerId = (UUID) game.getState().getValue(input.getSourceId() + "_" + game.getState().getZoneChangeCounter(input.getSourceId()) + "_chosenOpponent"); - return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game)); + return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game, false)); } } diff --git a/Mage.Sets/src/mage/cards/d/DefiantVanguard.java b/Mage.Sets/src/mage/cards/d/DefiantVanguard.java index fa476e12d76..3a46e41e60f 100644 --- a/Mage.Sets/src/mage/cards/d/DefiantVanguard.java +++ b/Mage.Sets/src/mage/cards/d/DefiantVanguard.java @@ -1,7 +1,6 @@ package mage.cards.d; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; @@ -16,11 +15,7 @@ import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.ComparisonType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.common.FilterPermanentCard; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; @@ -29,14 +24,18 @@ import mage.game.permanent.Permanent; import mage.target.common.TargetCardInLibrary; import mage.target.targetpointer.FixedTarget; +import java.util.UUID; + /** - * * @author bunchOfDevs */ public final class DefiantVanguard extends CardImpl { + protected static final String EFFECT_KEY = "DefiantVanguardEffect_"; + private static final FilterPermanentCard filter = new FilterPermanentCard("Rebel permanent card with mana value 4 or less"); + static { filter.add(SubType.REBEL.getPredicate()); filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 5)); @@ -100,8 +99,8 @@ class DefiantVanguardTriggeredAbility extends TriggeredAbilityImpl { Permanent blocked = game.getPermanent(event.getTargetId()); if (blocker != null && blocked != null) { - game.getState().setValue(blocked.toString(), blocked.getZoneChangeCounter(game)); // in case the attacker changes zone - game.getState().setValue(blocker.toString(), blocker.getZoneChangeCounter(game)); // in case the blocker changes zone + game.getState().setValue(DefiantVanguard.EFFECT_KEY + blocked.getId(), blocked.getZoneChangeCounter(game)); // in case the attacker changes zone + game.getState().setValue(DefiantVanguard.EFFECT_KEY + blocker.getId(), blocker.getZoneChangeCounter(game)); // in case the blocker changes zone getAllEffects().setTargetPointer(new FixedTarget(blocked.getId())); return true; } @@ -131,13 +130,13 @@ class DefiantVanguardEffect extends OneShotEffect { Permanent blockedCreature = game.getPermanent(getTargetPointer().getFirst(game, source)); Permanent defiantVanguard = game.getPermanent(source.getSourceId()); if (blockedCreature != null) { - if (game.getState().getValue(blockedCreature.toString()).equals(blockedCreature.getZoneChangeCounter(game))) { // true if it did not change zones + if (game.getState().getValue(DefiantVanguard.EFFECT_KEY + blockedCreature.getId()).equals(blockedCreature.getZoneChangeCounter(game))) { // true if it did not change zones blockedCreature.destroy(source, game, false); result = true; } } if (defiantVanguard != null) { - if (game.getState().getValue(defiantVanguard.toString()).equals(defiantVanguard.getZoneChangeCounter(game))) { // true if it did not change zones + if (game.getState().getValue(DefiantVanguard.EFFECT_KEY + defiantVanguard.getId()).equals(defiantVanguard.getZoneChangeCounter(game))) { // true if it did not change zones defiantVanguard.destroy(source, game, false); result = true; } diff --git a/Mage.Sets/src/mage/cards/o/OgreMarauder.java b/Mage.Sets/src/mage/cards/o/OgreMarauder.java index eef1dcaf58a..e30f6d34f2b 100644 --- a/Mage.Sets/src/mage/cards/o/OgreMarauder.java +++ b/Mage.Sets/src/mage/cards/o/OgreMarauder.java @@ -21,7 +21,6 @@ import mage.constants.SubType; import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; -import mage.target.common.TargetControlledCreaturePermanent; /** * diff --git a/Mage.Sets/src/mage/cards/s/SharaeOfNumbingDepths.java b/Mage.Sets/src/mage/cards/s/SharaeOfNumbingDepths.java index 23bfd48086f..ce1306105f2 100644 --- a/Mage.Sets/src/mage/cards/s/SharaeOfNumbingDepths.java +++ b/Mage.Sets/src/mage/cards/s/SharaeOfNumbingDepths.java @@ -83,7 +83,7 @@ class SharaeOfNumbingDepthsTriggeredAbility extends TriggeredAbilityImpl { public boolean checkTrigger(GameEvent event, Game game) { Permanent permanent = game.getPermanent(event.getTargetId()); return permanent != null - && StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE.match(permanent, game) - && isControlledBy(event.getPlayerId()); + && isControlledBy(event.getPlayerId()) // whenever you tap + && StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE.match(permanent, this.getControllerId(), this, game); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/s/SkymarkRoc.java b/Mage.Sets/src/mage/cards/s/SkymarkRoc.java index b0c3972d972..b9bc17a294a 100644 --- a/Mage.Sets/src/mage/cards/s/SkymarkRoc.java +++ b/Mage.Sets/src/mage/cards/s/SkymarkRoc.java @@ -18,7 +18,6 @@ import mage.filter.predicate.mageobject.ToughnessPredicate; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.target.common.TargetCreaturePermanent; /** diff --git a/Mage.Sets/src/mage/cards/u/UlamogTheCeaselessHunger.java b/Mage.Sets/src/mage/cards/u/UlamogTheCeaselessHunger.java index 2fd36db8c4f..0e84fc7d752 100644 --- a/Mage.Sets/src/mage/cards/u/UlamogTheCeaselessHunger.java +++ b/Mage.Sets/src/mage/cards/u/UlamogTheCeaselessHunger.java @@ -19,7 +19,6 @@ import mage.constants.Zone; import mage.filter.FilterPermanent; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.Player; @@ -113,7 +112,7 @@ class UlamogAttackTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - Permanent sourcePermanent = game.getPermanent(this.getSourceId()); + Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(this.getSourceId()); if (sourcePermanent != null && event.getSourceId() != null && event.getSourceId().equals(this.getSourceId())) { diff --git a/Mage.Sets/src/mage/cards/w/WardscaleDragon.java b/Mage.Sets/src/mage/cards/w/WardscaleDragon.java index 1b0af0849f4..950ff7ffd71 100644 --- a/Mage.Sets/src/mage/cards/w/WardscaleDragon.java +++ b/Mage.Sets/src/mage/cards/w/WardscaleDragon.java @@ -13,7 +13,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; @@ -73,7 +72,7 @@ class WardscaleDragonRuleEffect extends ContinuousRuleModifyingEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { Permanent sourcePermanent = game.getPermanent(source.getSourceId()); if (sourcePermanent != null && sourcePermanent.isAttacking()) { - return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game)); + return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game, false)); } return false; } diff --git a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java index 4a9d8320e34..5bd7714092f 100644 --- a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java +++ b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java @@ -98,6 +98,9 @@ class ZurgoAndOjutaiTriggeredAbility extends TriggeredAbilityImpl implements Bat @Override public boolean checkEvent(DamagedEvent event, Game game) { + if (!event.isCombatDamage()) { + return false; + } Permanent permanent = game.getPermanent(event.getSourceId()); Permanent defender = game.getPermanent(event.getTargetId()); return permanent != null diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java new file mode 100644 index 00000000000..ef83f53f4ab --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java @@ -0,0 +1,197 @@ +package org.mage.test.AI.basic; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_Block_1_small_attacker_vs_1_big_blocker() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + + attack(1, playerA, "Arbor Elf"); + + // ai must block + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Arbor Elf", 1); + } + + @Test + public void test_Block_1_small_attacker_vs_2_big_blockers() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 + + attack(1, playerA, "Arbor Elf"); + + // ai must block + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Arbor Elf", 1); + } + + @Test + public void test_Block_1_small_attacker_vs_1_small_blocker() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + + attack(1, playerA, "Arbor Elf"); + + // ai must block + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Arbor Elf", 1); + assertGraveyardCount(playerB, "Arbor Elf", 1); + } + + @Test + public void test_Block_1_big_attacker_vs_1_small_blocker() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + + attack(1, playerA, "Balduvian Bears"); + + // ai must not block + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerA, "Balduvian Bears", 0); + assertGraveyardCount(playerB, "Arbor Elf", 0); + } + + @Test + public void test_Block_2_big_attackers_vs_1_small_blocker() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + + attack(1, playerA, "Balduvian Bears"); + attack(1, playerA, "Deadbridge Goliath"); + + // ai must not block + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 5); + assertGraveyardCount(playerA, "Balduvian Bears", 0); + assertGraveyardCount(playerA, "Deadbridge Goliath", 0); + assertGraveyardCount(playerB, "Arbor Elf", 0); + } + + @Test + public void test_Block_2_big_attackers_vs_1_big_blocker_a() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6 + + attack(1, playerA, "Balduvian Bears"); + attack(1, playerA, "Deadbridge Goliath"); + + // ai must block bigger attacker and survive (6/6 must block 5/5) + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerA, "Balduvian Bears", 0); + assertGraveyardCount(playerA, "Deadbridge Goliath", 1); + assertGraveyardCount(playerB, "Colossal Dreadmaw", 0); + } + + @Test + public void test_Block_2_big_attackers_vs_1_big_blocker_b() { + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerA, "Colossal Dreadmaw", 1); // 6/6 + // + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 + + attack(1, playerA, "Arbor Elf"); + attack(1, playerA, "Balduvian Bears"); + attack(1, playerA, "Deadbridge Goliath"); + attack(1, playerA, "Colossal Dreadmaw"); + + // ai must block bigger attacker and survive (3/3 must block 2/2) + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20 - 1 - 5 - 6); + assertGraveyardCount(playerA, "Balduvian Bears", 1); + assertPermanentCount(playerB, "Spectral Bears", 1); + } + + @Test + public void test_Block_1_attacker_vs_many_blockers() { + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + // + addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 + addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5 + addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6 + + attack(1, playerA, "Balduvian Bears"); + + // ai must use smaller blocker and survive (3/3 must block 2/2) + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Balduvian Bears", 1); + assertDamageReceived(playerB, "Spectral Bears", 2); + } + + // TODO: add tests with multi blocker requirement effects + // TODO: add tests for DeathtouchAbility + // TODO: add tests for FirstStrikeAbility + // TODO: add tests for DoubleStrikeAbility + // TODO: add tests for IndestructibleAbility + // TODO: add tests for FlyingAbility + // TODO: add tests for ReachAbility + // TODO: add tests for ExaltedAbility??? +} diff --git a/Mage.Tests/src/test/java/org/mage/test/combat/RemoveFromCombatTest.java b/Mage.Tests/src/test/java/org/mage/test/combat/RemoveFromCombatTest.java index 503a37ea8fc..c2c0b3ba3c4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/combat/RemoveFromCombatTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/combat/RemoveFromCombatTest.java @@ -106,4 +106,30 @@ public class RemoveFromCombatTest extends CardTestPlayerBase { assertLife(playerB, 20 - 2); assertGraveyardCount(playerB, "Jace, Memory Adept", 1); } + + /** + * Validate rule 806.2a: Abilities which refer to Defending Player still mean that defending player, even if the + * attacking creature is removed from combat. + */ + @Test + public void test_RemoveAttackerWithDefendingPlayerTriggeredAbilityOnStack() { + + addCard(Zone.HAND, playerA, "Swords to Plowshares", 1); + addCard(Zone.BATTLEFIELD, playerA, "Agate-Blade Assassin", 1); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + // attack player + attack(1, playerA, "Agate-Blade Assassin", playerB); + // remove Agate-Blade Assassin from combat + castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Swords to Plowshares"); + addTarget(playerA, "Agate-Blade Assassin"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 1); + assertLife(playerA, 20 + 1 /* StP */ + 1 /* Agate-Blade Assassin trigger */); + } + } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index bcb47f1f5ea..7137f686c7b 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -39,6 +39,7 @@ import org.apache.log4j.Logger; import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author BetaSteward_at_googlemail.com @@ -59,6 +60,7 @@ public class Combat implements Serializable, Copyable { private final List useToughnessForDamageFilters = new ArrayList<>(); protected List groups = new ArrayList<>(); + protected List formerGroups = new ArrayList<>(); protected Map blockingGroups = new HashMap<>(); // all possible defenders (players, planeswalkers or battle) protected Set defenders = new HashSet<>(); @@ -83,6 +85,9 @@ public class Combat implements Serializable, Copyable { for (CombatGroup group : combat.groups) { groups.add(group.copy()); } + for (CombatGroup group : combat.formerGroups) { + formerGroups.add(group.copy()); + } defenders.addAll(combat.defenders); for (Map.Entry group : combat.blockingGroups.entrySet()) { blockingGroups.put(group.getKey(), group.getValue()); @@ -181,6 +186,7 @@ public class Combat implements Serializable, Copyable { public void clear() { groups.clear(); + formerGroups.clear(); blockingGroups.clear(); defenders.clear(); attackingPlayerId = null; @@ -858,6 +864,7 @@ public class Combat implements Serializable, Copyable { // map with attackers (UUID) that must be blocked by at least one blocker and a set of all creatures that can block it and don't block yet Map> mustBeBlockedByAtLeastX = new HashMap<>(); Map minNumberOfBlockersMap = new HashMap<>(); + Map minPossibleBlockersMap = new HashMap<>(); // check mustBlock requirements of creatures from opponents of attacking player for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, player.getId(), game)) { @@ -876,6 +883,12 @@ public class Combat implements Serializable, Copyable { CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature); if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) { minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers()); + Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature); + if (toBeBlockedCreaturePermanent != null) { + minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy()); + } else { + minPossibleBlockersMap.put(toBeBlockedCreature, 1); + } Set potentialBlockers; if (mustBeBlockedByAtLeastX.containsKey(toBeBlockedCreature)) { potentialBlockers = mustBeBlockedByAtLeastX.get(toBeBlockedCreature); @@ -973,6 +986,12 @@ public class Combat implements Serializable, Copyable { CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature); if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) { minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers()); + Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature); + if (toBeBlockedCreaturePermanent != null) { + minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy()); + } else { + minPossibleBlockersMap.put(toBeBlockedCreature, 1); + } Set potentialBlockers; if (mustBeBlockedByAtLeastX.containsKey(toBeBlockedCreature)) { potentialBlockers = mustBeBlockedByAtLeastX.get(toBeBlockedCreature); @@ -1059,6 +1078,13 @@ public class Combat implements Serializable, Copyable { for (UUID toBeBlockedCreatureId : mustBeBlockedByAtLeastX.keySet()) { for (CombatGroup combatGroup : game.getCombat().getGroups()) { if (combatGroup.getAttackers().contains(toBeBlockedCreatureId)) { + // Neyith of the Dire Hunt: If the target creature has menace, two creatures must block it if able. + // (2020-06-23) + // This is a basic check to avoid deadlocking on one blocker plus 'must be blocked if able' with menace; + // a full solution is more complicated but this prevents the most common case. + if (mustBeBlockedByAtLeastX.get(toBeBlockedCreatureId).size() < minPossibleBlockersMap.get(toBeBlockedCreatureId)) { + continue; + } boolean requirementFulfilled = false; // Check whether an applicable creature is blocking. for (UUID blockerId : combatGroup.getBlockers()) { @@ -1659,6 +1685,36 @@ public class Combat implements Serializable, Copyable { * @return */ public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game) { + return getDefendingPlayerId(attackingCreatureId, game, true); + } + + /** + * Returns the playerId of the player that is attacked by given attacking + * creature or formerly-attacking creature. + * + * @param attackingCreatureId + * @param game + * @return + */ + public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game, boolean allowFormer) { + if (allowFormer) { + /* + * 802.2a. Any rule, object, or effect that refers to a "defending player" refers to one specific defending + * player, not to all of the defending players. If an ability of an attacking creature refers to a + * defending player, or a spell or ability refers to both an attacking creature and a defending player, + * then unless otherwise specified, the defending player it's referring to is the player that creature is + * attacking, the controller of the planeswalker that creature is attacking, or the protector of the battle + * that player is attacking. If that creature is no longer attacking, the defending player it's referring + * to is the player that creature was attacking before it was removed from combat, the controller of the + * planeswalker that creature was attacking before it was removed from combat, or the protector of the + * battle that player was attacking before it was removed from combat. + */ + return Stream.concat(groups.stream(), formerGroups.stream()) + .filter(group -> (group.getAttackers().contains(attackingCreatureId) || group.getFormerAttackers().contains(attackingCreatureId))) + .map(CombatGroup::getDefendingPlayerId) + .findFirst() + .orElse(null); + } return groups .stream() .filter(group -> group.getAttackers().contains(attackingCreatureId)) @@ -1723,6 +1779,7 @@ public class Combat implements Serializable, Copyable { } } if (group.attackers.isEmpty()) { + formerGroups.add(group); groups.remove(group); } return; diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 22ad213ada8..9106b2d4abc 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; public class CombatGroup implements Serializable, Copyable { protected List attackers = new ArrayList<>(); + protected List formerAttackers = new ArrayList<>(); protected List blockers = new ArrayList<>(); protected List blockerOrder = new ArrayList<>(); protected List attackerOrder = new ArrayList<>(); @@ -49,6 +50,7 @@ public class CombatGroup implements Serializable, Copyable { protected CombatGroup(final CombatGroup group) { this.attackers.addAll(group.attackers); + this.formerAttackers.addAll(group.formerAttackers); this.blockers.addAll(group.blockers); this.blockerOrder.addAll(group.blockerOrder); this.attackerOrder.addAll(group.attackerOrder); @@ -81,6 +83,10 @@ public class CombatGroup implements Serializable, Copyable { return attackers; } + public List getFormerAttackers() { + return formerAttackers; + } + public List getBlockers() { return blockers; } @@ -737,6 +743,7 @@ public class CombatGroup implements Serializable, Copyable { public boolean remove(UUID creatureId) { boolean result = false; if (attackers.contains(creatureId)) { + formerAttackers.add(creatureId); attackers.remove(creatureId); result = true; attackerOrder.remove(creatureId); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 46f0a89a656..409ace36912 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -21,7 +21,7 @@ import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; import mage.counters.Counters; -import mage.filter.*; +import mage.filter.FilterOpponent; import mage.game.Game; import mage.game.GameState; import mage.game.ZoneChangeInfo; @@ -210,8 +210,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { + ", " + getBasicMageObject().getClass().getSimpleName() + ", " + imageInfo + ", " + this.getPower() + "/" + this.getToughness() + + (this.getDamage() > 0 ? ", damage " + this.getDamage() : "") + (this.isCopy() ? ", copy" : "") - + (this.isTapped() ? ", tapped" : ""); + + (this.isTapped() ? ", tapped" : "") + + (this.isAttacking() ? ", attacking" : "") + + (this.getBlocking() > 0 ? ", blocking" : ""); } @Override