diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index faed8a063a9..8d7cfee3a4b 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -2216,7 +2216,10 @@ public class ComputerUtil { } return ComputerUtilCard.getBestAI(list); } else { - return Iterables.getFirst(votes.keySet(), null); + // TODO: This is just picking randomly amongst already picked things. It should probably pick the worst instead. + List a = Arrays.asList(votes.keySet().toArray()); + Collections.shuffle(a, MyRandom.getRandom()); + return a.get(0); } case "Protection": if (votes.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 3011e56ba06..0671c1b5f14 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -134,7 +134,8 @@ public class ComputerUtilMana { Collections.sort(orderedCards, new Comparator() { @Override public int compare(final Card card1, final Card card2) { - return Integer.compare(manaCardMap.get(card1), manaCardMap.get(card2)); + int result = Integer.compare(manaCardMap.get(card1), manaCardMap.get(card2)); + return result != 0 ? result : Float.compare(card1.getTimestamp(), card2.getTimestamp()); } }); @@ -308,7 +309,7 @@ public class ComputerUtilMana { } // select which abilities may be used for each shard - Multimap sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost); + ListMultimap sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost); sortManaAbilities(sourcesForShards); @@ -858,7 +859,6 @@ public class ComputerUtilMana { } AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); - int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE); PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase(); @@ -877,7 +877,8 @@ public class ComputerUtilMana { // obey mana reservations for Main 2; otherwise, obey mana reservations depending on the "chance to reserve" // AI profile variable. if (sa.getSVar("LowPriorityAI").equals("")) { - if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) { + float chanceToReserve = aic.getFloatProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE); + if (MyRandom.getRandom().nextDouble() >= chanceToReserve) { return false; } } @@ -1005,7 +1006,7 @@ public class ComputerUtilMana { * @return a boolean. */ private static String payMultipleMana(ManaCostBeingPaid testCost, String mana, final Player p) { - List unused = new ArrayList<>(4); + List unused = new ArrayList(4); for (String manaPart : TextUtil.split(mana, ' ')) { if (StringUtils.isNumeric(manaPart)) { for (int i = Integer.parseInt(manaPart); i > 0; i--) { diff --git a/forge-core/src/main/java/forge/util/MyRandom.java b/forge-core/src/main/java/forge/util/MyRandom.java index 85d148b96d2..22b551932a3 100644 --- a/forge-core/src/main/java/forge/util/MyRandom.java +++ b/forge-core/src/main/java/forge/util/MyRandom.java @@ -34,22 +34,27 @@ public class MyRandom { private static Random random = new SecureRandom(); /** - *

- * percentTrue.
- * If percent is like 30, then 30% of the time it will be true. - *

- * - * @param percent - * a int. - * @return a boolean. + * Changes into a non-CSPRNG, which can be seeded for use in tests/repeatable experiments. */ - public static boolean percentTrue(final int percent) { - return percent > MyRandom.getRandom().nextInt(100); + public static void setSeed(int seed) { + System.out.println("Setting the RNG seed to: " + seed); + random = new Random(seed); + } + + /** + * Returns True with Percent probability. + * + * TODO: My guess is no one is passing in a number scaled to 100. This API should probably be cut. + */ + public static boolean percentTrue(final long percent) { + return percent > MyRandom.getRandom().nextDouble() * 100; } /** * Gets the random. * + * TODO: Make this private, and instead add a robust set of APIs here. + * * @return the random */ public static Random getRandom() { @@ -60,7 +65,7 @@ public class MyRandom { int[] groups = new int[numGroups]; for (int i = 0; i < value; i++) { - groups[random.nextInt(numGroups)]++; + groups[MyRandom.getRandom().nextInt(numGroups)]++; } return groups; diff --git a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java index 586e7c0231f..da27a3860fe 100644 --- a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java @@ -22,6 +22,7 @@ import forge.game.player.RegisteredPlayer; import forge.model.FModel; import forge.player.GamePlayerUtil; import forge.util.Lang; +import forge.util.MyRandom; public class SimulateMatch { public static void simulate(String[] args) { @@ -55,6 +56,7 @@ public class SimulateMatch { } else { System.err.println("Illegal parameter usage"); + argumentHelp(); return; } } @@ -78,6 +80,10 @@ public class SimulateMatch { type = GameType.valueOf(WordUtils.capitalize(params.get("f").get(0))); } + if (params.containsKey("s")) { + MyRandom.setSeed(Integer.parseInt(params.get("s").get(0))); + } + GameRules rules = new GameRules(type); rules.setAppliedVariants(EnumSet.of(type)); @@ -145,7 +151,7 @@ public class SimulateMatch { } private static void argumentHelp() { - System.out.println("Syntax: forge.exe sim -d ... -D [D] -n [N] -m [M] -t [T] -p [P] -f [F] -q"); + System.out.println("Syntax: forge.exe sim -d ... -D [D] -n [N] -m [M] -t [T] -p [P] -f [F] -s [S] -q"); System.out.println("\tsim - stands for simulation mode"); System.out.println("\tdeck1 (or deck2,...,X) - constructed deck name or filename (has to be quoted when contains multiple words)"); System.out.println("\tdeck is treated as file if it ends with a dot followed by three numbers or letters"); @@ -155,6 +161,7 @@ public class SimulateMatch { System.out.println("\tT - Type of tournament to run with all provided decks (Bracket, RoundRobin, Swiss)"); System.out.println("\tP - Amount of players per match (used only with Tournaments, defaults to 2)"); System.out.println("\tF - format of games, defaults to constructed"); + System.out.println("\ts - Set the RNG seed. Use if you want to play the same game twice."); System.out.println("\tq - Quiet flag. Output just the game result, not the entire game log."); } @@ -167,7 +174,6 @@ public class SimulateMatch { final Game g1 = mc.createGame(); // will run match in the same thread - long startTime = System.currentTimeMillis(); try { TimeLimitedCodeBlock.runWithTimeout(new Runnable() { @Override 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 e3aee9cb6f5..5813982902e 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 @@ -29,12 +29,14 @@ import forge.item.IPaperCard; import forge.model.FModel; import forge.properties.ForgePreferences; import forge.properties.ForgePreferences.FPref; +import forge.util.MyRandom; import junit.framework.TestCase; public class SimulationTestCase extends TestCase { private static boolean initialized = false; protected Game initAndCreateGame() { + MyRandom.setSeed(0); // Initialize the random seed to create predictable tests. List players = Lists.newArrayList(); Deck d1 = new Deck(); players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p2", null)));