Skip to content

Commit 6b9532f

Browse files
authored
[refactor/bugfix] use rule 802.2a where appropriate. (magefree#13179)
* [refactor/bugfix] use rule 802.2a where appropriate. Many effects which relied on getDefendingPlayerId would fail if the attacking creature had been removed from combat before they resolved, in which case the defending player ID would be null. This fixes these issues. * Add test for removing attacking creature with Defending Player triggered ability. Change allowFormer to be true by default, reduce falses to only necessary cases.
1 parent 8de9fb0 commit 6b9532f

File tree

8 files changed

+73
-7
lines changed

8 files changed

+73
-7
lines changed

Mage.Sets/src/mage/cards/b/BeckoningWillOWisp.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ enum BeckoningWillOWispPredicate implements ObjectSourcePlayerPredicate<Permanen
7070
@Override
7171
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
7272
UUID playerId = (UUID) game.getState().getValue(input.getSourceId() + "_" + game.getState().getZoneChangeCounter(input.getSourceId()) + "_chosenOpponent");
73-
return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game));
73+
return playerId != null && playerId.equals(game.getCombat().getDefendingPlayerId(input.getObject().getId(), game, false));
7474
}
7575
}
7676

Mage.Sets/src/mage/cards/o/OgreMarauder.java

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import mage.filter.StaticFilters;
2222
import mage.game.Game;
2323
import mage.players.Player;
24-
import mage.target.common.TargetControlledCreaturePermanent;
2524

2625
/**
2726
*

Mage.Sets/src/mage/cards/s/SkymarkRoc.java

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import mage.filter.predicate.permanent.ControllerIdPredicate;
1919
import mage.game.Game;
2020
import mage.game.events.GameEvent;
21-
import mage.game.events.GameEvent.EventType;
2221
import mage.target.common.TargetCreaturePermanent;
2322

2423
/**

Mage.Sets/src/mage/cards/u/UlamogTheCeaselessHunger.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import mage.filter.FilterPermanent;
2020
import mage.game.Game;
2121
import mage.game.events.GameEvent;
22-
import mage.game.events.GameEvent.EventType;
2322
import mage.game.permanent.Permanent;
2423
import mage.game.stack.Spell;
2524
import mage.players.Player;
@@ -113,7 +112,7 @@ public boolean checkEventType(GameEvent event, Game game) {
113112

114113
@Override
115114
public boolean checkTrigger(GameEvent event, Game game) {
116-
Permanent sourcePermanent = game.getPermanent(this.getSourceId());
115+
Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(this.getSourceId());
117116
if (sourcePermanent != null
118117
&& event.getSourceId() != null
119118
&& event.getSourceId().equals(this.getSourceId())) {

Mage.Sets/src/mage/cards/w/WardscaleDragon.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import mage.constants.SubType;
1414
import mage.constants.Duration;
1515
import mage.constants.Outcome;
16-
import mage.constants.Zone;
1716
import mage.game.Game;
1817
import mage.game.events.GameEvent;
1918
import mage.game.permanent.Permanent;
@@ -73,7 +72,7 @@ public boolean checksEventType(GameEvent event, Game game) {
7372
public boolean applies(GameEvent event, Ability source, Game game) {
7473
Permanent sourcePermanent = game.getPermanent(source.getSourceId());
7574
if (sourcePermanent != null && sourcePermanent.isAttacking()) {
76-
return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game));
75+
return event.getPlayerId().equals(game.getCombat().getDefendingPlayerId(sourcePermanent.getId(), game, false));
7776
}
7877
return false;
7978
}

Mage.Tests/src/test/java/org/mage/test/combat/RemoveFromCombatTest.java

+26
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,30 @@ public void test_Defender_AttackPlaneswalkerAndRemoveDefender() {
106106
assertLife(playerB, 20 - 2);
107107
assertGraveyardCount(playerB, "Jace, Memory Adept", 1);
108108
}
109+
110+
/**
111+
* Validate rule 806.2a: Abilities which refer to Defending Player still mean that defending player, even if the
112+
* attacking creature is removed from combat.
113+
*/
114+
@Test
115+
public void test_RemoveAttackerWithDefendingPlayerTriggeredAbilityOnStack() {
116+
117+
addCard(Zone.HAND, playerA, "Swords to Plowshares", 1);
118+
addCard(Zone.BATTLEFIELD, playerA, "Agate-Blade Assassin", 1); // 2/2
119+
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
120+
121+
// attack player
122+
attack(1, playerA, "Agate-Blade Assassin", playerB);
123+
// remove Agate-Blade Assassin from combat
124+
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerA, "Swords to Plowshares");
125+
addTarget(playerA, "Agate-Blade Assassin");
126+
127+
setStrictChooseMode(true);
128+
setStopAt(1, PhaseStep.END_TURN);
129+
execute();
130+
131+
assertLife(playerB, 20 - 1);
132+
assertLife(playerA, 20 + 1 /* StP */ + 1 /* Agate-Blade Assassin trigger */);
133+
}
134+
109135
}

Mage/src/main/java/mage/game/combat/Combat.java

+37
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.io.Serializable;
4040
import java.util.*;
4141
import java.util.stream.Collectors;
42+
import java.util.stream.Stream;
4243

4344
/**
4445
* @author BetaSteward_at_googlemail.com
@@ -59,6 +60,7 @@ public class Combat implements Serializable, Copyable<Combat> {
5960
private final List<FilterCreaturePermanent> useToughnessForDamageFilters = new ArrayList<>();
6061

6162
protected List<CombatGroup> groups = new ArrayList<>();
63+
protected List<CombatGroup> formerGroups = new ArrayList<>();
6264
protected Map<UUID, CombatGroup> blockingGroups = new HashMap<>();
6365
// all possible defenders (players, planeswalkers or battle)
6466
protected Set<UUID> defenders = new HashSet<>();
@@ -83,6 +85,9 @@ protected Combat(final Combat combat) {
8385
for (CombatGroup group : combat.groups) {
8486
groups.add(group.copy());
8587
}
88+
for (CombatGroup group : combat.formerGroups) {
89+
formerGroups.add(group.copy());
90+
}
8691
defenders.addAll(combat.defenders);
8792
for (Map.Entry<UUID, CombatGroup> group : combat.blockingGroups.entrySet()) {
8893
blockingGroups.put(group.getKey(), group.getValue());
@@ -181,6 +186,7 @@ public void checkForRemoveFromCombat(Game game) {
181186

182187
public void clear() {
183188
groups.clear();
189+
formerGroups.clear();
184190
blockingGroups.clear();
185191
defenders.clear();
186192
attackingPlayerId = null;
@@ -1679,6 +1685,36 @@ public UUID getDefenderId(UUID attackerId) {
16791685
* @return
16801686
*/
16811687
public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game) {
1688+
return getDefendingPlayerId(attackingCreatureId, game, true);
1689+
}
1690+
1691+
/**
1692+
* Returns the playerId of the player that is attacked by given attacking
1693+
* creature or formerly-attacking creature.
1694+
*
1695+
* @param attackingCreatureId
1696+
* @param game
1697+
* @return
1698+
*/
1699+
public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game, boolean allowFormer) {
1700+
if (allowFormer) {
1701+
/*
1702+
* 802.2a. Any rule, object, or effect that refers to a "defending player" refers to one specific defending
1703+
* player, not to all of the defending players. If an ability of an attacking creature refers to a
1704+
* defending player, or a spell or ability refers to both an attacking creature and a defending player,
1705+
* then unless otherwise specified, the defending player it's referring to is the player that creature is
1706+
* attacking, the controller of the planeswalker that creature is attacking, or the protector of the battle
1707+
* that player is attacking. If that creature is no longer attacking, the defending player it's referring
1708+
* to is the player that creature was attacking before it was removed from combat, the controller of the
1709+
* planeswalker that creature was attacking before it was removed from combat, or the protector of the
1710+
* battle that player was attacking before it was removed from combat.
1711+
*/
1712+
return Stream.concat(groups.stream(), formerGroups.stream())
1713+
.filter(group -> (group.getAttackers().contains(attackingCreatureId) || group.getFormerAttackers().contains(attackingCreatureId)))
1714+
.map(CombatGroup::getDefendingPlayerId)
1715+
.findFirst()
1716+
.orElse(null);
1717+
}
16821718
return groups
16831719
.stream()
16841720
.filter(group -> group.getAttackers().contains(attackingCreatureId))
@@ -1743,6 +1779,7 @@ public void removeAttacker(UUID attackerId, Game game) {
17431779
}
17441780
}
17451781
if (group.attackers.isEmpty()) {
1782+
formerGroups.add(group);
17461783
groups.remove(group);
17471784
}
17481785
return;

Mage/src/main/java/mage/game/combat/CombatGroup.java

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
public class CombatGroup implements Serializable, Copyable<CombatGroup> {
2828

2929
protected List<UUID> attackers = new ArrayList<>();
30+
protected List<UUID> formerAttackers = new ArrayList<>();
3031
protected List<UUID> blockers = new ArrayList<>();
3132
protected List<UUID> blockerOrder = new ArrayList<>();
3233
protected List<UUID> attackerOrder = new ArrayList<>();
@@ -49,6 +50,7 @@ public CombatGroup(UUID defenderId, boolean defenderIsPermanent, UUID defendingP
4950

5051
protected CombatGroup(final CombatGroup group) {
5152
this.attackers.addAll(group.attackers);
53+
this.formerAttackers.addAll(group.formerAttackers);
5254
this.blockers.addAll(group.blockers);
5355
this.blockerOrder.addAll(group.blockerOrder);
5456
this.attackerOrder.addAll(group.attackerOrder);
@@ -81,6 +83,10 @@ public List<UUID> getAttackers() {
8183
return attackers;
8284
}
8385

86+
public List<UUID> getFormerAttackers() {
87+
return formerAttackers;
88+
}
89+
8490
public List<UUID> getBlockers() {
8591
return blockers;
8692
}
@@ -737,6 +743,7 @@ public boolean removeAttackedPermanent(UUID permanentId) {
737743
public boolean remove(UUID creatureId) {
738744
boolean result = false;
739745
if (attackers.contains(creatureId)) {
746+
formerAttackers.add(creatureId);
740747
attackers.remove(creatureId);
741748
result = true;
742749
attackerOrder.remove(creatureId);

0 commit comments

Comments
 (0)