* Fixed that permanents under non owner control sine they are on the battlefield were no exiled if the controller left the game (e.g. Captive Audience) (fixes #5593).

This commit is contained in:
LevelX2 2020-06-27 23:47:04 +02:00
parent 2c745109e4
commit d2d892a7cb
7 changed files with 308 additions and 235 deletions

View file

@ -1,5 +1,6 @@
package org.mage.test.multiplayer; package org.mage.test.multiplayer;
import java.io.FileNotFoundException;
import mage.constants.MultiplayerAttackOption; import mage.constants.MultiplayerAttackOption;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.RangeOfInfluence; import mage.constants.RangeOfInfluence;
@ -14,8 +15,6 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestMultiPlayerBase; import org.mage.test.serverside.base.CardTestMultiPlayerBase;
import java.io.FileNotFoundException;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
@ -344,4 +343,54 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase {
Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped());
} }
/**
* Captive Audience doesn't work correctly in multiplayer #5593
*
* Currently, if the controller of Captive Audience leaves the game, Captive
* Audience returns to its owner instead of being exiled.
*/
@Test
public void TestCaptiveAudienceGoesToExile() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
// Captive Audience enters the battlefield under the control of an opponent of your choice.
// At the beginning of your upkeep, choose one that hasn't been chosen
// Your life total becomes 4.
// Discard your hand.
// Each opponent creates five 2/2 black Zombie creature tokens.
addCard(Zone.HAND, playerA, "Captive Audience"); // Enchantment {5}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1);
setChoice(playerA, "PlayerA"); // Starting Player
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Captive Audience");
setChoice(playerA, "PlayerD");
setModeChoice(playerD, "1");
attack(5, playerA, "Silvercoat Lion", playerD);
attack(5, playerA, "Pillarfield Ox", playerD);
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
assertLife(playerA, 2);
Assert.assertFalse("Player D is no longer in the game", playerD.isInGame());
assertPermanentCount(playerD, 0);
assertPermanentCount(playerA, "Captive Audience", 0);
assertGraveyardCount(playerA, "Captive Audience", 0);
assertExileCount(playerA, "Captive Audience", 1);
}
} }

View file

@ -1,5 +1,13 @@
package org.mage.test.serverside.base.impl; package org.mage.test.serverside.base.impl;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.ObjectColor; import mage.ObjectColor;
@ -35,15 +43,6 @@ import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestAPI; import org.mage.test.serverside.base.CardTestAPI;
import org.mage.test.serverside.base.MageTestPlayerBase; import org.mage.test.serverside.base.MageTestPlayerBase;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** /**
* API for test initialization and asserting the test results. * API for test initialization and asserting the test results.
* *
@ -1353,8 +1352,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
} }
/** /**
* Raise error on any unused commands, choices or targets * Raise error on any unused commands, choices or targets If you want to
* If you want to test that ability can't be activated then use call checkPlayableAbility() * test that ability can't be activated then use call checkPlayableAbility()
* *
* @throws AssertionError * @throws AssertionError
*/ */
@ -1691,9 +1690,12 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
} }
/** /**
* For use choices set "Yes" or "No" the the choice string. For X values set * For use choices set "Yes" or "No" the the choice string.<br>
* "X=[xValue]" example: for X=3 set choice string to "X=3". * For X values set "X=[xValue]" example: for X=3 set choice string to
* <br>For ColorChoice use "Red", "Green", "Blue", "Black" or "White" * "X=3".<br>
* For ColorChoice use "Red", "Green", "Blue", "Black" or "White"<br>
* use command setModeChoice if you have to set a mode from modal
* ability<br>
* *
* @param player * @param player
* @param choice * @param choice

View file

@ -24,7 +24,6 @@ import mage.target.targetpointer.FixedTarget;
* *
* @author LevelX2 * @author LevelX2
*/ */
public class EntersBattlefieldUnderControlOfOpponentOfChoiceEffect extends OneShotEffect { public class EntersBattlefieldUnderControlOfOpponentOfChoiceEffect extends OneShotEffect {
public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect() { public EntersBattlefieldUnderControlOfOpponentOfChoiceEffect() {
@ -58,6 +57,8 @@ public class EntersBattlefieldUnderControlOfOpponentOfChoiceEffect extends OneSh
} }
Permanent permanent = game.getPermanentEntering(source.getSourceId()); Permanent permanent = game.getPermanentEntering(source.getSourceId());
if (permanent != null) { if (permanent != null) {
permanent.setOriginalControllerId(opponent.getId()); // permanent was controlled by this player since the existance of this object so original controller has to be set to the first controller
permanent.setControllerId(opponent.getId()); // neccessary to set already here because spell caster never controlled the permanent (important for rule 800.4a)
game.informPlayers(permanent.getLogName() + " enters the battlefield under the control of " + opponent.getLogName()); game.informPlayers(permanent.getLogName() + " enters the battlefield under the control of " + opponent.getLogName());
} }
ContinuousEffect continuousEffect = new GainControlTargetEffect( ContinuousEffect continuousEffect = new GainControlTargetEffect(

View file

@ -1,5 +1,9 @@
package mage.game; package mage.game;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.MageException; import mage.MageException;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.*;
@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent;
import mage.watchers.common.*; import mage.watchers.common.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
public abstract class GameImpl implements Game, Serializable { public abstract class GameImpl implements Game, Serializable {
private static final int ROLLBACK_TURNS_MAX = 4; private static final int ROLLBACK_TURNS_MAX = 4;
@ -1805,7 +1804,7 @@ public abstract class GameImpl implements Game, Serializable {
break; break;
} }
// triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -2596,7 +2595,7 @@ public abstract class GameImpl implements Game, Serializable {
} }
//20100423 - 800.4a //20100423 - 800.4a
Set<Card> toOutside = new HashSet<>(); Set<Card> toOutside = new HashSet<>();
for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) {
Permanent perm = it.next(); Permanent perm = it.next();
if (perm.isOwnedBy(playerId)) { if (perm.isOwnedBy(playerId)) {
if (perm.getAttachedTo() != null) { if (perm.getAttachedTo() != null) {
@ -2621,6 +2620,10 @@ public abstract class GameImpl implements Game, Serializable {
for (ContinuousEffect effect : getContinuousEffects().getLayeredEffects(this)) { for (ContinuousEffect effect : getContinuousEffects().getLayeredEffects(this)) {
if (effect.hasLayer(Layer.ControlChangingEffects_2)) { if (effect.hasLayer(Layer.ControlChangingEffects_2)) {
for (Ability ability : getContinuousEffects().getLayeredEffectAbilities(effect)) { for (Ability ability : getContinuousEffects().getLayeredEffectAbilities(effect)) {
if (effect.getTargetPointer().getTargets(this, ability).contains(perm.getId())) {
effect.discard();
continue Effects;
}
for (Target target : ability.getTargets()) { for (Target target : ability.getTargets()) {
for (UUID targetId : target.getTargets()) { for (UUID targetId : target.getTargets()) {
if (targetId.equals(perm.getId())) { if (targetId.equals(perm.getId())) {
@ -2630,6 +2633,7 @@ public abstract class GameImpl implements Game, Serializable {
} }
} }
} }
} }
} }
} }
@ -2641,7 +2645,7 @@ public abstract class GameImpl implements Game, Serializable {
player.moveCards(toOutside, Zone.OUTSIDE, null, this); player.moveCards(toOutside, Zone.OUTSIDE, null, this);
// triggered abilities that don't use the stack have to be executed // triggered abilities that don't use the stack have to be executed
List<TriggeredAbility> abilities = state.getTriggered(player.getId()); List<TriggeredAbility> abilities = state.getTriggered(player.getId());
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -2661,7 +2665,7 @@ public abstract class GameImpl implements Game, Serializable {
// Remove cards from the player in all exile zones // Remove cards from the player in all exile zones
for (ExileZone exile : this.getExile().getExileZones()) { for (ExileZone exile : this.getExile().getExileZones()) {
for (Iterator<UUID> it = exile.iterator(); it.hasNext(); ) { for (Iterator<UUID> it = exile.iterator(); it.hasNext();) {
Card card = this.getCard(it.next()); Card card = this.getCard(it.next());
if (card != null && card.isOwnedBy(playerId)) { if (card != null && card.isOwnedBy(playerId)) {
it.remove(); it.remove();
@ -2671,7 +2675,7 @@ public abstract class GameImpl implements Game, Serializable {
//Remove all commander/emblems/plane the player controls //Remove all commander/emblems/plane the player controls
boolean addPlaneAgain = false; boolean addPlaneAgain = false;
for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext(); ) { for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext();) {
CommandObject obj = it.next(); CommandObject obj = it.next();
if (obj.isControlledBy(playerId)) { if (obj.isControlledBy(playerId)) {
if (obj instanceof Emblem) { if (obj instanceof Emblem) {

View file

@ -1,5 +1,8 @@
package mage.game.permanent; package mage.game.permanent;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -10,12 +13,10 @@ import mage.game.Controllable;
import mage.game.Game; import mage.game.Game;
import mage.game.GameState; import mage.game.GameState;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public interface Permanent extends Card, Controllable { public interface Permanent extends Card, Controllable {
void setOriginalControllerId(UUID controllerId);
void setControllerId(UUID controllerId); void setControllerId(UUID controllerId);
boolean isTapped(); boolean isTapped();
@ -103,7 +104,8 @@ public interface Permanent extends Card, Controllable {
/** /**
* @param source * @param source
* @param game * @param game
* @param silentMode - use it to ignore warning message for users (e.g. for checking only) * @param silentMode - use it to ignore warning message for users (e.g. for
* checking only)
* @return * @return
*/ */
boolean cantBeAttachedBy(MageObject source, Game game, boolean silentMode); boolean cantBeAttachedBy(MageObject source, Game game, boolean silentMode);

View file

@ -1,5 +1,7 @@
package mage.game.permanent; package mage.game.permanent;
import java.io.Serializable;
import java.util.*;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.ObjectColor; import mage.ObjectColor;
@ -38,9 +40,6 @@ import mage.util.GameLog;
import mage.util.ThreadLocalStringBuilder; import mage.util.ThreadLocalStringBuilder;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -184,6 +183,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
abilities.setControllerId(controllerId); abilities.setControllerId(controllerId);
} }
@Override
public void setOriginalControllerId(UUID originalControllerId) {
this.originalControllerId = originalControllerId;
}
/** /**
* Called before each applyEffects or if after a permanent was copied for * Called before each applyEffects or if after a permanent was copied for
* the copied object * the copied object
@ -793,7 +797,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.attachedTo = attachToObjectId; this.attachedTo = attachToObjectId;
this.attachedToZoneChangeCounter = game.getState().getZoneChangeCounter(attachToObjectId); this.attachedToZoneChangeCounter = game.getState().getZoneChangeCounter(attachToObjectId);
for (Ability ability : this.getAbilities()) { for (Ability ability : this.getAbilities()) {
for (Iterator<Effect> ite = ability.getEffects(game, EffectType.CONTINUOUS).iterator(); ite.hasNext(); ) { for (Iterator<Effect> ite = ability.getEffects(game, EffectType.CONTINUOUS).iterator(); ite.hasNext();) {
ContinuousEffect effect = (ContinuousEffect) ite.next(); ContinuousEffect effect = (ContinuousEffect) ite.next();
game.getContinuousEffects().setOrder(effect); game.getContinuousEffects().setOrder(effect);
// It's important to update the timestamp of the copied effect in ContinuousEffects because it does the action // It's important to update the timestamp of the copied effect in ContinuousEffects because it does the action
@ -1619,9 +1623,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List<UUID> appliedEffects) { public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List<UUID> appliedEffects) {
Zone fromZone = game.getState().getZone(objectId); Zone fromZone = game.getState().getZone(objectId);
ZoneChangeEvent event = new ZoneChangeEvent(this, sourceId, ownerId, fromZone, Zone.EXILED, appliedEffects); ZoneChangeEvent event = new ZoneChangeEvent(this, sourceId, ownerId, fromZone, Zone.EXILED, appliedEffects);
ZoneChangeInfo.Exile info = new ZoneChangeInfo.Exile(event, exileId, name); ZoneChangeInfo.Exile zcInfo = new ZoneChangeInfo.Exile(event, exileId, name);
boolean successfullyMoved = ZonesHandler.moveCard(info, game); boolean successfullyMoved = ZonesHandler.moveCard(zcInfo, game);
//20180810 - 701.3d //20180810 - 701.3d
detachAllAttachments(game); detachAllAttachments(game);
return successfullyMoved; return successfullyMoved;

View file

@ -1,6 +1,10 @@
package mage.players; package mage.players;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
@ -66,11 +70,6 @@ import mage.util.GameLog;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class); private static final Logger logger = Logger.getLogger(PlayerImpl.class);
@ -2958,7 +2957,8 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param ability * @param ability
* @param availableMana if null, it won't be checked if enough mana is available * @param availableMana if null, it won't be checked if enough mana is
* available
* @param sourceObject * @param sourceObject
* @param game * @param game
* @return * @return
@ -3290,6 +3290,17 @@ public abstract class PlayerImpl implements Player, Serializable {
return getPlayable(game, hidden, Zone.ALL, true); return getPlayable(game, hidden, Zone.ALL, true);
} }
/**
* Returns a list of all available spells and abilities the player can
* currently cast/activate with his available ressources
*
* @param game
* @param hidden also from hidden objects (e.g. turned face down cards ?)
* @param fromZone of objects from which zone (ALL = from all zones)
* @param hideDuplicatedAbilities if equal abilities exist return only the
* first instance
* @return
*/
public List<ActivatedAbility> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { public List<ActivatedAbility> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
List<ActivatedAbility> playable = new ArrayList<>(); List<ActivatedAbility> playable = new ArrayList<>();
if (shouldSkipGettingPlayable(game)) { if (shouldSkipGettingPlayable(game)) {
@ -4004,7 +4015,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// identify cards from one owner // identify cards from one owner
Cards cards = new CardsImpl(); Cards cards = new CardsImpl();
UUID ownerId = null; UUID ownerId = null;
for (Iterator<Card> it = allCards.iterator(); it.hasNext(); ) { for (Iterator<Card> it = allCards.iterator(); it.hasNext();) {
Card card = it.next(); Card card = it.next();
if (cards.isEmpty()) { if (cards.isEmpty()) {
ownerId = card.getOwnerId(); ownerId = card.getOwnerId();