diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index f7959dc46b7..c068d6a90f5 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -24,6 +24,7 @@ import forge.game.card.CardFactoryUtil; import forge.game.card.CounterType; import forge.game.combat.Combat; import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.RegisteredPlayer; import forge.game.spellability.AbilityActivated; @@ -64,6 +65,9 @@ public class GameCopier { } public Game makeCopy() { + return makeCopy(null); + } + public Game makeCopy(PhaseType advanceToPhase) { List origPlayers = origGame.getMatch().getPlayers(); List newPlayers = new ArrayList<>(); for (RegisteredPlayer p : origPlayers) { @@ -148,6 +152,10 @@ public class GameCopier { newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap)); } + if (advanceToPhase != null) { + newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase); + } + return newGame; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java index c9100d14314..f2caed5ccf4 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java @@ -12,6 +12,7 @@ import forge.ai.simulation.GameStateEvaluator.Score; import forge.game.Game; import forge.game.GameObject; import forge.game.card.Card; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetChoices; @@ -28,10 +29,10 @@ public class GameSimulator { private Score origScore; private SpellAbilityChoicesIterator interceptor; - public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) { + public GameSimulator(SimulationController controller, Game origGame, Player origAiPlayer, PhaseType advanceToPhase) { this.controller = controller; copier = new GameCopier(origGame); - simGame = copier.makeCopy(); + simGame = copier.makeCopy(advanceToPhase); aiPlayer = (Player) copier.find(origAiPlayer); eval = new GameStateEvaluator(); @@ -42,20 +43,9 @@ public class GameSimulator { debugPrint = false; origScore = eval.getScoreForGameState(origGame, origAiPlayer); - eval.setDebugging(true); - List simLines = new ArrayList(); - debugLines = simLines; - Score simScore = eval.getScoreForGameState(simGame, aiPlayer); - if (!simScore.equals(origScore)) { - // Re-eval orig with debug printing. - origLines = new ArrayList(); - debugLines = origLines; - eval.getScoreForGameState(origGame, origAiPlayer); - // Print debug info. - printDiff(origLines, simLines); - throw new RuntimeException("Game copy error. See diff output above for details."); + if (advanceToPhase == null) { + ensureGameCopyScoreMatches(origGame, origAiPlayer); } - eval.setDebugging(false); // If the stack on the original game is not empty, resolve it // first and get the updated eval score, since this is what we'll @@ -73,6 +63,23 @@ public class GameSimulator { debugLines = null; } + private void ensureGameCopyScoreMatches(Game origGame, Player origAiPlayer) { + eval.setDebugging(true); + List simLines = new ArrayList(); + debugLines = simLines; + Score simScore = eval.getScoreForGameState(simGame, aiPlayer); + if (!simScore.equals(origScore)) { + // Re-eval orig with debug printing. + origLines = new ArrayList(); + debugLines = origLines; + eval.getScoreForGameState(origGame, origAiPlayer); + // Print debug info. + printDiff(origLines, simLines); + throw new RuntimeException("Game copy error. See diff output above for details."); + } + eval.setDebugging(false); + } + public void setInterceptor(SpellAbilityChoicesIterator interceptor) { this.interceptor = interceptor; ((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor); diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java index 5eb847740ae..31226a189ac 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -1,19 +1,16 @@ package forge.ai.simulation; -import forge.ai.AiAttackController; import forge.ai.CreatureEvaluator; import forge.game.Game; import forge.game.card.Card; import forge.game.card.CounterType; import forge.game.combat.Combat; -import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.zone.ZoneType; public class GameStateEvaluator { private boolean debugging = false; - private boolean ignoreTempBoosts = false; private SimulationCreatureEvaluator eval = new SimulationCreatureEvaluator(); public void setDebugging(boolean debugging) { @@ -21,19 +18,25 @@ public class GameStateEvaluator { } private static void debugPrint(String s) { - //System.err.println(s); GameSimulator.debugPrint(s); } - private Combat simulateUpcomingCombatThisTurn(Game game) { - PhaseHandler handler = game.getPhaseHandler(); - if (handler.getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { + private static class CombatSimResult { + public GameCopier copier; + public Game gameCopy; + } + private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame) { + PhaseType phase = evalGame.getPhaseHandler().getPhase(); + if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) { return null; } - AiAttackController aiAtk = new AiAttackController(handler.getPlayerTurn()); - Combat combat = new Combat(handler.getPlayerTurn()); - aiAtk.declareAttackers(combat); - return combat; + GameCopier copier = new GameCopier(evalGame); + Game gameCopy = copier.makeCopy(); + gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE); + CombatSimResult result = new CombatSimResult(); + result.copier = copier; + result.gameCopy = gameCopy; + return result; } private static String cardToString(Card c) { @@ -44,13 +47,27 @@ public class GameStateEvaluator { return str; } + private Score getScoreForGameOver(Game game, Player aiPlayer) { + return game.getOutcome().getWinningPlayer() == aiPlayer ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE); + } + public Score getScoreForGameState(Game game, Player aiPlayer) { if (game.isGameOver()) { - return game.getOutcome().getWinningPlayer() == aiPlayer ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE); + return getScoreForGameOver(game, aiPlayer); } - Combat combat = simulateUpcomingCombatThisTurn(game); + CombatSimResult result = simulateUpcomingCombatThisTurn(game); + if (result != null) { + Player aiPlayerCopy = (Player) result.copier.find(aiPlayer); + if (result.gameCopy.isGameOver()) { + return getScoreForGameOver(result.gameCopy, aiPlayerCopy); + } + return getScoreForGameStateImpl(result.gameCopy, aiPlayerCopy); + } + return getScoreForGameStateImpl(game, aiPlayer); + } + private Score getScoreForGameStateImpl(Game game, Player aiPlayer) { int score = 0; // TODO: more than 2 players int myCards = 0; @@ -85,7 +102,7 @@ public class GameStateEvaluator { int summonSickScore = score; PhaseType gamePhase = game.getPhaseHandler().getPhase(); for (Card c : game.getCardsIn(ZoneType.Battlefield)) { - int value = evalCard(game, aiPlayer, c, combat); + int value = evalCard(game, aiPlayer, c); int summonSickValue = value; // To make the AI hold-off on playing creatures before MAIN2 if they give no other benefits, // keep track of the score while treating summon sick creatures as having a value of 0. @@ -111,20 +128,10 @@ public class GameStateEvaluator { return new Score(score, summonSickScore); } - public int evalCard(Game game, Player aiPlayer, Card c, Combat combat) { + public int evalCard(Game game, Player aiPlayer, Card c) { // TODO: These should be based on other considerations - e.g. in relation to opponents state. if (c.isCreature()) { - // Ignore temp boosts post combat, since it's a waste. - // TODO: Make this smarter. Right now, it only looks if the creature will attack - but - // does not consider things like blocks or the outcome of combat. - // Also, sometimes temp boosts post combat could be useful - e.g. if you then want to make your - // creature fight another, etc. - ignoreTempBoosts = true; - if (combat != null && combat.isAttacking(c)) { - ignoreTempBoosts = false; - } - int result = eval.evaluateCreature(c); - return result; + return eval.evaluateCreature(c); } else if (c.isLand()) { return 100; } else if (c.isEnchantingCard()) { @@ -153,19 +160,13 @@ public class GameStateEvaluator { @Override protected int getEffectivePower(final Card c) { - if (ignoreTempBoosts) { - Card.StatBreakdown breakdown = c.getNetCombatDamageBreakdown(); - return breakdown.getTotal() - breakdown.tempBoost; - } - return c.getNetCombatDamage(); + Card.StatBreakdown breakdown = c.getNetCombatDamageBreakdown(); + return breakdown.getTotal() - breakdown.tempBoost; } @Override protected int getEffectiveToughness(final Card c) { - if (ignoreTempBoosts) { - Card.StatBreakdown breakdown = c.getNetToughnessBreakdown(); - return breakdown.getTotal() - breakdown.tempBoost; - } - return c.getNetToughness(); + Card.StatBreakdown breakdown = c.getNetToughnessBreakdown(); + return breakdown.getTotal() - breakdown.tempBoost; } } diff --git a/forge-ai/src/main/java/forge/ai/simulation/Plan.java b/forge-ai/src/main/java/forge/ai/simulation/Plan.java index 808728f82f0..d89d9009641 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/Plan.java +++ b/forge-ai/src/main/java/forge/ai/simulation/Plan.java @@ -7,22 +7,34 @@ import com.google.common.base.Joiner; import forge.ai.simulation.GameStateEvaluator.Score; import forge.game.card.Card; +import forge.game.phase.PhaseType; import forge.game.spellability.SpellAbility; public class Plan { - private List decisions; + private final List decisions; + private final Score finalScore; private int nextDecisionIndex; private int nextChoice; private Decision selectedDecision; + private PhaseType startPhase; - public Plan(ArrayList decisions) { + public Plan(ArrayList decisions, Score finalScore) { this.decisions = decisions; + this.finalScore = finalScore; } + public Score getFinalScore() { + return finalScore; + } + + public PhaseType getStartPhase() { + return startPhase; + } + public List getDecisions() { return decisions; } - + public boolean hasNextDecision() { return nextDecisionIndex < decisions.size(); } diff --git a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java index 7566538d9c6..a609f69a69c 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java @@ -125,7 +125,7 @@ public class SimulationController { } } sequence.subList(writeIndex, sequence.size()).clear(); - return new Plan(sequence); + return new Plan(sequence, getBestScore()); } private Plan.Decision getLastMergedDecision() { @@ -221,7 +221,7 @@ public class SimulationController { if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) { GameStateEvaluator evaluator = new GameStateEvaluator(); Player player = sa.getActivatingPlayer(); - int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null); + int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2]); if (cardScore == effect.targetScore) { Score currentScore = getCurrentScore(); // TODO: summonSick score? @@ -249,7 +249,7 @@ public class SimulationController { if (currentHostAndTarget != null) { GameStateEvaluator evaluator = new GameStateEvaluator(); Player player = sa.getActivatingPlayer(); - int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null); + int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2]); effectCache.add(new CachedEffect(hostAndTarget[0], sa, hostAndTarget[1], cardScore, scoreDelta)); cached = " (added to cache)"; } diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java index 49cc252a7c5..03ad65511b3 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -1,5 +1,6 @@ package forge.ai.simulation; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -57,7 +58,22 @@ public class SpellAbilityPicker { print("---- choose ability (phase = " + phaseStr + ")"); } - private List getCandidateSpellsAndAbilities(List all) { + private List getCandidateSpellsAndAbilities() { + CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player); + List all = ComputerUtilAbility.getSpellAbilities(cards, player); + CardCollection landsToPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player); + if (landsToPlay != null) { + HashMap landsDeDupe = new HashMap(); + for (Card land : landsToPlay) { + Card previousLand = landsDeDupe.get(land.getName()); + // Skip identical lands. + if (previousLand != null && previousLand.getZone() == land.getZone() && previousLand.getOwner() == land.getOwner()) { + continue; + } + landsDeDupe.put(land.getName(), land); + all.add(new PlayLandAbility(land)); + } + } List candidateSAs = ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player); int writeIndex = 0; for (int i = 0; i < candidateSAs.size(); i++) { @@ -89,28 +105,12 @@ public class SpellAbilityPicker { return null; } - CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player); - List all = ComputerUtilAbility.getSpellAbilities(cards, player); - CardCollection landsToPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player); - if (landsToPlay != null) { - HashMap landsDeDupe = new HashMap(); - for (Card land : landsToPlay) { - Card previousLand = landsDeDupe.get(land.getName()); - // Skip identical lands. - if (previousLand != null && previousLand.getZone() == land.getZone() && previousLand.getOwner() == land.getOwner()) { - continue; - } - landsDeDupe.put(land.getName(), land); - all.add(new PlayLandAbility(land)); - } - } - Score origGameScore = new GameStateEvaluator().getScoreForGameState(game, player); - List candidateSAs = getCandidateSpellsAndAbilities(all); + List candidateSAs = getCandidateSpellsAndAbilities(); if (controller != null) { // This is a recursion during a higher-level simulation. Just return the head of the best // sequence directly, no need to create a Plan object. - return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore); + return chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, null); } printPhaseInfo(); @@ -130,31 +130,81 @@ public class SpellAbilityPicker { return sa; } - private void createNewPlan(Score origGameScore, List candidateSAs) { - plan = null; + private Plan formulatePlanWithPhase(Score origGameScore, List candidateSAs, PhaseType phase) { SimulationController controller = new SimulationController(origGameScore); - SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore); - if (sa == null) { - print("No good plan at this time"); - return; + SpellAbility sa = chooseSpellAbilityToPlayImpl(controller, candidateSAs, origGameScore, phase); + if (sa != null) { + return controller.getBestPlan(); } + return null; + } - plan = controller.getBestPlan(); - print("New plan with score " + controller.getBestScore() + ":"); + private void printPlan(Plan plan, String intro) { + if (plan == null) { + print(intro + ": no plan!"); + } + print(intro +" plan with score " + plan.getFinalScore() + ":"); int i = 0; for (Plan.Decision d : plan.getDecisions()) { print(++i + ". " + d); } } - private SpellAbility chooseSpellAbilityToPlayImpl(SimulationController controller, List candidateSAs, Score origGameScore) { + private static boolean isSorcerySpeed(SpellAbility sa) { + // TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves? + if (sa instanceof PlayLandAbility) { + return false; + } + if (sa.isSpell()) { + return !sa.getHostCard().isInstant() && !sa.getHostCard().hasKeyword("Flash"); + } + if (sa.getRestrictions().isPwAbility()) { + return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."); + } + return sa.isAbility() && sa.getRestrictions().isSorcerySpeed(); + } + + private void createNewPlan(Score origGameScore, List candidateSAs) { + plan = null; + + Plan bestPlan = formulatePlanWithPhase(origGameScore, candidateSAs, null); + if (bestPlan == null) { + print("No good plan at this time"); + return; + } + + PhaseType currentPhase = game.getPhaseHandler().getPhase(); + if (currentPhase.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + List candidateSAs2 = new ArrayList(); + for (SpellAbility sa : candidateSAs) { + if (!isSorcerySpeed(sa)) { + System.err.println("Not sorcery: " + sa); + candidateSAs2.add(sa); + } + } + if (!candidateSAs2.isEmpty()) { + System.err.println("Formula plan with phase bloom"); + Plan afterBlockersPlan = formulatePlanWithPhase(origGameScore, candidateSAs2, PhaseType.COMBAT_DECLARE_BLOCKERS); + if (afterBlockersPlan != null && afterBlockersPlan.getFinalScore().value >= bestPlan.getFinalScore().value) { + printPlan(afterBlockersPlan, "After blockers"); + print("Deciding to wait until after declare blockers."); + return; + } + } + } + + printPlan(bestPlan, "Current phase (" + currentPhase + ")"); + plan = bestPlan; + } + + private SpellAbility chooseSpellAbilityToPlayImpl(SimulationController controller, List candidateSAs, Score origGameScore, PhaseType phase) { long startTime = System.currentTimeMillis(); SpellAbility bestSa = null; Score bestSaValue = origGameScore; print("Evaluating... (orig score = " + origGameScore + ")"); for (int i = 0; i < candidateSAs.size(); i++) { - Score value = evaluateSa(controller, candidateSAs, i); + Score value = evaluateSa(controller, phase, candidateSAs, i); if (value.value > bestSaValue.value) { bestSaValue = value; bestSa = candidateSAs.get(i); @@ -195,6 +245,11 @@ public class SpellAbilityPicker { plan = null; return null; } + PhaseType startPhase = plan.getStartPhase(); + if (startPhase != null && game.getPhaseHandler().getPhase().isBefore(startPhase)) { + print("Waiting until phase " + startPhase + " to proceed with the plan."); + return null; + } Plan.Decision decision = plan.selectNextDecision(); if (!decision.initialScore.equals(origGameScore)) { printPlannedActionFailure(decision, "Unexpected game score (" + decision.initialScore + " vs. expected " + origGameScore + ")"); @@ -308,7 +363,7 @@ public class SpellAbilityPicker { return AiPlayDecision.WillPlay; } - private Score evaluateSa(final SimulationController controller, List saList, int saIndex) { + private Score evaluateSa(final SimulationController controller, PhaseType phase, List saList, int saIndex) { controller.evaluateSpellAbility(saList, saIndex); SpellAbility sa = saList.get(saIndex); @@ -316,7 +371,7 @@ public class SpellAbilityPicker { final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller); Score lastScore = null; do { - GameSimulator simulator = new GameSimulator(controller, game, player); + GameSimulator simulator = new GameSimulator(controller, game, player, phase); simulator.setInterceptor(choicesIterator); lastScore = simulator.simulateSpellAbility(sa); if (lastScore.value > bestScore.value) { diff --git a/forge-game/src/main/java/forge/game/combat/Combat.java b/forge-game/src/main/java/forge/game/combat/Combat.java index 8eaf99c4e9b..2e9db03bd31 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -19,6 +19,7 @@ package forge.game.combat; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -91,19 +92,23 @@ public class Combat { attackableEntries.add(map.map(entry)); } + HashMap bandsMap = new HashMap<>(); for (Entry entry : combat.attackedByBands.entries()) { + AttackingBand origBand = entry.getValue(); ArrayList attackers = new ArrayList(); - for (Card c : entry.getValue().getAttackers()) { + for (Card c : origBand.getAttackers()) { attackers.add(map.map(c)); } - attackedByBands.put(map.map(entry.getKey()), new AttackingBand(attackers)); + AttackingBand newBand = new AttackingBand(attackers); + Boolean blocked = entry.getValue().isBlocked(); + if (blocked != null) { + newBand.setBlocked(blocked); + } + bandsMap.put(origBand, newBand); + attackedByBands.put(map.map(entry.getKey()), newBand); } for (Entry entry : combat.blockedBands.entries()) { - ArrayList attackers = new ArrayList(); - for (Card c : entry.getKey().getAttackers()) { - attackers.add(map.map(c)); - } - blockedBands.put(new AttackingBand(attackers), map.map(entry.getValue())); + blockedBands.put(bandsMap.get(entry.getKey()), map.map(entry.getValue())); } for (Entry entry : combat.defendingDamageMap.entrySet()) { defendingDamageMap.put(map.map(entry.getKey()), entry.getValue()); @@ -123,6 +128,28 @@ public class Combat { attackConstraints = new AttackConstraints(this); } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (GameEntity defender : attackableEntries) { + CardCollection attackers = getAttackersOf(defender); + if (attackers.isEmpty()) { + continue; + } + sb.append(defender); + sb.append(" is being attacked by:\n"); + for (Card attacker : attackers) { + sb.append(" ").append(attacker).append("\n"); + for (Card blocker : getBlockers(attacker)) { + sb.append(" ... blocked by: ").append(blocker).append("\n"); + } + } + } + if (sb.length() == 0) { + return ""; + } + return sb.toString(); + } public void endCombat() { //backup attackers and blockers diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 40a855e4116..c54d8548356 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -889,8 +889,6 @@ public class PhaseHandler implements java.io.Serializable { // don't even offer priority, because it's untap of 1st turn now givePriorityToPlayer = false; - final Set allAffectedCards = new HashSet(); - // MAIN GAME LOOP while (!game.isGameOver()) { if (givePriorityToPlayer) { @@ -903,17 +901,9 @@ public class PhaseHandler implements java.io.Serializable { 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 (checkStateBasedEffects()) { + // state-based effects check could lead to game over + return; } if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { @@ -996,9 +986,39 @@ public class PhaseHandler implements java.io.Serializable { } } + private boolean checkStateBasedEffects() { + final Set allAffectedCards = new HashSet(); + 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 true; // 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(); + } + return false; + } + + public final boolean devAdvanceToPhase(PhaseType targetPhase) { + while (phase.isBefore(targetPhase)) { + if (checkStateBasedEffects()) { + return false; + } + onPhaseEnd(); + advanceToNextPhase(); + onPhaseBegin(); + } + checkStateBasedEffects(); + return true; + } + // 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) { + public final void devModeSet(final PhaseType phase0, final Player player0, boolean endCombat) { if (phase0 != null) { setPhase(phase0); } @@ -1007,7 +1027,12 @@ public class PhaseHandler implements java.io.Serializable { } game.fireEvent(new GameEventTurnPhase(playerTurn, phase, "")); - endCombat(); // not-null can be created only when declare attackers phase begins + if (endCombat) { + endCombat(); // not-null can be created only when declare attackers phase begins + } + } + public final void devModeSet(final PhaseType phase0, final Player player0) { + devModeSet(phase0, player0, true); } public final void endTurnByEffect() { diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java index 40996243937..c1b41e9e0a4 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java @@ -179,7 +179,7 @@ public class GameSimulatorTest extends SimulationTestCase { game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); - assertEquals(20, p.getOpponent().getLife()); + assertEquals(20, game.getPlayers().get(0).getLife()); GameSimulator sim = createSimulator(game, p); Game simGame = sim.getSimulatedGameState(); diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTestCase.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTestCase.java index eb57690c79f..84beba953cc 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTestCase.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTestCase.java @@ -66,7 +66,7 @@ public class SimulationTestCase extends TestCase { public boolean shouldRecurse() { return false; } - }, game, p); + }, game, p, null); } protected Card findCardWithName(Game game, String name) { diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java index a8f33ad15c0..b768b5d228c 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java @@ -5,6 +5,7 @@ import java.util.List; import forge.game.Game; import forge.game.card.Card; import forge.game.card.CounterType; +import forge.game.combat.Combat; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -22,7 +23,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { addCard("Runeclaw Bear", opponent); opponent.setLife(2, null); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); SpellAbilityPicker picker = new SpellAbilityPicker(game, p); @@ -43,7 +44,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { Card bearCard = addCard("Runeclaw Bear", opponent); opponent.setLife(20, null); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); SpellAbilityPicker picker = new SpellAbilityPicker(game, p); @@ -91,7 +92,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { Player opponent = game.getPlayers().get(0); addCard("Runeclaw Bear", opponent); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); // Expected: All creatures get -2/-2 to kill the bear. @@ -110,7 +111,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { addCard("Swamp", p); Card spell = addCardToZone("Dromar's Charm", p, ZoneType.Hand); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); // Expected: Gain 5 life, since other modes aren't helpful. @@ -134,7 +135,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { addCard("Runeclaw Bear", opponent); opponent.setLife(20, null); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); // Expected: 2x 1 damage to each creature, 1x 2 damage to each opponent. @@ -162,7 +163,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { addCard("Runeclaw Bear", opponent); opponent.setLife(6, null); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); // Expected: 3x 2 damage to each opponent. @@ -188,7 +189,7 @@ public class SpellAbilityPickerTest extends SimulationTestCase { Card men = addCard("Flying Men", opponent); opponent.setLife(20, null); - game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); game.getAction().checkStateEffects(true); SpellAbilityPicker picker = new SpellAbilityPicker(game, p); @@ -292,4 +293,96 @@ public class SpellAbilityPickerTest extends SimulationTestCase { String saDesc = plan.getDecisions().get(1).saRef.toString(); assertTrue(saDesc, saDesc.startsWith("Lightning Bolt deals 3 damage to target creature or player.")); } + + public void testPlayingPumpSpellsAfterBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + Card blocker = addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + Card attacker2 = addCard("Kird Ape", p); + attacker2.setSickness(false); + addCard("Mountain", p); + addCardToZone("Brute Force", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_BEGIN, p); + game.getAction().checkStateEffects(true); + assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, p); + Combat combat = new Combat(p); + combat.addAttacker(attacker1, opponent); + combat.addAttacker(attacker2, opponent); + game.getPhaseHandler().setCombat(combat); + game.getAction().checkStateEffects(true); + assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_BLOCKERS, p, false); + game.getAction().checkStateEffects(true); + combat.addBlocker(attacker1, blocker); + combat.getBandOfAttacker(attacker1).setBlocked(true); + combat.getBandOfAttacker(attacker2).setBlocked(false); + combat.orderBlockersForDamageAssignment(); + combat.orderAttackersForDamageAssignment(); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + assertNotNull(sa); + assertEquals("Target creature gets +3/+3 until end of turn.", sa.toString()); + assertEquals(attacker2, sa.getTargetCard()); + } + + public void testPlayingSorceryPumpSpellsBeforeBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + Card attacker2 = addCard("Kird Ape", p); + attacker2.setSickness(false); + addCard("Mountain", p); + Card furor = addCardToZone("Furor of the Bitten", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + assertNotNull(sa); + assertEquals(furor.getSpellAbilities().get(0), sa); + assertEquals(attacker1, sa.getTargetCard()); + } + + public void testPlayingRemovalBeforeBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + Card blocker = addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + addCard("Swamp", p); + addCard("Swamp", p); + addCardToZone("Doom Blade", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + assertNotNull(sa); + assertEquals("Destroy target nonblack creature.", sa.toString()); + assertEquals(blocker, sa.getTargetCard()); + } }