* Added optional rollback current turn and up to 3 previous turns to the battlefield menu. All other players have to agree to the rollback to let it happen.

This commit is contained in:
LevelX2 2015-06-07 00:53:08 +02:00
parent 5736efa103
commit 8acf28eed1
38 changed files with 661 additions and 252 deletions

View file

@ -39,6 +39,7 @@ public enum PlayerAction {
PASS_PRIORITY_UNTIL_NEXT_TURN,
PASS_PRIORITY_UNTIL_STACK_RESOLVED,
PASS_PRIORITY_CANCEL_ALL_ACTIONS,
ROLLBACK_TURNS,
UNDO,
CONCEDE,
MANA_AUTO_PAYMENT_ON,
@ -46,7 +47,10 @@ public enum PlayerAction {
RESET_AUTO_SELECT_REPLACEMENT_EFFECTS,
REVOKE_PERMISSIONS_TO_SEE_HAND_CARDS,
REQUEST_PERMISSION_TO_SEE_HAND_CARDS,
REQUEST_PERMISSION_TO_ROLLBACK_TURN,
ADD_PERMISSION_TO_SEE_HAND_CARDS,
ADD_PERMISSION_TO_ROLLBACK_TURN,
DENY_PERMISSON_TO_ROLLBACK_TURN,
PERMISSION_REQUESTS_ALLOWED_ON,
PERMISSION_REQUESTS_ALLOWED_OFF
}

View file

@ -208,9 +208,7 @@ public interface Game extends MageItem, Serializable {
*/
PreventionEffectData preventDamage(GameEvent event, Ability source, Game game, boolean preventAllDamage);
//game play methods
void start(UUID choosingPlayerId);
void start(UUID choosingPlayerId, GameOptions options);
void resume();
void pause();
boolean isPaused();
@ -293,5 +291,10 @@ public interface Game extends MageItem, Serializable {
int getPriorityTime();
void setPriorityTime(int priorityTime);
UUID getStartingPlayerId();
void saveRollBackGameState();
boolean canRollbackTurns(int turnsToRollback);
void rollbackTurns(int turnsToRollback);
boolean executingRollback();
}
}

View file

@ -75,7 +75,7 @@ public abstract class GameCommanderImpl extends GameImpl {
}
@Override
protected void init(UUID choosingPlayerId, GameOptions gameOptions) {
protected void init(UUID choosingPlayerId) {
Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects"));
//Move commander to command zone
for (UUID playerId: state.getPlayerList(startingPlayerId)) {
@ -101,7 +101,7 @@ public abstract class GameCommanderImpl extends GameImpl {
}
this.getState().addAbility(ability, null);
super.init(choosingPlayerId, gameOptions);
super.init(choosingPlayerId);
if (startingPlayerSkipsDraw) {
state.getTurnMods().add(new TurnMod(startingPlayerId, PhaseStep.DRAW));
}

View file

@ -116,6 +116,7 @@ import mage.players.Players;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.TargetPlayer;
import mage.util.GameLog;
import mage.util.functions.ApplyToPermanent;
import mage.watchers.Watchers;
import mage.watchers.common.BlockedAttackerWatcher;
@ -129,6 +130,8 @@ import org.apache.log4j.Logger;
public abstract class GameImpl implements Game, Serializable {
private static final int ROLLBACK_TURNS_MAX = 4;
private static final transient Logger logger = Logger.getLogger(GameImpl.class);
private static final FilterPermanent filterAura = new FilterPermanent();
@ -155,7 +158,8 @@ public abstract class GameImpl implements Game, Serializable {
private transient Object customData;
protected boolean simulation = false;
protected final UUID id;
protected final UUID id;
protected boolean ready;
protected transient TableEventSource tableEventSource = new TableEventSource();
protected transient PlayerQueryEventSource playerQueryEventSource = new PlayerQueryEventSource();
@ -170,6 +174,9 @@ public abstract class GameImpl implements Game, Serializable {
protected GameState state;
private transient Stack<Integer> savedStates = new Stack<>();
protected transient GameStates gameStates = new GameStates();
// game states to allow player roll back
protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>();
protected boolean executingRollback;
protected Date startTime;
protected Date endTime;
@ -206,7 +213,7 @@ public abstract class GameImpl implements Game, Serializable {
this.attackOption = attackOption;
this.state = new GameState();
this.startLife = startLife;
// this.actions = new LinkedList<MageAction>();
this.executingRollback = false;
}
public GameImpl(final GameImpl game) {
@ -232,7 +239,6 @@ public abstract class GameImpl implements Game, Serializable {
copyCount++;
copyTime += (System.currentTimeMillis() - t1);
}
// this.actions = new LinkedList<MageAction>();
this.stateCheckRequired = game.stateCheckRequired;
this.scorePlayer = game.scorePlayer;
this.scopeRelevant = game.scopeRelevant;
@ -268,7 +274,10 @@ public abstract class GameImpl implements Game, Serializable {
@Override
public GameOptions getOptions() {
return gameOptions;
if (gameOptions != null) {
return gameOptions;
}
return new GameOptions(); // happens during the first game updates
}
@Override
@ -610,23 +619,17 @@ public abstract class GameImpl implements Game, Serializable {
return 0;
}
@Override
public void start(UUID choosingPlayerId) {
start(choosingPlayerId, this.gameOptions != null ? gameOptions : GameOptions.getDefault());
}
@Override
public void cleanUp() {
gameCards.clear();
}
@Override
public void start(UUID choosingPlayerId, GameOptions options) {
public void start(UUID choosingPlayerId) {
startTime = new Date();
this.gameOptions = options;
if (state.getPlayers().values().iterator().hasNext()) {
scorePlayer = state.getPlayers().values().iterator().next();
init(choosingPlayerId, options);
init(choosingPlayerId);
play(startingPlayerId);
}
}
@ -731,13 +734,26 @@ public abstract class GameImpl implements Game, Serializable {
}
private boolean playTurn(Player player) {
this.logStartOfTurn(player);
if (checkStopOnTurnOption()) {
return false;
}
state.setActivePlayerId(player.getId());
player.becomesActivePlayer();
state.getTurn().play(this, player.getId());
do {
if (executingRollback) {
executingRollback = false;
player = getPlayer(state.getActivePlayerId());
for (Player playerObject: getPlayers().values()) {
if (playerObject.isInGame()) {
playerObject.abortReset();
}
}
} else {
state.setActivePlayerId(player.getId());
saveRollBackGameState();
}
this.logStartOfTurn(player);
if (checkStopOnTurnOption()) {
return false;
}
state.getTurn().play(this, player);
} while (executingRollback);
if (isPaused() || gameOver(null)) {
return false;
}
@ -778,7 +794,7 @@ public abstract class GameImpl implements Game, Serializable {
return false;
}
protected void init(UUID choosingPlayerId, GameOptions gameOptions) {
protected void init(UUID choosingPlayerId) {
for (Player player: state.getPlayers().values()) {
player.beginTurn(this);
// init only if match is with timer (>0) and time left was not set yet (== MAX_VALUE).
@ -1151,6 +1167,9 @@ public abstract class GameImpl implements Game, Serializable {
}
// resetPassed should be called if player performs any action
if (player.priority(this)) {
if(executingRollback()) {
return;
}
applyEffects();
}
if (isPaused()) {
@ -2566,6 +2585,51 @@ public abstract class GameImpl implements Game, Serializable {
}
}
@Override
public void saveRollBackGameState() {
if (gameOptions.rollbackTurnsAllowed) {
int toDelete = getTurnNum()- ROLLBACK_TURNS_MAX;
if (toDelete > 0 && gameStatesRollBack.containsKey(toDelete)) {
gameStatesRollBack.remove(toDelete);
}
gameStatesRollBack.put(getTurnNum(), state.copy());
}
}
@Override
public boolean canRollbackTurns(int turnsToRollback) {
int turnToGoTo = getTurnNum() - turnsToRollback;
return turnToGoTo > 0 && gameStatesRollBack.containsKey(turnToGoTo);
}
@Override
public synchronized void rollbackTurns(int turnsToRollback) {
if (gameOptions.rollbackTurnsAllowed) {
int turnToGoTo = getTurnNum() - turnsToRollback;
if (turnToGoTo < 1 || !gameStatesRollBack.containsKey(turnToGoTo)) {
informPlayers(GameLog.getPlayerRequestColoredText("Player request: It's not possible to rollback " + turnsToRollback +" turn(s)"));
} else {
GameState restore = gameStatesRollBack.get(turnToGoTo);
if (restore != null) {
informPlayers(GameLog.getPlayerRequestColoredText("Player request: Rolling back to start of turn " + restore.getTurnNum()));
for (Player playerObject: getPlayers().values()) {
if (playerObject.isHuman() && playerObject.isInGame()) {
playerObject.abort();
}
}
state.restore(restore);
// because restore uses the objects without copy each copy the state again
gameStatesRollBack.put(getTurnNum(), state.copy());
executingRollback = true;
fireUpdatePlayersEvent();
}
}
}
}
@Override
public boolean executingRollback() {
return executingRollback;
}
}

View file

@ -37,4 +37,9 @@ public class GameOptions implements Serializable {
* If true, library won't be shuffled at the beginning of the game
*/
public boolean skipInitShuffling = false;
/**
* If true, players can roll back turn if all players agree
*/
public boolean rollbackTurnsAllowed = true;
}

View file

@ -77,19 +77,19 @@ public class GameState implements Serializable, Copyable<GameState> {
private final Players players;
private final PlayerList playerList;
private final Turn turn;
private UUID choosingPlayerId; // player that makes a choice at game start
// revealed cards <Name, <Cards>>, will be reset if all players pass priority
private final Revealed revealed;
private final Map<UUID, LookedAt> lookedAt = new HashMap<>();
private final DelayedTriggeredAbilities delayed;
private final SpecialActions specialActions;
private final TurnMods turnMods;
private final Watchers watchers;
private DelayedTriggeredAbilities delayed;
private SpecialActions specialActions;
private Watchers watchers;
private Turn turn;
private TurnMods turnMods;
private UUID activePlayerId; // playerId which turn it is
private UUID priorityPlayerId; // player that has currently priority
private UUID choosingPlayerId; // player that makes a choice at game start
private SpellStack stack;
private Command command;
private Exile exile;
@ -134,21 +134,24 @@ public class GameState implements Serializable, Copyable<GameState> {
public GameState(final GameState state) {
this.players = state.players.copy();
this.playerList = state.playerList.copy();
this.choosingPlayerId = state.choosingPlayerId;
this.revealed = state.revealed.copy();
this.lookedAt.putAll(state.lookedAt);
this.gameOver = state.gameOver;
this.paused = state.paused;
this.activePlayerId = state.activePlayerId;
this.priorityPlayerId = state.priorityPlayerId;
this.choosingPlayerId = state.choosingPlayerId;
this.turn = state.turn.copy();
this.stack = state.stack.copy();
this.command = state.command.copy();
this.exile = state.exile.copy();
this.revealed = state.revealed.copy();
this.lookedAt.putAll(state.lookedAt);
this.battlefield = state.battlefield.copy();
this.turnNum = state.turnNum;
this.stepNum = state.stepNum;
this.extraTurn = state.extraTurn;
this.legendaryRuleActive = state.legendaryRuleActive;
this.gameOver = state.gameOver;
this.effects = state.effects.copy();
for (TriggeredAbility trigger: state.triggered) {
this.triggered.add(trigger.copy());
@ -163,7 +166,6 @@ public class GameState implements Serializable, Copyable<GameState> {
this.values.put(entry.getKey(), entry.getValue());
}
this.zones.putAll(state.zones);
this.paused = state.paused;
this.simultaneousEvents.addAll(state.simultaneousEvents);
for (Map.Entry<UUID, CardState> entry: state.cardState.entrySet()) {
cardState.put(entry.getKey(), entry.getValue().copy());
@ -172,6 +174,40 @@ public class GameState implements Serializable, Copyable<GameState> {
this.copiedCards.putAll(state.copiedCards);
this.permanentOrderNumber = state.permanentOrderNumber;
}
public void restore(GameState state) {
this.activePlayerId = state.activePlayerId;
this.priorityPlayerId = state.priorityPlayerId;
this.turn = state.turn;
this.stack = state.stack;
this.command = state.command;
this.exile = state.exile;
this.battlefield = state.battlefield;
this.turnNum = state.turnNum;
this.stepNum = state.stepNum;
this.extraTurn = state.extraTurn;
this.legendaryRuleActive = state.legendaryRuleActive;
this.effects = state.effects;
this.triggered = state.triggered;
this.triggers = state.triggers;
this.delayed = state.delayed;
this.specialActions = state.specialActions;
this.combat = state.combat;
this.turnMods = state.turnMods;
this.watchers = state.watchers;
this.values = state.values;
for (Player copyPlayer: state.players.values()) {
Player origPlayer = players.get(copyPlayer.getId());
origPlayer.restore(copyPlayer);
}
this.zones = state.zones;
this.simultaneousEvents = state.simultaneousEvents;
this.cardState = state.cardState;
this.zoneChangeCounter = state.zoneChangeCounter;
this.copiedCards = state.copiedCards;
this.permanentOrderNumber = state.permanentOrderNumber;
}
@Override
public GameState copy() {
@ -558,28 +594,6 @@ public class GameState implements Serializable, Copyable<GameState> {
zones.put(id, zone);
}
public void restore(GameState state) {
this.stack = state.stack;
this.command = state.command;
this.effects = state.effects;
this.triggers = state.triggers;
this.triggered = state.triggered;
this.combat = state.combat;
this.exile = state.exile;
this.battlefield = state.battlefield;
this.zones = state.zones;
this.values = state.values;
for (Player copyPlayer: state.players.values()) {
Player origPlayer = players.get(copyPlayer.getId());
origPlayer.restore(copyPlayer);
}
this.simultaneousEvents = state.simultaneousEvents;
this.cardState = state.cardState;
this.zoneChangeCounter = state.zoneChangeCounter;
this.copiedCards = state.copiedCards;
this.permanentOrderNumber = state.permanentOrderNumber;
}
public void addSimultaneousEvent(GameEvent event, Game game) {
simultaneousEvents.add(event);
}

View file

@ -42,8 +42,12 @@ public class GameStates implements Serializable {
private static final transient Logger logger = Logger.getLogger(GameStates.class);
// private List<byte[]> states = new LinkedList<byte[]>();
private final List<GameState> states = new LinkedList<>();
// private final List<byte[]> states;
private final List<GameState> states;
public GameStates() {
this.states = new LinkedList<>();
}
public void save(GameState gameState) {
// states.add(new Copier<GameState>().copyCompressed(gameState));
@ -60,8 +64,8 @@ public class GameStates implements Serializable {
while (states.size() > index + 1) {
states.remove(states.size() - 1);
}
// return new Copier<GameState>().uncompressCopy(states.get(index));
logger.trace("Rolling back state: " + index);
// return new Copier<GameState>().uncompressCopy(states.get(index));
return states.get(index);
}
return null;
@ -78,7 +82,7 @@ public class GameStates implements Serializable {
public GameState get(int index) {
if (index < states.size()) {
// return new Copier<GameState>().uncompressCopy(states.get(index));
// return new Copier<GameState>().uncompressCopy(states.get(index));
return states.get(index);
}
return null;

View file

@ -74,7 +74,7 @@ public abstract class GameTinyLeadersImpl extends GameImpl{
}
@Override
protected void init(UUID choosingPlayerId, GameOptions gameOptions) {
protected void init(UUID choosingPlayerId) {
Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects"));
//Move tiny leader to command zone
for (UUID playerId: state.getPlayerList(startingPlayerId)) {
@ -101,7 +101,7 @@ public abstract class GameTinyLeadersImpl extends GameImpl{
}
this.getState().addAbility(ability, null);
super.init(choosingPlayerId, gameOptions);
super.init(choosingPlayerId);
if (startingPlayerSkipsDraw) {
state.getTurnMods().add(new TurnMod(startingPlayerId, PhaseStep.DRAW));
}

View file

@ -53,6 +53,7 @@ public class MatchOptions implements Serializable {
protected List<String> playerTypes = new ArrayList<>();
protected String password;
protected SkillLevel skillLevel;
protected boolean rollbackTurnsAllowed;
/**
* Time each player has during the game to play using his\her priority.
@ -159,4 +160,12 @@ public class MatchOptions implements Serializable {
public void setSkillLevel(SkillLevel skillLevel) {
this.skillLevel = skillLevel;
}
public boolean isRollbackTurnsAllowed() {
return rollbackTurnsAllowed;
}
public void setRollbackTurnsAllowed(boolean rollbackTurnsAllowed) {
this.rollbackTurnsAllowed = rollbackTurnsAllowed;
}
}

View file

@ -28,6 +28,7 @@
package mage.game.match;
import java.io.Serializable;
import mage.cards.Card;
import mage.cards.decks.Deck;
import mage.players.Player;
@ -36,7 +37,10 @@ import mage.players.Player;
*
* @author BetaSteward_at_googlemail.com
*/
public class MatchPlayer {
public class MatchPlayer implements Serializable {
private static final long serialVersionUID = 42L;
private int wins;
private boolean matchWinner;
@ -45,7 +49,7 @@ public class MatchPlayer {
private final String name;
private boolean quit;
private final boolean timerTimeout;
//private final boolean timerTimeout;
private boolean doneSideboarding;
private int priorityTimeLeft;
@ -56,7 +60,7 @@ public class MatchPlayer {
this.wins = 0;
this.doneSideboarding = true;
this.quit = false;
this.timerTimeout = false;
//this.timerTimeout = false;
this.name = player.getName();
this.matchWinner = false;
}

View file

@ -113,6 +113,9 @@ public abstract class Phase implements Serializable {
currentStep = step;
if (!game.getState().getTurnMods().skipStep(activePlayerId, getStep().getType())) {
playStep(game);
if (game.executingRollback()) {
return true;
}
}
if (!game.isSimulation() && checkStopOnStepOption(game)) {
return false;
@ -201,6 +204,9 @@ public abstract class Phase implements Serializable {
prePriority(game, activePlayerId);
if (!game.isPaused() && !game.gameOver(null)) {
currentStep.priority(game, activePlayerId, false);
if(game.executingRollback()) {
return;
}
}
if (!game.isPaused() && !game.gameOver(null)) {
postPriority(game, activePlayerId);

View file

@ -117,30 +117,34 @@ public class Turn implements Serializable {
return null;
}
public void play(Game game, UUID activePlayerId) {
public void play(Game game, Player activePlayer) {
activePlayer.becomesActivePlayer();
this.setDeclareAttackersStepStarted(false);
if (game.isPaused() || game.gameOver(null)) {
return;
}
if (game.getState().getTurnMods().skipTurn(activePlayerId)) {
if (game.getState().getTurnMods().skipTurn(activePlayer.getId())) {
return;
}
checkTurnIsControlledByOtherPlayer(game, activePlayerId);
checkTurnIsControlledByOtherPlayer(game, activePlayer.getId());
this.activePlayerId = activePlayerId;
this.activePlayerId = activePlayer.getId();
resetCounts();
game.getPlayer(activePlayerId).beginTurn(game);
game.getPlayer(activePlayer.getId()).beginTurn(game);
for (Phase phase: phases) {
if (game.isPaused() || game.gameOver(null)) {
return;
}
if (!isEndTurnRequested() || phase.getType().equals(TurnPhase.END)) {
currentPhase = phase;
game.fireEvent(new GameEvent(GameEvent.EventType.PHASE_CHANGED, activePlayerId, null, activePlayerId));
if (!game.getState().getTurnMods().skipPhase(activePlayerId, currentPhase.getType())) {
if (phase.play(game, activePlayerId)) {
game.fireEvent(new GameEvent(GameEvent.EventType.PHASE_CHANGED, activePlayer.getId(), null, activePlayer.getId()));
if (!game.getState().getTurnMods().skipPhase(activePlayer.getId(), currentPhase.getType())) {
if (phase.play(game, activePlayer.getId())) {
if(game.executingRollback()) {
return;
}
//20091005 - 500.4/703.4n
game.emptyManaPools();
game.saveState(false);

View file

@ -280,6 +280,7 @@ public interface Player extends MageItem, Copyable<Player> {
void leave();
void concede(Game game);
void abort();
void abortReset();
void skip();
// priority, undo, ...

View file

@ -369,8 +369,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.canPaySacrificeCost = player.canPaySacrificeCost();
this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife();
this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard();
this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts());
this.storedBookmark = player.getStoredBookmark();
this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts());
this.topCardRevealed = player.isTopCardRevealed();
this.playersUnderYourControl.clear();
@ -385,7 +384,9 @@ public abstract class PlayerImpl implements Player, Serializable {
this.castSourceIdWithAlternateMana = player.getCastSourceIdWithAlternateMana();
this.castSourceIdManaCosts = player.getCastSourceIdManaCosts();
this.usersAllowedToSeeHandCards.addAll(player.getUsersAllowedToSeeHandCards());
// Don't restore!
// this.storedBookmark
// this.usersAllowedToSeeHandCards
}
@Override
@ -1855,7 +1856,7 @@ public abstract class PlayerImpl implements Player, Serializable {
passedAllTurns = false;
passedUntilEndOfTurn = true;
passedUntilStackResolved = false;
skippedAtLeastOnce = !game.getTurn().getStepType().equals(PhaseStep.END_TURN);
skippedAtLeastOnce = !PhaseStep.END_TURN.equals(game.getTurn().getStepType());
this.skip();
break;
case PASS_PRIORITY_UNTIL_NEXT_TURN: // F4
@ -3126,4 +3127,9 @@ public abstract class PlayerImpl implements Player, Serializable {
return matchPlayer;
}
@Override
public void abortReset() {
abort = false;
}
}

View file

@ -38,6 +38,7 @@ import java.util.zip.GZIPOutputStream;
/**
*
* @author BetaSteward_at_googlemail.com
* @param <T>
*/
public class Copier<T> {
@ -95,8 +96,7 @@ public class Copier<T> {
public T uncompressCopy(byte[] buffer) {
T copy = null;
try {
ObjectInputStream in = new CopierObjectInputStream(loader, new GZIPInputStream(new ByteArrayInputStream(buffer)));
try (ObjectInputStream in = new CopierObjectInputStream(loader, new GZIPInputStream(new ByteArrayInputStream(buffer)))) {
copy = (T) in.readObject();
}
catch(IOException e) {

View file

@ -38,6 +38,7 @@ import mage.ObjectColor;
public class GameLog {
static final String LOG_COLOR_PLAYER = "#20B2AA"; // LightSeaGreen
static final String LOG_COLOR_PLAYER_REQUEST = "#D2691E"; // Chocolate
static final String LOG_COLOR_GREEN = "#90EE90"; // LightGreen
static final String LOG_COLOR_RED = "#FF6347"; // Tomato
static final String LOG_COLOR_BLUE = "#87CEFA"; // LightSkyBlue
@ -64,6 +65,10 @@ public class GameLog {
public static String getColoredPlayerName(String name) {
return "<font color=\'" + LOG_COLOR_PLAYER + "\'>" + name + "</font>";
}
public static String getPlayerRequestColoredText(String name) {
return "<font color=\'" + LOG_COLOR_PLAYER_REQUEST + "\'>" + name + "</font>";
}
private static String getColorName(ObjectColor objectColor) {
if (objectColor.isMulticolored()) {