foul-magics/Mage/src/main/java/mage/game/turn/Turn.java

416 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Phase> 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<Phase> 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 players 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 doesnt 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);
}
}