Java tutorial
/* * Forge: Play Magic: the Gathering. * Copyright (C) 2011 Forge Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package forge.game.phase; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import forge.card.mana.ManaCost; import forge.game.*; import forge.game.ability.AbilityFactory; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardFactoryUtil; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.cost.Cost; import forge.game.event.*; import forge.game.player.Player; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.player.PlayerController.ManaPaymentPurpose; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.CollectionSuppliers; import forge.util.collect.FCollectionView; import forge.util.maps.HashMapOfLists; import forge.util.maps.MapOfLists; import org.apache.commons.lang3.time.StopWatch; import java.util.*; /** * <p> * Phase class. * </p> * * @author Forge * @version $Id: PhaseHandler.java 13001 2012-01-08 12:25:25Z Sloth $ */ public class PhaseHandler implements java.io.Serializable { private static final long serialVersionUID = 5207222278370963197L; // Start turn at 0, since we start even before first untap private PhaseType phase = null; private int turn = 0; private final transient Stack<ExtraTurn> extraTurns = new Stack<ExtraTurn>(); private final transient Map<PhaseType, Stack<PhaseType>> extraPhases = new HashMap<PhaseType, Stack<PhaseType>>(); private int nUpkeepsThisTurn = 0; private int nUpkeepsThisGame = 0; private int nCombatsThisTurn = 0; private boolean bPreventCombatDamageThisTurn = false; private int planarDiceRolledthisTurn = 0; private transient Player playerTurn = null; // priority player private transient Player pPlayerPriority = null; private transient Player pFirstPriority = null; private transient Combat combat = null; private boolean bRepeatCleanup = false; private transient Player playerDeclaresBlockers = null; private transient Player playerDeclaresAttackers = null; /** The need to next phase. */ private boolean givePriorityToPlayer = false; private final transient Game game; public PhaseHandler(final Game game0) { game = game0; } public final PhaseType getPhase() { return phase; } private final void setPhase(final PhaseType phase0) { if (phase == phase0) { return; } phase = phase0; game.updatePhaseForView(); } public final int getTurn() { return turn; } public final boolean isPlayerTurn(final Player player) { return player.equals(playerTurn); } public final Player getPlayerTurn() { return playerTurn; } private final void setPlayerTurn(final Player playerTurn0) { if (playerTurn == playerTurn0) { return; } playerTurn = playerTurn0; game.updatePlayerTurnForView(); setPriority(playerTurn); } public final Player getPriorityPlayer() { return pPlayerPriority; } public final void setPriority(final Player p) { pFirstPriority = p; pPlayerPriority = p; } public final void resetPriority() { setPriority(playerTurn); } public final boolean inCombat() { return combat != null; } public final Combat getCombat() { return combat; } private void advanceToNextPhase() { PhaseType oldPhase = phase; if (bRepeatCleanup) { // for when Cleanup needs to repeat itself bRepeatCleanup = false; } else { // If the phase that's ending has a stack of additional phases // Take the LIFO one and move to that instead of the normal one if (extraPhases.containsKey(phase)) { PhaseType nextPhase = extraPhases.get(phase).pop(); // If no more additional phases are available, remove it from the map // and let the next add, reput the key if (extraPhases.get(phase).isEmpty()) { extraPhases.remove(phase); } setPhase(nextPhase); } else { setPhase(PhaseType.getNext(phase)); } } game.getStack().clearUndoStack(); //can't undo action from previous phase String phaseType = oldPhase == phase ? "Repeat" : phase == PhaseType.getNext(oldPhase) ? "" : "Additional"; if (phase == PhaseType.UNTAP) { turn++; game.updateTurnForView(); game.fireEvent(new GameEventTurnBegan(playerTurn, turn)); // Tokens starting game in play should suffer from Sum. Sickness final CardCollectionView list = playerTurn.getCardsIncludePhasingIn(ZoneType.Battlefield); for (final Card c : list) { if (playerTurn.getTurn() > 0 || !c.isStartsGameInPlay()) { c.setSickness(false); } } playerTurn.incrementTurn(); game.getAction().resetActivationsPerTurn(); final List<Card> lands = CardLists.filter(playerTurn.getLandsInPlay(), Presets.UNTAPPED); playerTurn.setNumPowerSurgeLands(lands.size()); } game.fireEvent(new GameEventTurnPhase(playerTurn, phase, phaseType)); } private boolean isSkippingPhase(final PhaseType phase) { // TODO: Refactor this method to replacement effect switch (phase) { case UNTAP: if (playerTurn.hasKeyword("Skip your next untap step.")) { playerTurn.removeKeyword("Skip your next untap step."); return true; } return playerTurn.hasKeyword("Skip the untap step of this turn.") || playerTurn.hasKeyword("Skip your untap step."); case UPKEEP: return playerTurn.hasKeyword("Skip your upkeep step."); case DRAW: return playerTurn.isSkippingDraw() || turn == 1 && game.getPlayers().size() == 2; case MAIN1: case MAIN2: return playerTurn.isSkippingMain(); case COMBAT_BEGIN: case COMBAT_DECLARE_ATTACKERS: return playerTurn.isSkippingCombat(); case COMBAT_DECLARE_BLOCKERS: case COMBAT_FIRST_STRIKE_DAMAGE: case COMBAT_DAMAGE: return !inCombat(); default: return false; } } private final void onPhaseBegin() { boolean skipped = false; game.getTriggerHandler().resetActiveTriggers(); if (isSkippingPhase(phase)) { skipped = true; givePriorityToPlayer = false; if (phase == PhaseType.COMBAT_DECLARE_ATTACKERS) { playerTurn.removeKeyword("Skip your next combat phase."); } } else { // Perform turn-based actions switch (phase) { case UNTAP: givePriorityToPlayer = false; game.getUntap().executeUntil(playerTurn); game.getUntap().executeAt(); break; case UPKEEP: nUpkeepsThisTurn++; nUpkeepsThisGame++; game.getUpkeep().executeUntil(playerTurn); game.getUpkeep().executeAt(); break; case DRAW: playerTurn.drawCard(); break; case MAIN1: if (playerTurn.isArchenemy() && isPreCombatMain()) { playerTurn.setSchemeInMotion(); } break; case COMBAT_BEGIN: //PhaseUtil.verifyCombat(); break; case COMBAT_DECLARE_ATTACKERS: if (!playerTurn.hasLost()) { combat = new Combat(playerTurn); game.getStack().freezeStack(); declareAttackersTurnBasedAction(); game.getStack().unfreezeStack(); if (combat != null && combat.getAttackers().isEmpty() && !game.getTriggerHandler().hasDelayedTriggers()) { endCombat(); } } givePriorityToPlayer = inCombat(); break; case COMBAT_DECLARE_BLOCKERS: combat.removeAbsentCombatants(); game.getStack().freezeStack(); declareBlockersTurnBasedAction(); game.getStack().unfreezeStack(); break; case COMBAT_FIRST_STRIKE_DAMAGE: if (combat.removeAbsentCombatants()) { game.updateCombatForView(); } // no first strikers, skip this step if (!combat.assignCombatDamage(true)) { givePriorityToPlayer = false; } else { combat.dealAssignedDamage(); } break; case COMBAT_DAMAGE: if (combat.removeAbsentCombatants()) { game.updateCombatForView(); } if (!combat.assignCombatDamage(false)) { givePriorityToPlayer = false; } else { combat.dealAssignedDamage(); } break; case COMBAT_END: // End Combat always happens game.getEndOfCombat().executeUntil(); game.getEndOfCombat().executeAt(); //SDisplayUtil.showTab(EDocID.REPORT_STACK.getDoc()); break; case MAIN2: //SDisplayUtil.showTab(EDocID.REPORT_STACK.getDoc()); break; case END_OF_TURN: if (playerTurn.getController().isAI()) { playerTurn.getController().resetAtEndOfTurn(); } game.getEndOfTurn().executeAt(); break; case CLEANUP: // Rule 514.1 final int handSize = playerTurn.getZone(ZoneType.Hand).size(); final int max = playerTurn.getMaxHandSize(); int numDiscard = playerTurn.isUnlimitedHandSize() || handSize <= max || handSize == 0 ? 0 : handSize - max; if (numDiscard > 0) { for (Card c : playerTurn.getController().chooseCardsToDiscardToMaximumHandSize(numDiscard)) { playerTurn.discard(c, null); } } // Rule 514.2 // Reset Damage received map for (final Card c : game.getCardsIncludePhasingIn(ZoneType.Battlefield)) { c.onCleanupPhase(playerTurn); } game.getEndOfCombat().executeUntil(); //Repeat here in case Time Stop et. al. ends combat early game.getEndOfTurn().executeUntil(); for (Player player : game.getPlayers()) { player.onCleanupPhase(); player.getController().autoPassCancel(); // autopass won't wrap to next turn } playerTurn.removeKeyword("Skip all combat phases of this turn."); game.getCleanup().executeUntil(getNextTurn()); nUpkeepsThisTurn = 0; // Rule 514.3 givePriorityToPlayer = false; // Rule 514.3a - state-based actions game.getAction().checkStateEffects(true); break; default: break; } } if (!skipped) { // Run triggers if phase isn't being skipped final HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Phase", phase.nameForScripts); runParams.put("Player", playerTurn); game.getTriggerHandler().runTrigger(TriggerType.Phase, runParams, false); } // This line fixes Combat Damage triggers not going off when they should game.getStack().unfreezeStack(); // Rule 514.3a if (phase == PhaseType.CLEANUP && (!game.getStack().isEmpty() || game.getStack().hasSimultaneousStackEntries())) { bRepeatCleanup = true; givePriorityToPlayer = true; } } private void onPhaseEnd() { // If the Stack isn't empty why is nextPhase being called? if (!game.getStack().isEmpty()) { throw new IllegalStateException("Phase.nextPhase() is called, but Stack isn't empty."); } for (Player p : game.getPlayers()) { int burn = p.getManaPool().clearPool(true).size(); boolean manaBurns = game.getRules().hasManaBurn(); if (manaBurns) { p.loseLife(burn); } // Play the Mana Burn sound if (burn > 0) { game.fireEvent(new GameEventManaBurn(burn, manaBurns)); } } switch (phase) { case UNTAP: nCombatsThisTurn = 0; break; case UPKEEP: for (Card c : game.getCardsIn(ZoneType.Battlefield)) { c.getDamageHistory().setNotAttackedSinceLastUpkeepOf(playerTurn); c.getDamageHistory().setNotBlockedSinceLastUpkeepOf(playerTurn); c.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(playerTurn); if (playerTurn.equals(c.getController())) { c.setCameUnderControlSinceLastUpkeep(false); } } game.getUpkeep().executeUntilEndOfPhase(playerTurn); game.getUpkeep().registerUntilEndCommand(playerTurn); break; case COMBAT_END: GameEventCombatEnded eventEndCombat = null; if (combat != null) { List<Card> attackers = combat.getAttackers(); List<Card> blockers = combat.getAllBlockers(); eventEndCombat = new GameEventCombatEnded(attackers, blockers); } endCombat(); playerTurn.resetAttackedThisCombat(); if (eventEndCombat != null) { game.fireEvent(eventEndCombat); } break; case CLEANUP: bPreventCombatDamageThisTurn = false; if (!bRepeatCleanup) { setPlayerTurn(handleNextTurn()); // "Trigger" for begin turn to get around a phase skipping final HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Player", playerTurn); game.getTriggerHandler().runTrigger(TriggerType.TurnBegin, runParams, false); } planarDiceRolledthisTurn = 0; // Play the End Turn sound game.fireEvent(new GameEventTurnEnded()); break; default: // no action } } private void declareAttackersTurnBasedAction() { final Player whoDeclares = playerDeclaresAttackers == null || playerDeclaresAttackers.hasLost() ? playerTurn : playerDeclaresAttackers; if (CombatUtil.canAttack(playerTurn)) { boolean success = false; do { if (game.isGameOver()) { // they just like to close window at any moment return; } whoDeclares.getController().declareAttackers(playerTurn, combat); combat.removeAbsentCombatants(); success = CombatUtil.validateAttackers(combat); if (!success) { whoDeclares.getController().notifyOfValue(null, null, "Attack declaration invalid"); continue; } for (final Card attacker : combat.getAttackers()) { final boolean shouldTapForAttack = !attacker.hasKeyword("Vigilance") && !attacker.hasKeyword("Attacking doesn't cause CARDNAME to tap."); if (shouldTapForAttack) { // set tapped to true without firing triggers because it may affect propaganda costs attacker.setTapped(true); } final boolean canAttack = CombatUtil.checkPropagandaEffects(game, attacker, combat); attacker.setTapped(false); if (canAttack) { if (shouldTapForAttack) { attacker.tap(); } } else { combat.removeFromCombat(attacker); success = CombatUtil.validateAttackers(combat); if (!success) { break; } } } } while (!success); } if (game.isGameOver()) { // they just like to close window at any moment return; } nCombatsThisTurn++; // Prepare and fire event 'attackers declared' Multimap<GameEntity, Card> attackersMap = ArrayListMultimap.create(); for (GameEntity ge : combat.getDefenders()) { attackersMap.putAll(ge, combat.getAttackersOf(ge)); } game.fireEvent(new GameEventAttackersDeclared(playerTurn, attackersMap)); // This Exalted handler should be converted to script if (combat.getAttackers().size() == 1) { final Player attackingPlayer = combat.getAttackingPlayer(); final Card attacker = combat.getAttackers().get(0); for (Card card : attackingPlayer.getCardsIn(ZoneType.Battlefield)) { int exaltedMagnitude = card.getAmountOfKeyword("Exalted"); for (int i = 0; i < exaltedMagnitude; i++) { String abScript = String.format( "AB$ Pump | Cost$ 0 | Defined$ CardUID_%d | NumAtt$ +1 | NumDef$ +1 | StackDescription$ Exalted for attacker {c:CardUID_%d} (Whenever a creature you control attacks alone, that creature gets +1/+1 until end of turn).", attacker.getId(), attacker.getId()); SpellAbility ability = AbilityFactory.getAbility(abScript, card); ability.setActivatingPlayer(card.getController()); ability.setDescription(ability.getStackDescription()); ability.setTrigger(true); game.getStack().addSimultaneousStackEntry(ability); } } } // fire AttackersDeclared trigger if (!combat.getAttackers().isEmpty()) { List<GameEntity> attackedTarget = new ArrayList<GameEntity>(); for (final Card c : combat.getAttackers()) { attackedTarget.add(combat.getDefenderByAttacker(c)); } final HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Attackers", combat.getAttackers()); runParams.put("AttackingPlayer", combat.getAttackingPlayer()); runParams.put("AttackedTarget", attackedTarget); game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false); } for (final Card c : combat.getAttackers()) { CombatUtil.checkDeclaredAttacker(game, c, combat); } game.getTriggerHandler().resetActiveTriggers(); game.updateCombatForView(); game.fireEvent(new GameEventCombatChanged()); } private void declareBlockersTurnBasedAction() { Player p = playerTurn; do { p = game.getNextPlayerAfter(p); // Apply Odric's effect here Player whoDeclaresBlockers = playerDeclaresBlockers == null || playerDeclaresBlockers.hasLost() ? p : playerDeclaresBlockers; if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.attackerChoosesBlockers)) { whoDeclaresBlockers = combat.getAttackingPlayer(); } if (combat.isPlayerAttacked(p)) { if (CombatUtil.canBlock(p, combat)) { whoDeclaresBlockers.getController().declareBlockers(p, combat); } } else { continue; } if (game.isGameOver()) { // they just like to close window at any moment return; } // Handles removing cards like Mogg Flunkies from combat if group block // didn't occur for (Card blocker : CardLists.filterControlledBy(combat.getAllBlockers(), p)) { final List<Card> attackers = new ArrayList<Card>(combat.getAttackersBlockedBy(blocker)); for (Card attacker : attackers) { boolean hasPaid = payRequiredBlockCosts(game, blocker, attacker); if (!hasPaid) { combat.removeBlockAssignment(attacker, blocker); } } } List<Card> remainingBlockers = CardLists.filterControlledBy(combat.getAllBlockers(), p); for (Card c : remainingBlockers) { if (remainingBlockers.size() < 2 && c.hasKeyword("CARDNAME can't attack or block alone.")) { combat.undoBlockingAssignment(c); } } // Player is done declaring blockers - redraw UI at this point // map: defender => (many) attacker => (many) blocker Map<GameEntity, MapOfLists<Card, Card>> blockers = new HashMap<GameEntity, MapOfLists<Card, Card>>(); for (GameEntity ge : combat.getDefendersControlledBy(p)) { MapOfLists<Card, Card> protectThisDefender = new HashMapOfLists<Card, Card>( CollectionSuppliers.<Card>arrayLists()); for (Card att : combat.getAttackersOf(ge)) { protectThisDefender.addAll(att, combat.getBlockers(att)); } blockers.put(ge, protectThisDefender); } game.fireEvent(new GameEventBlockersDeclared(p, blockers)); } while (p != playerTurn); combat.orderBlockersForDamageAssignment(); // 509.2 combat.orderAttackersForDamageAssignment(); // 509.3 combat.removeAbsentCombatants(); combat.fireTriggersForUnblockedAttackers(); final List<Card> declaredBlockers = combat.getAllBlockers(); if (!declaredBlockers.isEmpty()) { final List<Card> blockedAttackers = new ArrayList<Card>(); for (final Card blocker : declaredBlockers) { for (final Card blockedAttacker : combat.getAttackersBlockedBy(blocker)) { if (!blockedAttackers.contains(blockedAttacker)) { blockedAttackers.add(blockedAttacker); } } } // fire blockers declared trigger final HashMap<String, Object> bdRunParams = new HashMap<String, Object>(); bdRunParams.put("Blockers", declaredBlockers); bdRunParams.put("Attackers", blockedAttackers); game.getTriggerHandler().runTrigger(TriggerType.BlockersDeclared, bdRunParams, false); } for (final Card c1 : combat.getAllBlockers()) { if (c1.getDamageHistory().getCreatureBlockedThisCombat()) { continue; } if (!c1.getDamageHistory().getCreatureBlockedThisCombat()) { for (final SpellAbility ab : CardFactoryUtil.getBushidoEffects(c1)) { game.getStack().add(ab); } // Run triggers final HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Blocker", c1); runParams.put("Attackers", combat.getAttackersBlockedBy(c1)); game.getTriggerHandler().runTrigger(TriggerType.Blocks, runParams, false); } c1.getDamageHistory().setCreatureBlockedThisCombat(true); c1.getDamageHistory().clearNotBlockedSinceLastUpkeepOf(); } for (final Card a : combat.getAttackers()) { if (combat.isBlocked(a)) { a.getDamageHistory().clearNotBeenBlockedSinceLastUpkeepOf(); } final List<Card> blockers = combat.getBlockers(a); if (blockers.isEmpty()) { continue; } // Run triggers final HashMap<String, Object> runParams = new HashMap<String, Object>(); runParams.put("Attacker", a); runParams.put("Blockers", blockers); runParams.put("NumBlockers", blockers.size()); game.getTriggerHandler().runTrigger(TriggerType.AttackerBlocked, runParams, false); // Run this trigger once for each blocker for (final Card b : blockers) { final HashMap<String, Object> runParams2 = new HashMap<String, Object>(); runParams2.put("Attacker", a); runParams2.put("Blocker", b); game.getTriggerHandler().runTrigger(TriggerType.AttackerBlockedByCreature, runParams2, false); } if (!a.getDamageHistory().getCreatureGotBlockedThisCombat()) { // Bushido for (final SpellAbility ab : CardFactoryUtil.getBushidoEffects(a)) { game.getStack().add(ab); } // Rampage CombatUtil.handleRampage(game, a, blockers); } CombatUtil.handleFlankingKeyword(game, a, blockers); a.getDamageHistory().setCreatureGotBlockedThisCombat(true); } game.updateCombatForView(); game.fireEvent(new GameEventCombatChanged()); } private static boolean payRequiredBlockCosts(Game game, Card blocker, Card attacker) { Cost blockCost = new Cost(ManaCost.ZERO, true); // Sort abilities to apply them in proper order boolean noCost = true; List<ZoneType> checkZones = ZoneType.listValueOf("Battlefield,Command"); for (Card card : game.getCardsIn(checkZones)) { final FCollectionView<StaticAbility> staticAbilities = card.getStaticAbilities(); for (final StaticAbility stAb : staticAbilities) { Cost c1 = stAb.getBlockCost(blocker, attacker); if (c1 != null) { blockCost.add(c1); noCost = false; } } } SpellAbility fakeSA = new SpellAbility.EmptySa(blocker, blocker.getController()); return noCost || blocker.getController().getController().payManaOptional(blocker, blockCost, fakeSA, "Pay cost to declare " + blocker + " a blocker. ", ManaPaymentPurpose.DeclareBlocker); } public final boolean isPreventCombatDamageThisTurn() { return bPreventCombatDamageThisTurn; } private Player handleNextTurn() { game.getStack().onNextTurn(); // reset mustAttackEntity playerTurn.setMustAttackEntity(null); for (final Player p1 : game.getPlayers()) { for (final ZoneType z : Player.ALL_ZONES) { p1.getZone(z).resetCardsAddedThisTurn(); } } for (Player p : game.getPlayers()) { p.resetProwl(); p.resetSpellsCastThisTurn(); p.setLifeLostLastTurn(p.getLifeLostThisTurn()); p.setLifeLostThisTurn(0); p.setLifeGainedThisTurn(0); p.setLibrarySearched(0); p.setNumManaConversion(0); p.removeKeyword("Skip the untap step of this turn."); p.removeKeyword("Schemes can't be set in motion this turn."); } game.getTriggerHandler().clearThisTurnDelayedTrigger(); Player next = getNextActivePlayer(); game.getTriggerHandler().handlePlayerDefinedDelTriggers(next); if (game.getRules().hasAppliedVariant(GameType.Planechase)) { for (Card p : game.getActivePlanes()) { if (p != null) { p.setController(next, 0); game.getAction().controllerChangeZoneCorrection(p); } } } return next; } private Player getNextActivePlayer() { ExtraTurn extraTurn = !extraTurns.isEmpty() ? extraTurns.pop() : null; Player nextPlayer = extraTurn != null ? extraTurn.getPlayer() : game.getNextPlayerAfter(playerTurn); if (extraTurn != null) { // The bottom of the extra turn stack is the normal turn nextPlayer.setExtraTurn(!extraTurns.isEmpty()); if (nextPlayer.hasKeyword("If you would begin an extra turn, skip that turn instead.")) { return getNextActivePlayer(); } for (Trigger deltrig : extraTurn.getDelayedTriggers()) { game.getTriggerHandler().registerThisTurnDelayedTrigger(deltrig); } } else { nextPlayer.setExtraTurn(false); } if (nextPlayer.hasKeyword("Skip your next turn.")) { nextPlayer.removeKeyword("Skip your next turn."); if (extraTurn == null) { setPlayerTurn(nextPlayer); } return getNextActivePlayer(); } // TODO: This shouldn't filter by Time Vault, just in case Time Vault doesn't have it's normal ability. CardCollection vaults = CardLists.filter(nextPlayer.getCardsIn(ZoneType.Battlefield, "Time Vault"), Presets.TAPPED); if (!vaults.isEmpty()) { Card crd = vaults.getFirst(); SpellAbility fakeSA = new SpellAbility.EmptySa(crd, nextPlayer); boolean untapTimeVault = nextPlayer.getController().chooseBinary(fakeSA, "Skip a turn to untap a Time Vault?", BinaryChoiceType.UntapTimeVault, false); if (untapTimeVault) { if (vaults.size() > 1) { Card c = nextPlayer.getController().chooseSingleEntityForEffect(vaults, fakeSA, "Which Time Vault do you want to Untap?"); if (c != null) crd = c; } crd.untap(); if (extraTurn == null) { setPlayerTurn(nextPlayer); } return getNextActivePlayer(); } } if (extraTurn != null) { if (extraTurn.isSkipUntap()) { nextPlayer.addKeyword("Skip the untap step of this turn."); } if (extraTurn.isCantSetSchemesInMotion()) { nextPlayer.addKeyword("Schemes can't be set in motion this turn."); } } return nextPlayer; } public final synchronized boolean is(final PhaseType phase0, final Player player0) { return phase == phase0 && playerTurn.equals(player0); } public final synchronized boolean is(final PhaseType phase0) { return phase == phase0; } public final Player getNextTurn() { if (extraTurns.isEmpty()) { return game.getNextPlayerAfter(playerTurn); } return extraTurns.peek().getPlayer(); } public final ExtraTurn addExtraTurn(final Player player) { // use a stack to handle extra turns, make sure the bottom of the stack // restores original turn order if (extraTurns.isEmpty()) { extraTurns.push(new ExtraTurn(game.getNextPlayerAfter(playerTurn))); } return extraTurns.push(new ExtraTurn(player)); } public final void addExtraPhase(final PhaseType afterPhase, final PhaseType extraPhase) { // 500.8. Some effects can add phases to a turn. They do this by adding the phases directly after the specified phase. // If multiple extra phases are created after the same phase, the most recently created phase will occur first. if (!extraPhases.containsKey(afterPhase)) { extraPhases.put(afterPhase, new Stack<PhaseType>()); } extraPhases.get(afterPhase).push(extraPhase); } public final boolean isFirstCombat() { return (nCombatsThisTurn == 1); } public final boolean isFirstUpkeep() { return (nUpkeepsThisTurn == 0); } public final boolean isFirstUpkeepThisGame() { return (nUpkeepsThisGame == 0); } public final boolean isPreCombatMain() { return (nCombatsThisTurn == 0); } private final static boolean DEBUG_PHASES = false; public void startFirstTurn(Player goesFirst) { StopWatch sw = new StopWatch(); if (phase != null) { throw new IllegalStateException("Turns already started, call this only once per game"); } setPlayerTurn(goesFirst); advanceToNextPhase(); onPhaseBegin(); // don't even offer priority, because it's untap of 1st turn now givePriorityToPlayer = false; final Set<Card> allAffectedCards = new HashSet<Card>(); // MAIN GAME LOOP while (!game.isGameOver()) { if (givePriorityToPlayer) { if (DEBUG_PHASES) { sw.start(); } game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer())); List<SpellAbility> chosenSa = null; int loopCount = 0; do { do { // Rule 704.3 Whenever a player would get priority, the game checks ... for state-based actions, game.getAction().checkStateEffects(false, allAffectedCards); if (game.isGameOver()) { return; // state-based effects check could lead to game over } } while (game.getStack().addAllTriggeredAbilitiesToStack()); //loop so long as something was added to stack if (!allAffectedCards.isEmpty()) { game.fireEvent(new GameEventCardStatsChanged(allAffectedCards)); allAffectedCards.clear(); } if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { // If the active player has lost, and they have priority, set the next player to have priority System.out.println("Active player is no longer in the game..."); pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer()); pFirstPriority = pPlayerPriority; } chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay(); if (chosenSa == null) { break; // that means 'I pass' } if (DEBUG_PHASES) { System.out.print("... " + pPlayerPriority + " plays " + chosenSa); } pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve for (SpellAbility sa : chosenSa) { pPlayerPriority.getController().playChosenSpellAbility(sa); } loopCount++; } while (loopCount < 999 || !pPlayerPriority.getController().isAI()); if (loopCount >= 999 && pPlayerPriority.getController().isAI()) { System.out.print("AI looped too much with: " + chosenSa); } if (DEBUG_PHASES) { sw.stop(); System.out.print("... passed in " + sw.getTime() / 1000f + " s\n"); System.out.println("\t\tStack: " + game.getStack()); sw.reset(); } } else if (DEBUG_PHASES) { System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n"); } // actingPlayer is the player who may act // the firstAction is the player who gained Priority First in this segment // of Priority Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer()); if (game.isGameOver() || nextPlayer == null) { return; } // conceded? if (DEBUG_PHASES) { System.out.println(String.format("%s %s: %s is active, previous was %s", playerTurn, phase, pPlayerPriority, nextPlayer)); } if (pFirstPriority == nextPlayer) { if (game.getStack().isEmpty()) { if (playerTurn.hasLost()) { setPriority(game.getNextPlayerAfter(playerTurn)); } else { setPriority(playerTurn); } // end phase givePriorityToPlayer = true; onPhaseEnd(); advanceToNextPhase(); onPhaseBegin(); } else if (!game.getStack().hasSimultaneousStackEntries()) { game.getStack().resolveStack(); } } else { // pass the priority to other player pPlayerPriority = nextPlayer; } // If ever the karn's ultimate resolved if (game.getAge() == GameStage.RestartedByKarn) { setPhase(null); game.updatePhaseForView(); game.fireEvent(new GameEventGameRestarted(playerTurn)); return; } } } // this is a hack for the setup game state mode, do not use outside of devSetupGameState code // as it avoids calling any of the phase effects that may be necessary in a less enforced context public final void devModeSet(final PhaseType phase0, final Player player0) { if (phase0 != null) { setPhase(phase0); } if (player0 != null) { setPlayerTurn(player0); } game.fireEvent(new GameEventTurnPhase(playerTurn, phase, "")); endCombat(); // not-null can be created only when declare attackers phase begins } public final void endTurnByEffect() { endCombat(); extraPhases.clear(); setPhase(PhaseType.CLEANUP); onPhaseBegin(); } public final void setPreventCombatDamageThisTurn() { bPreventCombatDamageThisTurn = true; } public int getPlanarDiceRolledthisTurn() { return planarDiceRolledthisTurn; } public void incPlanarDiceRolledthisTurn() { planarDiceRolledthisTurn++; } public String debugPrintState(boolean hasPriority) { return String.format("%s's %s [%sP] %s", playerTurn, phase.nameForUi, hasPriority ? "+" : "-", getPriorityPlayer()); } // just to avoid exposing variable to outer classes public void onStackResolved() { givePriorityToPlayer = true; } public final void setPlayerDeclaresAttackers(Player player) { playerDeclaresAttackers = player; } public final void setPlayerDeclaresBlockers(Player player) { playerDeclaresBlockers = player; } public void endCombat() { if (combat != null) { combat.endCombat(); combat = null; } game.updateCombatForView(); } }