mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 11:18:01 +00:00
Fix simulation AI exception with random effects.
Uses a seeded deterministic random generator when simulating choices, which ensures the same number of sub-choices are always used, which the code requires. Adds a test.
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package forge.ai.simulation;
|
package forge.ai.simulation;
|
||||||
|
|
||||||
|
import forge.util.MyRandom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import forge.ai.AiPlayDecision;
|
import forge.ai.AiPlayDecision;
|
||||||
@@ -363,10 +365,17 @@ public class SpellAbilityPicker {
|
|||||||
controller.evaluateSpellAbility(saList, saIndex);
|
controller.evaluateSpellAbility(saList, saIndex);
|
||||||
SpellAbility sa = saList.get(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);
|
Score bestScore = new Score(Integer.MIN_VALUE);
|
||||||
final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller);
|
final SpellAbilityChoicesIterator choicesIterator = new SpellAbilityChoicesIterator(controller);
|
||||||
Score lastScore = null;
|
Score lastScore;
|
||||||
do {
|
do {
|
||||||
|
MyRandom.setRandom(new Random(randomSeedToUse));
|
||||||
GameSimulator simulator = new GameSimulator(controller, game, player, phase);
|
GameSimulator simulator = new GameSimulator(controller, game, player, phase);
|
||||||
simulator.setInterceptor(choicesIterator);
|
simulator.setInterceptor(choicesIterator);
|
||||||
lastScore = simulator.simulateSpellAbility(sa);
|
lastScore = simulator.simulateSpellAbility(sa);
|
||||||
@@ -375,6 +384,7 @@ public class SpellAbilityPicker {
|
|||||||
}
|
}
|
||||||
} while (choicesIterator.advance(lastScore));
|
} while (choicesIterator.advance(lastScore));
|
||||||
controller.doneEvaluating(bestScore);
|
controller.doneEvaluating(bestScore);
|
||||||
|
MyRandom.setRandom(origRandom);
|
||||||
return bestScore;
|
return bestScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ public class MyRandom {
|
|||||||
* If percent is like 30, then 30% of the time it will be true.
|
* If percent is like 30, then 30% of the time it will be true.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param percent
|
* @param percent an int.
|
||||||
* a int.
|
|
||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
public static boolean percentTrue(final int percent) {
|
public static boolean percentTrue(final int percent) {
|
||||||
@@ -56,6 +55,14 @@ public class MyRandom {
|
|||||||
return MyRandom.random;
|
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) {
|
public static int[] splitIntoRandomGroups(final int value, final int numGroups) {
|
||||||
int[] groups = new int[numGroups];
|
int[] groups = new int[numGroups];
|
||||||
|
|
||||||
|
|||||||
@@ -439,4 +439,50 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
|
|||||||
AssertJUnit.assertEquals("Destroy target nonblack creature.", sa.toString());
|
AssertJUnit.assertEquals("Destroy target nonblack creature.", sa.toString());
|
||||||
AssertJUnit.assertEquals(blocker, sa.getTargetCard());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user