Fix some iterators that try to modify themselves (ConcurrentModificationException, #10460)

* Add test to confirm functionality
* Reimplement Whirlwind Denial
* Fix Awaken the Sleeper
This commit is contained in:
xenohedron 2023-06-17 10:06:03 -04:00 committed by GitHub
parent 94dfbdfed4
commit b340ab3b73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 107 additions and 31 deletions

View file

@ -18,6 +18,8 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCreaturePermanent;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
/**
@ -73,12 +75,16 @@ class AwakenTheSleeperEffect extends OneShotEffect {
|| !player.chooseUse(outcome, "Destroy all equipment attached to " + permanent.getName() + '?', source, game)) {
return false;
}
List<Permanent> toDestroy = new LinkedList<>();
for (UUID attachmentId : permanent.getAttachments()) {
Permanent attachment = game.getPermanent(attachmentId);
if (attachment != null && attachment.hasSubtype(SubType.EQUIPMENT, game)) {
attachment.destroy(source, game);
toDestroy.add(attachment);
}
}
for (Permanent equipment : toDestroy) {
equipment.destroy(source, game);
}
return true;
}
}

View file

@ -8,14 +8,16 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.util.ManaUtil;
import java.util.Objects;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
/**
* @author TheElk801
* @author xenohedron
*/
public final class WhirlwindDenial extends CardImpl {
@ -55,36 +57,46 @@ class WhirlwindDenialEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
game.getStack()
.stream()
.filter(Objects::nonNull)
.forEachOrdered(stackObject -> {
if (!game.getOpponents(source.getControllerId()).contains(stackObject.getControllerId())) {
return;
}
Player player = game.getPlayer(stackObject.getControllerId());
if (player == null) {
return;
}
Cost cost = ManaUtil.createManaCost(4, false);
if (cost.canPay(source, source, stackObject.getControllerId(), game)
&& player.chooseUse(outcome, "Pay {4} to prevent "
+ stackObject.getIdName() + " from being countered?", source, game)
&& cost.pay(source, game, source, stackObject.getControllerId(), false)) {
game.informPlayers("The cost was paid by "
+ player.getLogName()
+ " to prevent "
+ stackObject.getIdName()
+ " from being countered.");
return;
}
game.informPlayers("The cost was not paid by "
+ player.getLogName()
+ " to prevent "
List<StackObject> stackObjectsToCounter = new LinkedList<>();
Cost cost = ManaUtil.createManaCost(4, false);
// As Whirlwind Denial resolves, first the opponent whose turn it is
// (or, if it's your turn, the next opponent in turn order) chooses which spells and/or abilities to pay for,
// then pays that amount. Then each other opponent in turn order does the same.
// Then all spells and abilities that weren't paid for are countered at the same time.
// (2020-01-24)
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
if (playerId.equals(source.getControllerId())) {
continue; // only opponents have to pay
}
Player player = game.getPlayer(playerId);
for (StackObject stackObject : game.getStack()) {
if (!playerId.equals(stackObject.getControllerId())) {
continue; // opponents only choose for their own spells/abilities
}
if (player == null) { // shouldn't be null, but if somehow so, they can't pay, so counter it
stackObjectsToCounter.add(stackObject);
continue;
}
if (cost.canPay(source, source, playerId, game)
&& player.chooseUse(outcome, "Pay {4} to prevent "
+ stackObject.getIdName() + " from being countered?", source, game)
&& cost.pay(source, game, source, stackObject.getControllerId(), false)) {
game.informPlayers(player.getLogName()
+ " pays the cost to prevent "
+ stackObject.getIdName()
+ " from being countered.");
game.getStack().counter(stackObject.getId(), source, game);
});
} else {
game.informPlayers(stackObject.getIdName()
+ " will be countered as "
+ player.getLogName()
+ " does not pay the cost.");
stackObjectsToCounter.add(stackObject); // will be countered all at the end
}
}
}
for (StackObject toCounter : stackObjectsToCounter) {
game.getStack().counter(toCounter.getId(), source, game);
}
return true;
}
}

View file

@ -0,0 +1,58 @@
package org.mage.test.cards.single.thb;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author xenohedron
*/
public class WhirlwindDenialTest extends CardTestPlayerBase {
private static final String denial = "Whirlwind Denial";
private static final String guttersnipe = "Guttersnipe"; // trigger deals 2 damage on cast
private static final String tithe = "Blood Tithe"; // deals 3 damage, gain 3 life
private void baseWhirlwindTest(boolean payTrigger, boolean paySpell) {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3+4+4);
addCard(Zone.BATTLEFIELD, playerA, guttersnipe);
addCard(Zone.HAND, playerA, tithe); // Player A tries to cast Blood Tithe for 3 damage + 2 damage
addCard(Zone.BATTLEFIELD, playerB, "Island", 3);
addCard(Zone.HAND, playerB, denial); // Player B denies it
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, tithe);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, denial);
setChoice(playerA, payTrigger);
setChoice(playerA, paySpell);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20 - (payTrigger ? 2 : 0) - (paySpell ? 3 : 0));
}
@Test
public void testWhirlwindPayAllCosts() {
baseWhirlwindTest(true, true);
}
@Test
public void testWhirlwindPayTrigger() {
baseWhirlwindTest(true, false);
}
@Test
public void testWhirlwindPaySpell() {
baseWhirlwindTest(false, true);
}
@Test
public void testWhirlwindPayNone() {
baseWhirlwindTest(false, false);
}
}