package mage.game.turn; import mage.abilities.Ability; import mage.constants.PhaseStep; import mage.constants.TurnPhase; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.PhaseChangedEvent; import mage.game.permanent.Permanent; import mage.game.stack.Spell; 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 { private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(50); private Phase currentPhase; private UUID activePlayerId; private final List phases = new ArrayList<>(); private boolean declareAttackersStepStarted = false; private boolean endTurn; // indicates that an end turn effect has resolved. public Turn() { endTurn = false; phases.add(new BeginningPhase()); phases.add(new PreCombatMainPhase()); phases.add(new CombatPhase()); phases.add(new PostCombatMainPhase()); phases.add(new EndPhase()); } protected Turn(final Turn turn) { if (turn.currentPhase != null) { this.currentPhase = turn.currentPhase.copy(); } this.activePlayerId = turn.activePlayerId; for (Phase phase : turn.phases) { this.phases.add(phase.copy()); } this.declareAttackersStepStarted = turn.declareAttackersStepStarted; this.endTurn = turn.endTurn; } public TurnPhase getPhaseType() { if (currentPhase != null) { return currentPhase.getType(); } return null; } public Phase getPhase() { return currentPhase; } public Phase getPhase(TurnPhase turnPhase) { for (Phase phase : phases) { if (phase.getType() == turnPhase) { return phase; } } return null; } public void setPhase(Phase phase) { this.currentPhase = phase; } public Step getStep() { if (currentPhase != null) { return currentPhase.getStep(); } return null; } /** * @param game * @param activePlayer * @return true if turn is skipped */ public boolean play(Game game, Player activePlayer) { // uncomment this to trace triggered abilities and/or continous effects // TraceUtil.traceTriggeredAbilities(game); // game.getState().getContinuousEffects().traceContinuousEffects(game); activePlayer.becomesActivePlayer(); this.setDeclareAttackersStepStarted(false); if (game.isPaused() || game.checkIfGameIsOver()) { return false; } TurnMod skipTurnMod = game.getState().getTurnMods().useNextSkipTurn(activePlayer.getId()); if (skipTurnMod != null) { game.informPlayers(String.format("%s skips their turn%s", activePlayer.getLogName(), skipTurnMod.getInfo() )); return true; } logStartOfTurn(game, activePlayer); resetCounts(); this.activePlayerId = activePlayer.getId(); this.currentPhase = null; // turn control must be called after potential turn skip due 720.1. checkTurnIsControlledByOtherPlayer(game, activePlayer.getId()); game.getPlayer(activePlayer.getId()).beginTurn(game); for (Phase phase : phases) { if (game.isPaused() || game.checkIfGameIsOver()) { return false; } if (isEndTurnRequested() && phase.getType() != TurnPhase.END) { continue; } currentPhase = phase; TurnMod skipPhaseMod = game.getState().getTurnMods().useNextSkipPhase(activePlayer.getId(), currentPhase.getType()); if (skipPhaseMod != null) { game.informPlayers(String.format("%s skips %s phase%s", activePlayer.getLogName(), currentPhase.getType(), skipPhaseMod.getInfo() )); continue; } game.fireEvent(new PhaseChangedEvent(activePlayer.getId(), null)); if (!phase.play(game, activePlayer.getId())) { continue; } if (game.executingRollback()) { return false; } //20091005 - 500.4/703.4n game.emptyManaPools(null); game.saveState(false); //20091005 - 500.8 while (playExtraPhases(game, phase.getType())) ; } return false; } public void resumePlay(Game game, boolean wasPaused) { activePlayerId = game.getActivePlayerId(); Player activePlayer = game.getPlayer(activePlayerId); UUID priorityPlayerId = game.getPriorityPlayerId(); TurnPhase needPhaseType = game.getTurnPhaseType(); PhaseStep needStepType = game.getTurnStepType(); Iterator it = phases.iterator(); Phase nextPhase; do { nextPhase = it.next(); } while (nextPhase.type != needPhaseType); // play first phase TurnMod skipPhaseMod = game.getState().getTurnMods().useNextSkipPhase(activePlayerId, nextPhase.getType()); if (skipPhaseMod != null && activePlayer != null) { game.informPlayers(String.format("%s skips %s phase%s", activePlayer.getLogName(), nextPhase.getType(), skipPhaseMod.getInfo() )); } else { if (game.isPaused() || game.checkIfGameIsOver()) { return; } currentPhase = nextPhase; game.fireEvent(new PhaseChangedEvent(activePlayerId, null)); if (nextPhase.resumePlay(game, needStepType, wasPaused)) { //20091005 - 500.4/703.4n game.emptyManaPools(null); //20091005 - 500.8 playExtraPhases(game, nextPhase.getType()); } } // play all other phases while (it.hasNext()) { nextPhase = it.next(); if (game.isPaused() || game.checkIfGameIsOver()) { return; } skipPhaseMod = game.getState().getTurnMods().useNextSkipPhase(activePlayerId, nextPhase.getType()); if (skipPhaseMod != null && activePlayer != null) { game.informPlayers(String.format("%s skips %s phase%s", activePlayer.getLogName(), nextPhase.getType(), skipPhaseMod.getInfo() )); } else { currentPhase = nextPhase; game.fireEvent(new PhaseChangedEvent(activePlayerId, null)); if (nextPhase.play(game, activePlayerId)) { //20091005 - 500.4/703.4n game.emptyManaPools(null); //20091005 - 500.8 playExtraPhases(game, nextPhase.getType()); } } // TODO: old code, can't find any usage of turn's phase change by events/cards // so it must be research and removed as outdated (maybe rollback or playExtraPhases related?) if (!currentPhase.equals(nextPhase)) { // phase was changed from the card game.fireEvent(new PhaseChangedEvent(activePlayerId, null)); break; } } } private void checkTurnIsControlledByOtherPlayer(Game game, UUID activePlayerId) { // 720.1. // Some cards allow a player to control another player during that player’s next turn. // This effect applies to the next turn that the affected player actually takes. // The affected player is controlled during the entire turn; the effect doesn’t end until // the beginning of the next turn. // // 720.1b // If a turn is skipped, any pending player-controlling effects wait until the player who would be // affected actually takes a turn. // remove old under control game.getPlayers().values().forEach(player -> { if (player.isInGame() && !player.isGameUnderControl()) { Player controllingPlayer = game.getPlayer(player.getTurnControlledBy()); if (player != controllingPlayer && controllingPlayer != null) { game.informPlayers(controllingPlayer.getLogName() + " lost control over " + player.getLogName()); } player.setGameUnderYourControl(true); } }); // add new under control TurnMod newControllerMod = game.getState().getTurnMods().useNextNewController(activePlayerId); if (newControllerMod != null && !newControllerMod.getNewControllerId().equals(activePlayerId)) { // game logs added in child's call (controlPlayersTurn) game.getPlayer(newControllerMod.getNewControllerId()).controlPlayersTurn(game, activePlayerId, newControllerMod.getInfo()); } } private void resetCounts() { for (Phase phase : phases) { phase.resetCount(); } } private boolean playExtraPhases(Game game, TurnPhase afterPhase) { while (true) { TurnMod extraPhaseMod = game.getState().getTurnMods().useNextExtraPhase(activePlayerId, afterPhase); if (extraPhaseMod == null) { return false; } TurnPhase extraPhase = extraPhaseMod.getExtraPhase(); if (extraPhase == null) { throw new IllegalStateException("Wrong code usage: miss data in turn mod's extra phase - " + extraPhaseMod.getInfo()); } Phase phase; switch (extraPhase) { case BEGINNING: phase = new BeginningPhase(true); break; case PRECOMBAT_MAIN: phase = new PreCombatMainPhase(); break; case COMBAT: phase = new CombatPhase(); break; case POSTCOMBAT_MAIN: phase = new PostCombatMainPhase(); break; case END: phase = new EndPhase(); break; default: throw new IllegalArgumentException("Unknown phase type: " + extraPhase); } PhaseStep skipAllButExtraStep = extraPhaseMod.getSkipAllButExtraStep(); if (skipAllButExtraStep != null) { phase.keepOnlyStep(skipAllButExtraStep); } currentPhase = phase; game.fireEvent(new PhaseChangedEvent(activePlayerId, extraPhaseMod)); Player activePlayer = game.getPlayer(activePlayerId); if (activePlayer != null) { game.informPlayers(String.format("%s starts an additional %s phase%s", activePlayer.getLogName(), phase.getType().toString(), extraPhaseMod.getInfo() )); } phase.play(game, activePlayerId); // TODO: is it lost extra phase on multiple phases here? // example: // - mods contains 2 mods for same main phases // - one played and afterPhase take main phase value // - so it can't find a second mod afterPhase = extraPhase; } } /** * Used for some spells with end turn effect (e.g. Time Stop). * * @param game * @param activePlayerId * @param source */ public void endTurn(Game game, UUID activePlayerId, Ability source) { // Ending the turn this way (Time Stop) means the following things happen in order: setEndTurnRequested(true); // 1) All spells and abilities on the stack are exiled. This includes (e.g.) Time Stop, though it will continue to resolve. // It also includes spells and abilities that can't be countered. while (!game.hasEnded() && !game.getStack().isEmpty()) { StackObject stackObject = game.getStack().peekFirst(); if (stackObject instanceof Spell) { ((Spell) stackObject).moveToExile(null, "", source, game); } else { game.getStack().remove(stackObject, game); // stack ability } } // 2) All attacking and blocking creatures are removed from combat. for (UUID attackerId : game.getCombat().getAttackers()) { Permanent permanent = game.getPermanent(attackerId); if (permanent != null) { permanent.removeFromCombat(game); } game.getCombat().removeAttacker(attackerId, game); } for (UUID blockerId : game.getCombat().getBlockers()) { Permanent permanent = game.getPermanent(blockerId); if (permanent != null) { permanent.removeFromCombat(game); } } // 3) State-based actions are checked. No player gets priority, and no triggered abilities are put onto the stack. // seems like trigger events have to be removed: http://tabakrules.tumblr.com/post/122350751009/days-undoing-has-been-officially-spoiled-on game.getState().clearTriggeredAbilities(); game.checkStateAndTriggered(); // triggered effects don't go to stack because check of endTurnRequested // 4) The current phase and/or step ends. // The game skips straight to the cleanup step. The cleanup step happens in its entirety. // this is caused by the endTurnRequest state } public boolean isDeclareAttackersStepStarted() { return declareAttackersStepStarted; } public void setDeclareAttackersStepStarted(boolean declareAttackersStepStarted) { this.declareAttackersStepStarted = declareAttackersStepStarted; } public void setEndTurnRequested(boolean endTurn) { this.endTurn = endTurn; } public boolean isEndTurnRequested() { return endTurn; } public Turn copy() { return new Turn(this); } public String getValue(int turnNum) { StringBuilder sb = threadLocalBuilder.get(); sb.append('[').append(turnNum) .append(':').append(currentPhase.getType()) .append(':').append(currentPhase.getStep().getType()) .append(']'); return sb.toString(); } private void logStartOfTurn(Game game, Player player) { StringBuilder sb = new StringBuilder("Turn "); sb.append(game.getState().getTurnNum()).append(' '); if (game.getState().isExtraTurn()) { sb.append("(extra) "); } sb.append(player.getLogName()); sb.append(" ("); int delimiter = game.getPlayers().size() - 1; for (Player gamePlayer : game.getPlayers().values()) { sb.append(gamePlayer.getLife()); int poison = gamePlayer.getCountersCount(CounterType.POISON); if (poison > 0) { sb.append("[P:").append(poison).append(']'); } if (delimiter > 0) { sb.append(" - "); delimiter--; } } sb.append(')'); game.fireStatusEvent(sb.toString(), true, false); } }