Fixed miss copy code in Game object (lki, cards), removed unused code. Possible fixes:

* simulated games was able to change objects from another games (ConcurrentModificationException, related to d202278ccd, details in 3a6cdd2615);
* AI: fixed cards disappear in multiplayer games with computer (details in #6738);
This commit is contained in:
Oleg Agafonov 2021-08-12 00:07:40 +04:00
parent 9f882824a0
commit 1664ee01cf
39 changed files with 201 additions and 125 deletions

View file

@ -15,8 +15,8 @@ public class TwoPlayerDuel extends GameImpl {
this(attackOption, range, mulligan, startLife, 60); this(attackOption, range, mulligan, startLife, 60);
} }
public TwoPlayerDuel(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife, int startingSize) { public TwoPlayerDuel(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startingLife, int startingHandSize) {
super(attackOption, range, mulligan, startLife, startingSize); super(attackOption, range, mulligan, startingLife, startingHandSize);
} }
public TwoPlayerDuel(final TwoPlayerDuel game) { public TwoPlayerDuel(final TwoPlayerDuel game) {

View file

@ -112,7 +112,7 @@ class AjaniStrengthOfThePrideEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if (player == null || player.getLife() < game.getLife() + 15) { if (player == null || player.getLife() < game.getStartingLife() + 15) {
return false; return false;
} }
new ExileSourceEffect().apply(game, source); new ExileSourceEffect().apply(game, source);

View file

@ -72,7 +72,7 @@ enum AngelOfDestinyCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
return player != null && player.getLife() >= game.getLife() + 15; return player != null && player.getLife() >= game.getStartingLife() + 15;
} }
} }

View file

@ -73,7 +73,7 @@ enum AnyaMercilessAngelDynamicValue implements DynamicValue {
if (controller == null) { if (controller == null) {
return 3 * opponentCount; return 3 * opponentCount;
} }
int startingLifeTotal = game.getLife(); int startingLifeTotal = game.getStartingLife();
for (UUID opponentId : game.getOpponents(controller.getId())) { for (UUID opponentId : game.getOpponents(controller.getId())) {
Player opponent = game.getPlayer(opponentId); Player opponent = game.getPlayer(opponentId);
if (opponent != null && opponent.getLife() < startingLifeTotal / 2) { if (opponent != null && opponent.getLife() < startingLifeTotal / 2) {

View file

@ -79,7 +79,7 @@ class AyliEternalPilgrimCondition implements Condition {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if(player != null) { if(player != null) {
return player.getLife() >= game.getLife() + 10; return player.getLife() >= game.getStartingLife() + 10;
} }
return false; return false;
} }

View file

@ -65,7 +65,7 @@ class ChaliceOfLifeEffect extends OneShotEffect {
player.gainLife(1, game, source); player.gainLife(1, game, source);
// if you have at least 10 life more than your starting life total, transform Chalice of Life. // if you have at least 10 life more than your starting life total, transform Chalice of Life.
if (player.getLife() >= game.getLife() + 10) { if (player.getLife() >= game.getStartingLife() + 10) {
permanent.transform(game); permanent.transform(game);
game.informPlayers(permanent.getName() + " transforms into " + permanent.getSecondCardFace().getName()); game.informPlayers(permanent.getName() + " transforms into " + permanent.getSecondCardFace().getName());
} }

View file

@ -47,6 +47,6 @@ enum CosmosElixirCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
return player != null && player.getLife() > game.getLife(); return player != null && player.getLife() > game.getStartingLife();
} }
} }

View file

@ -78,7 +78,7 @@ class ExquisiteArchangelEffect extends ReplacementEffectImpl {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (player != null && sourcePermanent != null) { if (player != null && sourcePermanent != null) {
new ExileSourceEffect().apply(game, source); new ExileSourceEffect().apply(game, source);
player.setLife(game.getLife(), game, source); player.setLife(game.getStartingLife(), game, source);
return true; return true;
} }
return false; return false;

View file

@ -60,7 +60,7 @@ enum GyrudaDoomOfDepthsCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck return deck
.stream() .stream()
.mapToInt(MageObject::getManaValue) .mapToInt(MageObject::getManaValue)

View file

@ -88,7 +88,7 @@ enum HappilyEverAfterCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if (player == null || player.getLife() < game.getLife()) { if (player == null || player.getLife() < game.getStartingLife()) {
return false; return false;
} }
ObjectColor color = new ObjectColor(""); ObjectColor color = new ObjectColor("");

View file

@ -63,7 +63,7 @@ enum JeganthaTheWellspringCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck.stream().noneMatch(JeganthaTheWellspringCompanionCondition::checkCard); return deck.stream().noneMatch(JeganthaTheWellspringCompanionCondition::checkCard);
} }

View file

@ -96,7 +96,7 @@ enum KaheeraTheOrphanguardCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck.stream() return deck.stream()
.filter(card -> card.hasCardTypeForDeckbuilding(CardType.CREATURE)) .filter(card -> card.hasCardTypeForDeckbuilding(CardType.CREATURE))
.allMatch(KaheeraTheOrphanguardCompanionCondition::isCardLegal); .allMatch(KaheeraTheOrphanguardCompanionCondition::isCardLegal);

View file

@ -66,7 +66,7 @@ enum KerugaCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck.stream().allMatch(card -> card.isLand() || card.getManaValue() >= 3); return deck.stream().allMatch(card -> card.isLand() || card.getManaValue() >= 3);
} }
} }

View file

@ -125,7 +125,7 @@ class LongRestEffect extends OneShotEffect {
if (numCards > 0) { if (numCards > 0) {
controller.moveCards(cardsToHand, Zone.HAND, source, game); controller.moveCards(cardsToHand, Zone.HAND, source, game);
if (numCards >= 8) { if (numCards >= 8) {
controller.setLife(game.getLife(), game, source); controller.setLife(game.getStartingLife(), game, source);
} }
return true; return true;
} }

View file

@ -67,7 +67,7 @@ enum LurrusOfTheDreamDenCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck.stream() return deck.stream()
.filter(card -> card.isPermanent()) .filter(card -> card.isPermanent())
.mapToInt(MageObject::getManaValue) .mapToInt(MageObject::getManaValue)

View file

@ -83,7 +83,7 @@ enum LutriTheSpellchaserCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
Map<String, Integer> cardMap = new HashMap<>(); Map<String, Integer> cardMap = new HashMap<>();
deck.stream() deck.stream()
.filter(card -> !card.isLand()) .filter(card -> !card.isLand())

View file

@ -58,7 +58,7 @@ enum OboshThePreypiercerCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck return deck
.stream() .stream()
.filter(card -> !card.isLand()) .filter(card -> !card.isLand())

View file

@ -61,7 +61,7 @@ class OketrasLastMercyEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
controller.setLife(game.getLife(), game, source); controller.setLife(game.getStartingLife(), game, source);
return true; return true;
} }
return false; return false;

View file

@ -62,7 +62,7 @@ enum LifeCondition implements Condition {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player you = game.getPlayer(source.getControllerId()); Player you = game.getPlayer(source.getControllerId());
if (you != null) { if (you != null) {
return you.getLife() >= game.getLife(); return you.getLife() >= game.getStartingLife();
} }
return false; return false;
} }

View file

@ -67,7 +67,7 @@ class ResoluteArchangelEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
controller.setLife(game.getLife(), game, source); controller.setLife(game.getStartingLife(), game, source);
return true; return true;
} }
return false; return false;
@ -82,7 +82,7 @@ enum ControllerLifeLowerThanStrtingLife implements Condition {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
return controller.getLife() < game.getLife(); return controller.getLife() < game.getStartingLife();
} }
return false; return false;
} }

View file

@ -101,6 +101,6 @@ enum RighteousValkyrieCondition implements Condition {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
return player != null && player.getLife() >= game.getLife() + 7; return player != null && player.getLife() >= game.getStartingLife() + 7;
} }
} }

View file

@ -66,7 +66,7 @@ enum SpeakerOfTheHeavensCondition implements Condition {
return false; return false;
} }
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if (player == null || player.getLife() < game.getLife() + 7) { if (player == null || player.getLife() < game.getStartingLife() + 7) {
return false; return false;
} }
return true; return true;

View file

@ -77,7 +77,7 @@ class TorgaarFamineIncarnateEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source)); Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source));
if (targetPlayer != null) { if (targetPlayer != null) {
int startingLifeTotal = game.getLife(); int startingLifeTotal = game.getStartingLife();
targetPlayer.setLife(startingLifeTotal / 2, game, source); targetPlayer.setLife(startingLifeTotal / 2, game, source);
} }
return true; return true;

View file

@ -64,7 +64,7 @@ enum UmoriCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
Set<CardType> cardTypes = new HashSet<>(); Set<CardType> cardTypes = new HashSet<>();
for (Card card : deck) { for (Card card : deck) {
// Lands are fine. // Lands are fine.

View file

@ -72,8 +72,8 @@ enum YorionSkyNomadCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck.size() >= startingSize + 20; return deck.size() >= startingHandSize + 20;
} }
} }

View file

@ -73,7 +73,7 @@ enum ZirdaTheDawnwakerCompanionCondition implements CompanionCondition {
} }
@Override @Override
public boolean isLegal(Set<Card> deck, int startingSize) { public boolean isLegal(Set<Card> deck, int startingHandSize) {
return deck return deck
.stream() .stream()
.filter(card -> card.isPermanent()) .filter(card -> card.isPermanent())

View file

@ -13,6 +13,7 @@ import mage.constants.SubTypeSet;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.game.Game; import mage.game.Game;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.util.Copyable;
import mage.util.SubTypes; import mage.util.SubTypes;
import java.io.Serializable; import java.io.Serializable;
@ -21,7 +22,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public interface MageObject extends MageItem, Serializable { public interface MageObject extends MageItem, Serializable, Copyable<MageObject> {
String getName(); String getName();
@ -136,6 +137,7 @@ public interface MageObject extends MageItem, Serializable {
void adjustTargets(Ability ability, Game game); void adjustTargets(Ability ability, Game game);
// memory object copy (not mtg) // memory object copy (not mtg)
@Override
MageObject copy(); MageObject copy();
// copied card info (mtg) // copied card info (mtg)

View file

@ -41,8 +41,8 @@ public class CompanionAbility extends SpecialAction {
return "Companion &mdash; " + companionCondition.getRule(); return "Companion &mdash; " + companionCondition.getRule();
} }
public boolean isLegal(Set<Card> cards, int startingSize) { public boolean isLegal(Set<Card> cards, int startingHandSize) {
return companionCondition.isLegal(cards, startingSize); return companionCondition.isLegal(cards, startingHandSize);
} }
} }

View file

@ -19,8 +19,8 @@ public interface CompanionCondition extends Serializable {
/** /**
* @param deck The set of cards to check. * @param deck The set of cards to check.
* @param startingSize * @param startingHandSize
* @return Whether the companion is valid for that deck. * @return Whether the companion is valid for that deck.
*/ */
boolean isLegal(Set<Card> deck, int startingSize); boolean isLegal(Set<Card> deck, int startingHandSize);
} }

View file

@ -64,9 +64,7 @@ public class MageDrawAction extends MageAction {
if (!player.isTopCardRevealed() && numDrawn > 0) { if (!player.isTopCardRevealed() && numDrawn > 0) {
game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : "")); game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
} }
setScore(player, score); setScore(player, score);
game.setStateCheckRequired();
} }
return numDrawn; return numDrawn;
} }

View file

@ -36,6 +36,9 @@ public abstract class MeldCard extends CardImpl {
this.halves = new CardsImpl(card.halves); this.halves = new CardsImpl(card.halves);
} }
@Override
public abstract MeldCard copy();
public void setMelded(boolean isMelded, Game game) { public void setMelded(boolean isMelded, Game game) {
game.getState().getCardState(getId()).setMelded(isMelded); game.getState().getCardState(getId()).setMelded(isMelded);
} }

View file

@ -16,7 +16,6 @@ import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.game.Game; import mage.game.Game;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.util.Copyable;
import mage.util.GameLog; import mage.util.GameLog;
import mage.util.SubTypes; import mage.util.SubTypes;
@ -28,7 +27,7 @@ import java.util.UUID;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
public abstract class Designation implements MageObject, Copyable<Designation> { public abstract class Designation implements MageObject {
private static final List<CardType> emptySet = new ArrayList<>(); private static final List<CardType> emptySet = new ArrayList<>();
private static final ObjectColor emptyColor = new ObjectColor(); private static final ObjectColor emptyColor = new ObjectColor();
@ -69,6 +68,9 @@ public abstract class Designation implements MageObject, Copyable<Designation> {
this.unique = designation.unique; this.unique = designation.unique;
} }
@Override
public abstract Designation copy();
@Override @Override
public FrameStyle getFrameStyle() { public FrameStyle getFrameStyle() {
return frameStyle; return frameStyle;

View file

@ -12,12 +12,13 @@ import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.counters.Counter; import mage.counters.Counter;
import mage.counters.Counters; import mage.counters.Counters;
import mage.util.Copyable;
/** /**
* *
* @author BetaSteward * @author BetaSteward
*/ */
public class CardState implements Serializable { public class CardState implements Serializable, Copyable<CardState> {
protected boolean faceDown; protected boolean faceDown;
protected Map<String, String> info; protected Map<String, String> info;
@ -50,6 +51,7 @@ public class CardState implements Serializable {
this.melded = state.melded; this.melded = state.melded;
} }
@Override
public CardState copy() { public CardState copy() {
return new CardState(this); return new CardState(this);
} }

View file

@ -51,7 +51,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
int getNumPlayers(); int getNumPlayers();
int getLife(); int getStartingLife();
RangeOfInfluence getRangeOfInfluence(); RangeOfInfluence getRangeOfInfluence();
@ -244,10 +244,6 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Player getLosingPlayer(); Player getLosingPlayer();
void setStateCheckRequired();
boolean getStateCheckRequired();
//client event methods //client event methods
void addTableEventListener(Listener<TableEvent> listener); void addTableEventListener(Listener<TableEvent> listener);

View file

@ -28,8 +28,8 @@ public abstract class GameCommanderImpl extends GameImpl {
protected boolean startingPlayerSkipsDraw = true; protected boolean startingPlayerSkipsDraw = true;
public GameCommanderImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife, int startingSize) { public GameCommanderImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startingLife, int startingHandSize) {
super(attackOption, range, mulligan, startLife, startingSize); super(attackOption, range, mulligan, startingLife, startingHandSize);
} }
public GameCommanderImpl(final GameCommanderImpl game) { public GameCommanderImpl(final GameCommanderImpl game) {

View file

@ -59,7 +59,10 @@ import mage.target.Target;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.TargetPlayer; import mage.target.TargetPlayer;
import mage.util.*; import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.MessageToClient;
import mage.util.RandomUtil;
import mage.util.functions.CopyApplier; import mage.util.functions.CopyApplier;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import mage.watchers.common.*; import mage.watchers.common.*;
@ -71,14 +74,23 @@ import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Game object. It must contain static data (e.g. no changeable in the game like game settings)
* <p>
* "transient field" logic uses for serialization/replays (mark temporary fields as transient,
* also look for non restored fields in copy constructor for details)
* <p>
* WARNING, if you add new fields then don't forget to add it to copy constructor (deep copy, not ref).
* If it's a temporary/auto-generated data then mark that field as transient and comment in copy constructor.
*/
public abstract class GameImpl implements Game { public abstract class GameImpl implements Game {
private static final int ROLLBACK_TURNS_MAX = 4; private static final int ROLLBACK_TURNS_MAX = 4;
private static final String UNIT_TESTS_ERROR_TEXT = "Error in unit tests"; private static final String UNIT_TESTS_ERROR_TEXT = "Error in unit tests";
private static final Logger logger = Logger.getLogger(GameImpl.class); private static final Logger logger = Logger.getLogger(GameImpl.class);
private transient Object customData; private transient Object customData; // temporary data, used in AI simulations
private transient Player losingPlayer; // temporary data, used in AI simulations
protected boolean simulation = false; protected boolean simulation = false;
protected boolean checkPlayableState = false; protected boolean checkPlayableState = false;
@ -95,18 +107,21 @@ public abstract class GameImpl implements Game {
protected Map<Zone, Map<UUID, CardState>> lkiCardState = new EnumMap<>(Zone.class); protected Map<Zone, Map<UUID, CardState>> lkiCardState = new EnumMap<>(Zone.class);
protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>(); protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>();
// Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly) // Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
protected Map<Zone, Set<UUID>> shortLivingLKI = new EnumMap<>(Zone.class); protected Map<Zone, Set<UUID>> lkiShortLiving = new EnumMap<>(Zone.class);
// Permanents entering the Battlefield while handling replacement effects before they are added to the battlefield // Permanents entering the Battlefield while handling replacement effects before they are added to the battlefield
protected Map<UUID, Permanent> permanentsEntering = new HashMap<>(); protected Map<UUID, Permanent> permanentsEntering = new HashMap<>();
// used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist)
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
protected GameState state; protected GameState state;
private transient Stack<Integer> savedStates = new Stack<>(); private transient Stack<Integer> savedStates = new Stack<>();
protected transient GameStates gameStates = new GameStates(); protected transient GameStates gameStates = new GameStates();
// game states to allow player rollback // game states to allow player rollback
protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>(); protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>();
protected boolean executingRollback; protected transient boolean executingRollback;
protected int turnToGoToForRollback; protected transient int turnToGoToForRollback;
protected Date startTime; protected Date startTime;
protected Date endTime; protected Date endTime;
@ -121,73 +136,126 @@ public abstract class GameImpl implements Game {
protected GameOptions gameOptions; protected GameOptions gameOptions;
protected String startMessage; protected String startMessage;
// private final transient LinkedList<MageAction> actions; private boolean scopeRelevant = false; // replacement effects: used to indicate that currently applied replacement effects have to check for scope relevance (614.12 13/01/18)
private Player scorePlayer; private boolean saveGame = false; // replay code, not done
// private int score = 0; private int priorityTime; // match time limit
private Player losingPlayer; private final int startingLife;
private boolean stateCheckRequired = false; private final int startingHandSize;
protected transient PlayerList playerList; // auto-generated from state, don't copy
// used to indicate that currently applied replacement effects have to check for scope relevance (614.12 13/01/18) // infinite loop check (temporary data, do not copy)
private boolean scopeRelevant = false; private transient int infiniteLoopCounter; // used to check if the game is in an infinite loop
private boolean saveGame = false; private transient int lastNumberOfAbilitiesOnTheStack; // used to check how long no new ability was put to stack
private int priorityTime; private transient List<Integer> lastPlayersLifes = null; // if life is going down, it's no infinite loop
private transient final LinkedList<UUID> stackObjectsCheck = new LinkedList<>(); // used to check if different sources used the stack
private final int startLife;
private final int startingSize;
protected PlayerList playerList; // auto-generated from state, don't copy
// infinite loop check (no copy of this attributes neccessary)
private int infiniteLoopCounter; // used to check if the game is in an infinite loop
private int lastNumberOfAbilitiesOnTheStack; // used to check how long no new ability was put to stack
private List<Integer> lastPlayersLifes = null; // if life is going down, it's no infinite loop
private final LinkedList<UUID> stackObjectsCheck = new LinkedList<>(); // used to check if different sources used the stack
// used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist)
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
// temporary store for income concede commands, don't copy // temporary store for income concede commands, don't copy
private final LinkedList<UUID> concedingPlayers = new LinkedList<>(); private final LinkedList<UUID> concedingPlayers = new LinkedList<>();
public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife, int startingSize) { public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startingLife, int startingHandSize) {
this.id = UUID.randomUUID(); this.id = UUID.randomUUID();
this.range = range; this.range = range;
this.mulligan = mulligan; this.mulligan = mulligan;
this.attackOption = attackOption; this.attackOption = attackOption;
this.state = new GameState(); this.state = new GameState();
this.startLife = startLife; this.startingLife = startingLife;
this.executingRollback = false; this.executingRollback = false;
this.startingSize = startingSize; this.startingHandSize = startingHandSize;
initGameDefaultWatchers(); initGameDefaultWatchers();
} }
public GameImpl(final GameImpl game) { public GameImpl(final GameImpl game) {
this.id = game.id; //this.customData = game.customData; // temporary data, no need on game copy
this.ready = game.ready; //this.losingPlayer = game.losingPlayer; // temporary data, no need on game copy
this.startingPlayerId = game.startingPlayerId;
this.winnerId = game.winnerId;
this.range = game.range;
this.mulligan = game.getMulligan().copy();
this.attackOption = game.attackOption;
this.state = game.state.copy();
this.gameCards = game.gameCards;
this.simulation = game.simulation; this.simulation = game.simulation;
this.checkPlayableState = game.checkPlayableState; this.checkPlayableState = game.checkPlayableState;
this.gameOptions = game.gameOptions;
this.lki.putAll(game.lki);
this.lkiExtended.putAll(game.lkiExtended);
this.lkiCardState.putAll(game.lkiCardState);
this.shortLivingLKI.putAll(game.shortLivingLKI);
this.permanentsEntering.putAll(game.permanentsEntering);
this.stateCheckRequired = game.stateCheckRequired; this.id = game.id;
this.scorePlayer = game.scorePlayer;
this.scopeRelevant = game.scopeRelevant; this.ready = game.ready;
this.priorityTime = game.priorityTime; //this.tableEventSource = game.tableEventSource; // client-server part, not need on copy/simulations
this.saveGame = game.saveGame; //this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations
this.startLife = game.startLife;
this.enterWithCounters.putAll(game.enterWithCounters); for (Entry<UUID, Card> entry : game.gameCards.entrySet()) {
this.startingSize = game.startingSize; this.gameCards.put(entry.getKey(), entry.getValue().copy());
}
for (Entry<UUID, MeldCard> entry : game.meldCards.entrySet()) {
this.meldCards.put(entry.getKey(), entry.getValue().copy());
}
// lki
for (Entry<Zone, Map<UUID, MageObject>> entry : game.lki.entrySet()) {
Map<UUID, MageObject> lkiMap = new HashMap<>();
for (Entry<UUID, MageObject> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lki.put(entry.getKey(), lkiMap);
}
// lkiCardState
for (Entry<Zone, Map<UUID, CardState>> entry : game.lkiCardState.entrySet()) {
Map<UUID, CardState> lkiMap = new HashMap<>();
for (Entry<UUID, CardState> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiCardState.put(entry.getKey(), lkiMap);
}
// lkiExtended
for (Entry<UUID, Map<Integer, MageObject>> entry : game.lkiExtended.entrySet()) {
Map<Integer, MageObject> lkiMap = new HashMap<>();
for (Entry<Integer, MageObject> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiExtended.put(entry.getKey(), lkiMap);
}
// lkiShortLiving
for (Entry<Zone, Set<UUID>> entry : game.lkiShortLiving.entrySet()) {
this.lkiShortLiving.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
for (Entry<UUID, Permanent> entry : game.permanentsEntering.entrySet()) {
this.permanentsEntering.put(entry.getKey(), entry.getValue().copy());
}
for (Entry<UUID, Counters> entry : game.enterWithCounters.entrySet()) {
this.enterWithCounters.put(entry.getKey(), entry.getValue().copy());
}
this.state = game.state.copy();
// client-server part, not need on copy/simulations:
/*
this.savedStates = game.savedStates;
this.gameStates = game.gameStates;
this.gameStatesRollBack = game.gameStatesRollBack;
this.executingRollback = game.executingRollback;
this.turnToGoToForRollback = game.turnToGoToForRollback;
*/
this.startTime = game.startTime;
this.endTime = game.endTime;
this.startingPlayerId = game.startingPlayerId;
this.winnerId = game.winnerId;
this.gameStopped = game.gameStopped; this.gameStopped = game.gameStopped;
this.range = game.range;
this.mulligan = game.mulligan.copy();
this.attackOption = game.attackOption;
this.gameOptions = game.gameOptions.copy();
this.startMessage = game.startMessage;
this.scopeRelevant = game.scopeRelevant;
this.saveGame = game.saveGame;
this.priorityTime = game.priorityTime;
this.startingLife = game.startingLife;
this.startingHandSize = game.startingHandSize;
//this.playerList = game.playerList; // auto-generated list, don't copy
// loop check code, no need to copy
/*
this.infiniteLoopCounter = game.infiniteLoopCounter;
this.lastNumberOfAbilitiesOnTheStack = game.lastNumberOfAbilitiesOnTheStack;
this.lastPlayersLifes = game.lastPlayersLifes;
this.stackObjectsCheck = game.stackObjectsCheck;
*/
} }
@Override @Override
@ -605,7 +673,7 @@ public abstract class GameImpl implements Game {
// copied cards removes, but delayed triggered possible from it, see https://github.com/magefree/mage/issues/5437 // copied cards removes, but delayed triggered possible from it, see https://github.com/magefree/mage/issues/5437
// TODO: remove that workround after LKI rework, see GameState.copyCard // TODO: remove that workround after LKI rework, see GameState.copyCard
if (card == null) { if (card == null) {
card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId.toString()); card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId);
} }
return card; return card;
} }
@ -845,7 +913,6 @@ public abstract class GameImpl implements Game {
public void start(UUID choosingPlayerId) { public void start(UUID choosingPlayerId) {
startTime = new Date(); startTime = new Date();
if (state.getPlayers().values().iterator().hasNext()) { if (state.getPlayers().values().iterator().hasNext()) {
scorePlayer = state.getPlayers().values().iterator().next();
init(choosingPlayerId); init(choosingPlayerId);
play(startingPlayerId); play(startingPlayerId);
} }
@ -1039,7 +1106,7 @@ public abstract class GameImpl implements Game {
for (Ability ability : card.getAbilities(this)) { for (Ability ability : card.getAbilities(this)) {
if (ability instanceof CompanionAbility) { if (ability instanceof CompanionAbility) {
CompanionAbility companionAbility = (CompanionAbility) ability; CompanionAbility companionAbility = (CompanionAbility) ability;
if (companionAbility.isLegal(new HashSet<>(player.getLibrary().getCards(this)), startingSize)) { if (companionAbility.isLegal(new HashSet<>(player.getLibrary().getCards(this)), startingHandSize)) {
potentialCompanions.add(card); potentialCompanions.add(card);
break; break;
} }
@ -1123,7 +1190,7 @@ public abstract class GameImpl implements Game {
for (UUID playerId : state.getPlayerList(startingPlayerId)) { for (UUID playerId : state.getPlayerList(startingPlayerId)) {
Player player = getPlayer(playerId); Player player = getPlayer(playerId);
if (!gameOptions.testMode || player.getLife() == 0) { if (!gameOptions.testMode || player.getLife() == 0) {
player.initLife(this.getLife()); player.initLife(this.getStartingLife());
} }
if (!gameOptions.testMode) { if (!gameOptions.testMode) {
player.drawCards(startingHandSize, null, this); player.drawCards(startingHandSize, null, this);
@ -3128,7 +3195,7 @@ public abstract class GameImpl implements Game {
@Override @Override
public boolean getShortLivingLKI(UUID objectId, Zone zone) { public boolean getShortLivingLKI(UUID objectId, Zone zone) {
Set<UUID> idSet = shortLivingLKI.get(zone); Set<UUID> idSet = lkiShortLiving.get(zone);
if (idSet != null) { if (idSet != null) {
return idSet.contains(objectId); return idSet.contains(objectId);
} }
@ -3153,7 +3220,7 @@ public abstract class GameImpl implements Game {
// remembers if a object was in a zone during the resolution of an effect // remembers if a object was in a zone during the resolution of an effect
// e.g. Wrath destroys all and you the question is is the replacement effect to apply because the source was also moved by the same effect // e.g. Wrath destroys all and you the question is is the replacement effect to apply because the source was also moved by the same effect
// because it happens all at the same time the replacement effect has still to be applied // because it happens all at the same time the replacement effect has still to be applied
Set<UUID> idSet = shortLivingLKI.computeIfAbsent(zone, k -> new HashSet<>()); Set<UUID> idSet = lkiShortLiving.computeIfAbsent(zone, k -> new HashSet<>());
idSet.add(objectId); idSet.add(objectId);
if (object instanceof Permanent) { if (object instanceof Permanent) {
Map<Integer, MageObject> lkiExtendedMap = lkiExtended.computeIfAbsent(objectId, k -> new HashMap<>()); Map<Integer, MageObject> lkiExtendedMap = lkiExtended.computeIfAbsent(objectId, k -> new HashMap<>());
@ -3184,7 +3251,7 @@ public abstract class GameImpl implements Game {
@Override @Override
public void resetShortLivingLKI() { public void resetShortLivingLKI() {
shortLivingLKI.clear(); lkiShortLiving.clear();
} }
@Override @Override
@ -3336,16 +3403,6 @@ public abstract class GameImpl implements Game {
playerQueryEventSource.informPlayer(player.getId(), message); playerQueryEventSource.informPlayer(player.getId(), message);
} }
@Override
public boolean getStateCheckRequired() {
return stateCheckRequired;
}
@Override
public void setStateCheckRequired() {
stateCheckRequired = true;
}
/** /**
* If true, only self scope replacement effects are applied * If true, only self scope replacement effects are applied
* *
@ -3420,8 +3477,8 @@ public abstract class GameImpl implements Game {
} }
@Override @Override
public int getLife() { public int getStartingLife() {
return startLife; return startingLife;
} }
@Override @Override

View file

@ -1,6 +1,7 @@
package mage.game; package mage.game;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
@ -12,7 +13,7 @@ import java.util.Set;
* *
* @author ayratn * @author ayratn
*/ */
public class GameOptions implements Serializable { public class GameOptions implements Serializable, Copyable<GameOptions> {
private static final GameOptions deinstance = new GameOptions(); private static final GameOptions deinstance = new GameOptions();
@ -56,4 +57,22 @@ public class GameOptions implements Serializable {
*/ */
public boolean planeChase = false; public boolean planeChase = false;
public GameOptions() {
super();
}
private GameOptions(final GameOptions options) {
this.testMode = options.testMode;
this.stopOnTurn = options.stopOnTurn;
this.stopAtStep = options.stopAtStep;
this.skipInitShuffling = options.skipInitShuffling;
this.rollbackTurnsAllowed = options.rollbackTurnsAllowed;
this.bannedUsers.addAll(options.bannedUsers);
this.planeChase = options.planeChase;
}
@Override
public GameOptions copy() {
return new GameOptions(this);
}
} }

View file

@ -440,7 +440,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canLoseLife = true; this.canLoseLife = true;
this.topCardRevealed = false; this.topCardRevealed = false;
this.payManaMode = false; this.payManaMode = false;
this.setLife(game.getLife(), game, null); this.setLife(game.getStartingLife(), game, null);
this.setReachedNextTurnAfterLeaving(false); this.setReachedNextTurnAfterLeaving(false);
this.clearCastSourceIdManaCosts(); this.clearCastSourceIdManaCosts();

View file

@ -1,9 +1,6 @@
package mage.util; package mage.util;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@FunctionalInterface @FunctionalInterface