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);
}
public TwoPlayerDuel(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife, int startingSize) {
super(attackOption, range, mulligan, startLife, startingSize);
public TwoPlayerDuel(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startingLife, int startingHandSize) {
super(attackOption, range, mulligan, startingLife, startingHandSize);
}
public TwoPlayerDuel(final TwoPlayerDuel game) {

View file

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

View file

@ -72,7 +72,7 @@ enum AngelOfDestinyCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
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) {
return 3 * opponentCount;
}
int startingLifeTotal = game.getLife();
int startingLifeTotal = game.getStartingLife();
for (UUID opponentId : game.getOpponents(controller.getId())) {
Player opponent = game.getPlayer(opponentId);
if (opponent != null && opponent.getLife() < startingLifeTotal / 2) {

View file

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

View file

@ -65,7 +65,7 @@ class ChaliceOfLifeEffect extends OneShotEffect {
player.gainLife(1, game, source);
// 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);
game.informPlayers(permanent.getName() + " transforms into " + permanent.getSecondCardFace().getName());
}

View file

@ -47,6 +47,6 @@ enum CosmosElixirCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
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());
if (player != null && sourcePermanent != null) {
new ExileSourceEffect().apply(game, source);
player.setLife(game.getLife(), game, source);
player.setLife(game.getStartingLife(), game, source);
return true;
}
return false;

View file

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

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ enum KerugaCondition implements CompanionCondition {
}
@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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -101,6 +101,6 @@ enum RighteousValkyrieCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
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;
}
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 true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,8 +41,8 @@ public class CompanionAbility extends SpecialAction {
return "Companion &mdash; " + companionCondition.getRule();
}
public boolean isLegal(Set<Card> cards, int startingSize) {
return companionCondition.isLegal(cards, startingSize);
public boolean isLegal(Set<Card> cards, int startingHandSize) {
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 startingSize
* @param startingHandSize
* @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) {
game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
}
setScore(player, score);
game.setStateCheckRequired();
}
return numDrawn;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -59,7 +59,10 @@ import mage.target.Target;
import mage.target.TargetCard;
import mage.target.TargetPermanent;
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.watchers.Watcher;
import mage.watchers.common.*;
@ -71,14 +74,23 @@ import java.util.*;
import java.util.Map.Entry;
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 {
private static final int ROLLBACK_TURNS_MAX = 4;
private static final String UNIT_TESTS_ERROR_TEXT = "Error in unit tests";
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 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<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)
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
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;
private transient Stack<Integer> savedStates = new Stack<>();
protected transient GameStates gameStates = new GameStates();
// game states to allow player rollback
protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>();
protected boolean executingRollback;
protected int turnToGoToForRollback;
protected transient boolean executingRollback;
protected transient int turnToGoToForRollback;
protected Date startTime;
protected Date endTime;
@ -121,73 +136,126 @@ public abstract class GameImpl implements Game {
protected GameOptions gameOptions;
protected String startMessage;
// private final transient LinkedList<MageAction> actions;
private Player scorePlayer;
// private int score = 0;
private Player losingPlayer;
private boolean stateCheckRequired = false;
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 boolean saveGame = false; // replay code, not done
private int priorityTime; // match time limit
private final int startingLife;
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)
private boolean scopeRelevant = false;
private boolean saveGame = false;
private int priorityTime;
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<>();
// infinite loop check (temporary data, do not copy)
private transient int infiniteLoopCounter; // used to check if the game is in an infinite loop
private transient int lastNumberOfAbilitiesOnTheStack; // used to check how long no new ability was put to stack
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
// temporary store for income concede commands, don't copy
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.range = range;
this.mulligan = mulligan;
this.attackOption = attackOption;
this.state = new GameState();
this.startLife = startLife;
this.startingLife = startingLife;
this.executingRollback = false;
this.startingSize = startingSize;
this.startingHandSize = startingHandSize;
initGameDefaultWatchers();
}
public GameImpl(final GameImpl game) {
this.id = game.id;
this.ready = game.ready;
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.customData = game.customData; // temporary data, no need on game copy
//this.losingPlayer = game.losingPlayer; // temporary data, no need on game copy
this.simulation = game.simulation;
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.scorePlayer = game.scorePlayer;
this.scopeRelevant = game.scopeRelevant;
this.priorityTime = game.priorityTime;
this.saveGame = game.saveGame;
this.startLife = game.startLife;
this.enterWithCounters.putAll(game.enterWithCounters);
this.startingSize = game.startingSize;
this.id = game.id;
this.ready = game.ready;
//this.tableEventSource = game.tableEventSource; // client-server part, not need on copy/simulations
//this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations
for (Entry<UUID, Card> entry : game.gameCards.entrySet()) {
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.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
@ -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
// TODO: remove that workround after LKI rework, see GameState.copyCard
if (card == null) {
card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId.toString());
card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId);
}
return card;
}
@ -845,7 +913,6 @@ public abstract class GameImpl implements Game {
public void start(UUID choosingPlayerId) {
startTime = new Date();
if (state.getPlayers().values().iterator().hasNext()) {
scorePlayer = state.getPlayers().values().iterator().next();
init(choosingPlayerId);
play(startingPlayerId);
}
@ -1039,7 +1106,7 @@ public abstract class GameImpl implements Game {
for (Ability ability : card.getAbilities(this)) {
if (ability instanceof CompanionAbility) {
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);
break;
}
@ -1123,7 +1190,7 @@ public abstract class GameImpl implements Game {
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
Player player = getPlayer(playerId);
if (!gameOptions.testMode || player.getLife() == 0) {
player.initLife(this.getLife());
player.initLife(this.getStartingLife());
}
if (!gameOptions.testMode) {
player.drawCards(startingHandSize, null, this);
@ -3128,7 +3195,7 @@ public abstract class GameImpl implements Game {
@Override
public boolean getShortLivingLKI(UUID objectId, Zone zone) {
Set<UUID> idSet = shortLivingLKI.get(zone);
Set<UUID> idSet = lkiShortLiving.get(zone);
if (idSet != null) {
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
// 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
Set<UUID> idSet = shortLivingLKI.computeIfAbsent(zone, k -> new HashSet<>());
Set<UUID> idSet = lkiShortLiving.computeIfAbsent(zone, k -> new HashSet<>());
idSet.add(objectId);
if (object instanceof Permanent) {
Map<Integer, MageObject> lkiExtendedMap = lkiExtended.computeIfAbsent(objectId, k -> new HashMap<>());
@ -3184,7 +3251,7 @@ public abstract class GameImpl implements Game {
@Override
public void resetShortLivingLKI() {
shortLivingLKI.clear();
lkiShortLiving.clear();
}
@Override
@ -3336,16 +3403,6 @@ public abstract class GameImpl implements Game {
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
*
@ -3420,8 +3477,8 @@ public abstract class GameImpl implements Game {
}
@Override
public int getLife() {
return startLife;
public int getStartingLife() {
return startingLife;
}
@Override

View file

@ -1,6 +1,7 @@
package mage.game;
import mage.constants.PhaseStep;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.Collections;
@ -12,7 +13,7 @@ import java.util.Set;
*
* @author ayratn
*/
public class GameOptions implements Serializable {
public class GameOptions implements Serializable, Copyable<GameOptions> {
private static final GameOptions deinstance = new GameOptions();
@ -50,10 +51,28 @@ public class GameOptions implements Serializable {
* Names of users banned from participating in the game
*/
public Set<String> bannedUsers = Collections.emptySet();
/**
* Use planechase variant
*/
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.topCardRevealed = false;
this.payManaMode = false;
this.setLife(game.getLife(), game, null);
this.setLife(game.getStartingLife(), game, null);
this.setReachedNextTurnAfterLeaving(false);
this.clearCastSourceIdManaCosts();

View file

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