* Until end of your turn - fixed that effects discarded too early in multiplayer games (#5759, #5676);

Tests: added dozen tests for end of turn effects and related cards.
This commit is contained in:
Oleg Agafonov 2019-04-28 11:27:08 +04:00
parent 4288e45c23
commit 534037e095
22 changed files with 758 additions and 137 deletions

View file

@ -1,5 +1,3 @@
package mage.abilities;
import mage.constants.Duration;
@ -48,8 +46,8 @@ public class DelayedTriggeredAbilities extends AbilitiesImpl<DelayedTriggeredAbi
}
}
public void removeEndOfTurnAbilities() {
this.removeIf(ability -> ability.getDuration() == Duration.EndOfTurn);
public void removeEndOfTurnAbilities(Game game) {
this.removeIf(ability -> ability.getDuration() == Duration.EndOfTurn); // TODO: add Duration.EndOfYourTurn like effects
}
public void removeEndOfCombatAbilities() {

View file

@ -40,6 +40,8 @@ public interface ContinuousEffect extends Effect {
void init(Ability source, Game game);
void init(Ability source, Game game, UUID activePlayerId);
Layer getLayer();
SubLayer getSublayer();
@ -58,14 +60,14 @@ public interface ContinuousEffect extends Effect {
void addDependedToType(DependencyType dependencyType);
void setStartingTurnNum(Game game, UUID startingController);
int getStartingTurnNum();
int getNextStartingControllerTurnNum();
void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId);
UUID getStartingController();
void incYourTurnNumPlayed();
boolean isYourNextTurn(Game game);
@Override
void newId();

View file

@ -14,7 +14,7 @@ import mage.players.Player;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public abstract class ContinuousEffectImpl extends EffectImpl implements ContinuousEffect {
@ -38,10 +38,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
*/
protected boolean characterDefining = false;
// until your next turn
private int startingTurnNum;
private int yourNextTurnNum;
private UUID startingControllerId;
// until your next turn or until end of your next turn
private UUID startingControllerId; // player to checkss turns (can't different with real controller ability)
private boolean startingTurnWasActive;
private int yourTurnNumPlayed = 0; // turnes played after effect was created
public ContinuousEffectImpl(Duration duration, Outcome outcome) {
super(outcome);
@ -69,9 +69,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.affectedObjectsSet = effect.affectedObjectsSet;
this.affectedObjectList.addAll(effect.affectedObjectList);
this.temporary = effect.temporary;
this.startingTurnNum = effect.startingTurnNum;
this.yourNextTurnNum = effect.yourNextTurnNum;
this.startingControllerId = effect.startingControllerId;
this.startingTurnWasActive = effect.startingTurnWasActive;
this.yourTurnNumPlayed = effect.yourTurnNumPlayed;
this.dependencyTypes = effect.dependencyTypes;
this.dependendToTypes = effect.dependendToTypes;
this.characterDefining = effect.characterDefining;
@ -139,6 +139,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
@Override
public void init(Ability source, Game game) {
init(source, game, game.getActivePlayerId());
}
@Override
public void init(Ability source, Game game, UUID activePlayerId) {
targetPointer.init(game, source);
//20100716 - 611.2c
if (AbilityType.ACTIVATED == source.getAbilityType()
@ -161,50 +166,75 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.affectedObjectsSet = true;
}
}
setStartingTurnNum(game, source.getControllerId());
setStartingControllerAndTurnNum(game, source.getControllerId(), activePlayerId);
}
@Override
public void setStartingTurnNum(Game game, UUID startingController) {
this.startingControllerId = startingController;
this.startingTurnNum = game.getTurnNum();
this.yourNextTurnNum = game.isActivePlayer(startingControllerId) ? startingTurnNum + 2 : startingTurnNum + 1;
}
public int getStartingTurnNum() {
return this.startingTurnNum;
}
public int getNextStartingControllerTurnNum() {
return this.yourNextTurnNum;
}
public UUID getStartingController() {
return this.startingControllerId;
return startingControllerId;
}
@Override
public void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId) {
this.startingControllerId = startingController;
this.startingTurnWasActive = activePlayerId != null && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
this.yourTurnNumPlayed = 0;
}
@Override
public void incYourTurnNumPlayed() {
yourTurnNumPlayed++;
}
@Override
public boolean isYourNextTurn(Game game) {
if (this.startingTurnWasActive) {
return yourTurnNumPlayed == 1 && game.isActivePlayer(startingControllerId);
} else {
return yourTurnNumPlayed == 0 && game.isActivePlayer(startingControllerId);
}
}
@Override
public boolean isInactive(Ability source, Game game) {
if (duration == Duration.UntilYourNextTurn || duration == Duration.UntilEndOfYourNextTurn) {
Player player = game.getPlayer(startingControllerId);
if (player != null) {
if (player.isInGame()) {
boolean canDelete = false;
switch (duration) {
case UntilYourNextTurn:
canDelete = game.getTurnNum() >= yourNextTurnNum;
break;
case UntilEndOfYourNextTurn:
canDelete = (game.getTurnNum() > yourNextTurnNum)
|| (game.getTurnNum() == yourNextTurnNum && game.getStep().getType().isAfter(PhaseStep.END_TURN));
}
return canDelete;
}
return player.hasReachedNextTurnAfterLeaving();
}
return true;
// YOUR turn checks
// until end of turn - must be checked on cleanup step, see rules 514.2
// other must checked here (active and leave players), see rules 800.4
switch (duration) {
case UntilYourNextTurn:
case UntilEndOfYourNextTurn:
break;
default:
return false;
}
return false;
// cheat engine put cards without play and calls direct applyEffects with clean -- need to ignore it
if (game.getActivePlayerId() == null) {
return false;
}
boolean canDelete = false;
Player player = game.getPlayer(startingControllerId);
// discard on start of turn for leave player
// 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn
// or until a specific point in that turn will last until that turn would have begun.
// They neither expire immediately nor last indefinitely.
switch (duration) {
case UntilYourNextTurn:
case UntilEndOfYourNextTurn:
canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
}
// discard on another conditions (start of your turn)
switch (duration) {
case UntilYourNextTurn:
if (player != null && player.isInGame()) {
canDelete = canDelete || this.isYourNextTurn(game);
}
}
return canDelete;
}
@Override

View file

@ -1,9 +1,5 @@
package mage.abilities.effects;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.*;
@ -31,6 +27,11 @@ import mage.players.Player;
import mage.target.common.TargetCardInHand;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -54,7 +55,7 @@ public class ContinuousEffects implements Serializable {
private final Map<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> asThoughEffectsMap = new EnumMap<>(AsThoughEffectType.class);
public final List<ContinuousEffectsList<?>> allEffectsLists = new ArrayList<>();
private final ApplyCountersEffect applyCounters;
// private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect;
// private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect;
private final AuraReplacementEffect auraReplacementEffect;
private final List<ContinuousEffect> previous = new ArrayList<>();
@ -134,18 +135,18 @@ public class ContinuousEffects implements Serializable {
spliceCardEffects.removeEndOfCombatEffects();
}
public synchronized void removeEndOfTurnEffects() {
layeredEffects.removeEndOfTurnEffects();
continuousRuleModifyingEffects.removeEndOfTurnEffects();
replacementEffects.removeEndOfTurnEffects();
preventionEffects.removeEndOfTurnEffects();
requirementEffects.removeEndOfTurnEffects();
restrictionEffects.removeEndOfTurnEffects();
public synchronized void removeEndOfTurnEffects(Game game) {
layeredEffects.removeEndOfTurnEffects(game);
continuousRuleModifyingEffects.removeEndOfTurnEffects(game);
replacementEffects.removeEndOfTurnEffects(game);
preventionEffects.removeEndOfTurnEffects(game);
requirementEffects.removeEndOfTurnEffects(game);
restrictionEffects.removeEndOfTurnEffects(game);
for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) {
asThoughtlist.removeEndOfTurnEffects();
asThoughtlist.removeEndOfTurnEffects(game);
}
costModificationEffects.removeEndOfTurnEffects();
spliceCardEffects.removeEndOfTurnEffects();
costModificationEffects.removeEndOfTurnEffects(game);
spliceCardEffects.removeEndOfTurnEffects(game);
}
public synchronized void removeInactiveEffects(Game game) {
@ -163,6 +164,20 @@ public class ContinuousEffects implements Serializable {
spliceCardEffects.removeInactiveEffects(game);
}
public synchronized void incYourTurnNumPlayed(Game game) {
layeredEffects.incYourTurnNumPlayed(game);
continuousRuleModifyingEffects.incYourTurnNumPlayed(game);
replacementEffects.incYourTurnNumPlayed(game);
preventionEffects.incYourTurnNumPlayed(game);
requirementEffects.incYourTurnNumPlayed(game);
restrictionEffects.incYourTurnNumPlayed(game);
for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) {
asThoughtlist.incYourTurnNumPlayed(game);
}
costModificationEffects.incYourTurnNumPlayed(game);
spliceCardEffects.incYourTurnNumPlayed(game);
}
public synchronized List<ContinuousEffect> getLayeredEffects(Game game) {
List<ContinuousEffect> layerEffects = new ArrayList<>();
for (ContinuousEffect effect : layeredEffects) {
@ -322,7 +337,7 @@ public class ContinuousEffects implements Serializable {
}
// boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT);
//get all applicable transient Replacement effects
for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext();) {
for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext(); ) {
ReplacementEffect effect = iterator.next();
if (!effect.checksEventType(event, game)) {
continue;
@ -354,7 +369,7 @@ public class ContinuousEffects implements Serializable {
replaceEffects.put(effect, applicableAbilities);
}
}
for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext();) {
for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext(); ) {
PreventionEffect effect = iterator.next();
if (!effect.checksEventType(event, game)) {
continue;
@ -376,7 +391,7 @@ public class ContinuousEffects implements Serializable {
}
}
if (!applicableAbilities.isEmpty()) {
replaceEffects.put((ReplacementEffect) effect, applicableAbilities);
replaceEffects.put(effect, applicableAbilities);
}
}
return replaceEffects;
@ -478,7 +493,6 @@ public class ContinuousEffects implements Serializable {
}
/**
*
* @param objectId
* @param type
* @param affectedAbility
@ -697,10 +711,10 @@ public class ContinuousEffects implements Serializable {
* Checks if an event won't happen because of an rule modifying effect
*
* @param event
* @param targetAbility ability the event is attached to. can be null.
* @param targetAbility ability the event is attached to. can be null.
* @param game
* @param checkPlayableMode true if the event does not really happen but
* it's checked if the event would be replaced
* it's checked if the event would be replaced
* @return
*/
public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean checkPlayableMode) {
@ -747,7 +761,7 @@ public class ContinuousEffects implements Serializable {
do {
Map<ReplacementEffect, Set<Ability>> rEffects = getApplicableReplacementEffects(event, game);
// Remove all consumed effects (ability dependant)
for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext();) {
for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext(); ) {
ReplacementEffect entry = it1.next();
if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9.
Set<UUID> consumedAbilitiesIds = consumed.get(entry.getId());
@ -938,7 +952,7 @@ public class ContinuousEffects implements Serializable {
if (!waitingEffects.isEmpty()) {
// check if waiting effects can be applied now
for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) {
for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext(); ) {
Map.Entry<ContinuousEffect, Set<UUID>> entry = iterator.next();
if (appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself
appliedAbilities = appliedEffectAbilities.get(entry.getKey());
@ -1059,9 +1073,7 @@ public class ContinuousEffects implements Serializable {
final Card card = game.getPermanentOrLKIBattlefield(ability.getSourceId());
if (!(effect instanceof BecomesFaceDownCreatureEffect)) {
if (card != null) {
if (!card.getAbilities(game).contains(ability)) {
return false;
}
return card.getAbilities(game).contains(ability);
}
}
return true;

View file

@ -41,10 +41,21 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
return new ContinuousEffectsList<>(this);
}
public void removeEndOfTurnEffects() {
public void removeEndOfTurnEffects(Game game) {
// calls every turn on cleanup step (only end of turn duration)
// rules 514.2
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
T entry = i.next();
if (entry.getDuration() == Duration.EndOfTurn) {
boolean canRemove = false;
switch (entry.getDuration()) {
case EndOfTurn:
canRemove = true;
break;
case UntilEndOfYourNextTurn:
canRemove = entry.isYourNextTurn(game);
break;
}
if (canRemove) {
i.remove();
effectAbilityMap.remove(entry.getId());
}
@ -72,6 +83,15 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
}
}
public void incYourTurnNumPlayed(Game game) {
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
T entry = i.next();
if (game.isActivePlayer(entry.getStartingController())) {
entry.incYourTurnNumPlayed();
}
}
}
private boolean isInactive(T effect, Game game) {
Set<Ability> set = effectAbilityMap.get(effect.getId());
if (set == null) {

View file

@ -569,18 +569,20 @@ public class GameState implements Serializable, Copyable<GameState> {
combat.checkForRemoveFromCombat(game);
}
// Remove End of Combat effects
// remove end of combat effects
public void removeEocEffects(Game game) {
effects.removeEndOfCombatEffects();
delayed.removeEndOfCombatAbilities();
game.applyEffects();
}
// remove end of turn effects
public void removeEotEffects(Game game) {
effects.removeEndOfTurnEffects();
delayed.removeEndOfTurnAbilities();
effects.removeEndOfTurnEffects(game);
delayed.removeEndOfTurnAbilities(game);
exile.cleanupEndOfTurnZones(game);
game.applyEffects();
effects.incYourTurnNumPlayed(game);
}
public void addEffect(ContinuousEffect effect, Ability source) {
@ -788,7 +790,7 @@ public class GameState implements Serializable, Copyable<GameState> {
public void addCard(Card card) {
setZone(card.getId(), Zone.OUTSIDE);
for (Ability ability : card.getAbilities()) {
addAbility(ability, card);
addAbility(ability, null, card);
}
}

View file

@ -1,11 +1,5 @@
package mage.game.turn;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import mage.abilities.Ability;
import mage.constants.PhaseStep;
import mage.constants.TurnPhase;
@ -18,8 +12,13 @@ import mage.game.stack.StackObject;
import mage.players.Player;
import mage.util.ThreadLocalStringBuilder;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class Turn implements Serializable {
@ -93,7 +92,6 @@ public class Turn implements Serializable {
}
/**
*
* @param game
* @param activePlayer
* @return true if turn is skipped
@ -105,6 +103,7 @@ public class Turn implements Serializable {
return false;
}
if (game.getState().getTurnMods().skipTurn(activePlayer.getId())) {
game.informPlayers(activePlayer.getLogName() + " skips their turn.");
return true;
@ -239,6 +238,7 @@ public class Turn implements Serializable {
this.play(game, activePlayerId);
}
}*/
/**
* Used for some spells with end turn effect (e.g. Time Stop).
*