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 a367ab10d3d..546a8259071 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -175,7 +175,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) { // TODO update thisTurnCast if (advanceToPhase != null) { - newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, () -> GameSimulator.resolveStack(newGame, aiPlayer.getWeakestOpponent())); + newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, () -> GameSimulator.resolveStack(newGame)); } 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 aac320753a8..b8f3328df1e 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java @@ -53,7 +53,7 @@ public GameSimulator(SimulationController controller, Game origGame, Player orig debugLines = origLines; Game copyOrigGame = copier.makeCopy(); Player copyOrigAiPlayer = copyOrigGame.getPlayers().get(1); - resolveStack(copyOrigGame, copyOrigGame.getPlayers().get(0)); + resolveStack(copyOrigGame); origScore = eval.getScoreForGameState(copyOrigGame, copyOrigAiPlayer); } @@ -225,9 +225,7 @@ public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval, } if (resolve) { - // TODO: Support multiple opponents. - Player opponent = aiPlayer.getWeakestOpponent(); - resolveStack(simGame, opponent); + resolveStack(simGame); } // TODO: If this is during combat, before blockers are declared, @@ -260,11 +258,9 @@ public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval, return score; } - public static void resolveStack(final Game game, final Player opponent) { - // TODO: This needs to set an AI controller for all opponents, in case of multiplayer. - PlayerControllerAi sim = new PlayerControllerAi(game, opponent, opponent.getLobbyPlayer()); - sim.setUseSimulation(true); - opponent.runWithController(() -> { + public static void resolveStack(final Game game) { + List players = new ArrayList<>(game.getPlayers()); + runWithSimulationControllers(game, players, () -> { final Set allAffectedCards = new HashSet<>(); game.getAction().checkStateEffects(false, allAffectedCards); game.getStack().addAllTriggeredAbilitiesToStack(); @@ -288,7 +284,23 @@ public static void resolveStack(final Game game, final Player opponent) { // Continue until stack is empty. } - }, sim); + }); + } + + private static void runWithSimulationControllers(final Game game, final List players, final Runnable proc) { + if (players.isEmpty()) { + proc.run(); + return; + } + + Player player = players.remove(0); + if (player.getController().isAI()) { + runWithSimulationControllers(game, players, proc); + return; + } + PlayerControllerAi sim = new PlayerControllerAi(game, player, player.getLobbyPlayer()); + sim.setUseSimulation(true); + player.runWithController(() -> runWithSimulationControllers(game, players, proc), sim); } public Game getSimulatedGameState() { 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 979c61052fc..112c824984b 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -59,7 +59,7 @@ private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, fina gameCopy = copier.makeCopy(null, aiPlayer); } - gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, () -> GameSimulator.resolveStack(gameCopy, aiPlayer.getWeakestOpponent())); + gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, () -> GameSimulator.resolveStack(gameCopy)); CombatSimResult result = new CombatSimResult(); result.copier = copier; result.gameCopy = gameCopy; diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java index ecd30c6ff50..deb1cf12351 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java @@ -15,6 +15,7 @@ import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; +import forge.gamesimulationtests.util.PlayerControllerForTests; import forge.util.StreamUtil; import org.testng.AssertJUnit; @@ -61,6 +62,44 @@ public void testActivateAbilityTriggers() { AssertJUnit.assertEquals(1, warriorToken.getCurrentToughness()); } + @Test + public void testResolveStackUsesSimulationControllersForNonAiPlayers() { + Game game = initAndCreateThreePlayerGame(); + + Player opponent = game.getPlayers().get(0); + Player ai = game.getPlayers().get(1); + Player otherOpponent = game.getPlayers().get(2); + + opponent.setTeam(0); + ai.setTeam(1); + otherOpponent.setTeam(2); + + opponent.setLife(1, null); + otherOpponent.setLife(20, null); + + addCard("Runeclaw Bear", opponent); + addCard("Runeclaw Bear", ai); + Card otherOpponentBear = addCard("Runeclaw Bear", otherOpponent); + + Card spell = addCardToZone("Innocent Blood", opponent, ZoneType.Hand); + SpellAbility sa = spell.getFirstSpellAbility(); + sa.setActivatingPlayer(opponent); + game.getStack().addAndUnfreeze(sa); + + PlayerControllerForTests throwingController = new PlayerControllerForTests(game, otherOpponent, otherOpponent.getLobbyPlayer()) { + @Override + public CardCollectionView choosePermanentsToSacrifice(SpellAbility ability, int min, int max, + CardCollectionView validTargets, String message) { + throw new AssertionError("resolveStack should install a simulation controller for non-AI players"); + } + }; + + otherOpponent.runWithController(() -> GameSimulator.resolveStack(game), throwingController); + + AssertJUnit.assertTrue("The non-AI non-weakest player should have sacrificed during stack resolution", + otherOpponent.getZone(ZoneType.Graveyard).contains(otherOpponentBear)); + } + @Test public void testStaticAbilities() { String sliverCardName = "Sidewinder Sliver";