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 0d951d9a5e3..03885c8fd4d 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -1,8 +1,10 @@ package forge.ai.simulation; +import forge.util.MyRandom; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Random; import java.util.Set; import forge.ai.AiPlayDecision; @@ -363,10 +365,17 @@ public class SpellAbilityPicker { controller.evaluateSpellAbility(saList, saIndex); SpellAbility sa = saList.get(saIndex); + // Use a deterministic random seed when evaluating different choices of a spell ability. + // This is needed as otherwise random effects may result in a different number of choices + // each iteration, which will break the logic in SpellAbilityChoicesIterator. + Random origRandom = MyRandom.getRandom(); + long randomSeedToUse = origRandom.nextLong(); + Score bestScore = new Score(Integer.MIN_VALUE); final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller); - Score lastScore = null; + Score lastScore; do { + MyRandom.setRandom(new Random(randomSeedToUse)); GameSimulator simulator = new GameSimulator(controller, game, player, phase); simulator.setInterceptor(choicesIterator); lastScore = simulator.simulateSpellAbility(sa); @@ -375,6 +384,7 @@ public class SpellAbilityPicker { } } while (choicesIterator.advance(lastScore)); controller.doneEvaluating(bestScore); + MyRandom.setRandom(origRandom); return bestScore; } diff --git a/forge-core/src/main/java/forge/util/MyRandom.java b/forge-core/src/main/java/forge/util/MyRandom.java index 85d148b96d2..d63a2f05176 100644 --- a/forge-core/src/main/java/forge/util/MyRandom.java +++ b/forge-core/src/main/java/forge/util/MyRandom.java @@ -39,8 +39,7 @@ public class MyRandom { * If percent is like 30, then 30% of the time it will be true. *

* - * @param percent - * a int. + * @param percent an int. * @return a boolean. */ public static boolean percentTrue(final int percent) { @@ -56,6 +55,14 @@ public class MyRandom { return MyRandom.random; } + /** + * Sets the random provider. Used for deterministic simulation. + * @param random the random + */ + public static void setRandom(Random random) { + MyRandom.random = random; + } + public static int[] splitIntoRandomGroups(final int value, final int numGroups) { int[] groups = new int[numGroups]; diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java index 667ca1c9c3f..18fe2cad984 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java @@ -439,4 +439,50 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest { AssertJUnit.assertEquals("Destroy target nonblack creature.", sa.toString()); AssertJUnit.assertEquals(blocker, sa.getTargetCard()); } + + @Test + public void testChoicesResultingFromRandomEffects() { + // Sometimes, the effect of a spell can be random, and as a result of that, new choices + // could be selected during simulation. This test verifies that this doesn't cause problems. + // + // Note: The current implementation works around the issue by setting a consistent random + // seed during choice evaluation, although in the future, it may make sense to handle it + // some other way. + + // Run the test 100 times to ensure there's no flakiness. + for (int i = 0; i < 100; i++) { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Chaos Warp", p, ZoneType.Hand); + addCard("Mountain", p); + addCard("Mountain", p); + addCard("Mountain", p); + + addCard("Plains", opponent); + addCard("Mountain", opponent); + addCard("Forest", opponent); + // Use a card that is worthwhile to target even if the shuffle ends up choosing it + // again. In this case, life loss on ETB and leaving. + Card expectedTarget = addCard("Raving Oni-Slave", opponent); + + addCardToZone("Chaos Warp", opponent, ZoneType.Library); + addCardToZone("Island", opponent, ZoneType.Library); + addCardToZone("Swamp", opponent, ZoneType.Library); + // The presence of Pilgrim's Eye in the library is important for this test, as this + // will result in sub-choices (which land to pick) if this card ends up being the top + // of the library during simulation. + addCardToZone("Pilgrim's Eye", opponent, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals("Chaos Warp", sa.getHostCard().getName()); + AssertJUnit.assertEquals(expectedTarget, sa.getTargetCard()); + } + } }