diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java index a13f26171be..efb6a456eea 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationTriggersAITest.java @@ -50,21 +50,29 @@ public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps { @Test public void test_DeepglowSkate_PerformanceOnTooManyChoices() { - // bug: game freeze with 100% CPU usage // https://github.com/magefree/mage/issues/9438 - int cardsCount = 2; // 2+ cards will generate too much target options for simulations - int boostMultiplier = (int) Math.pow(2, cardsCount); + int quantity = 1; + String[] cardNames = { + "Island", "Plains", "Swamp", "Mountain", + "Runeclaw Bear", "Absolute Law", "Gilded Lotus", "Alpha Myr" + }; // When Deepglow Skate enters the battlefield, double the number of each kind of counter on any number // of target permanents. - addCard(Zone.HAND, playerA, "Deepglow Skate", cardsCount); // {4}{U} - addCard(Zone.BATTLEFIELD, playerA, "Island", 5 * cardsCount); - // + addCard(Zone.HAND, playerA, "Deepglow Skate", 1); // {4}{U} + // Bloat the battlefield with permanents (possible targets) + for (String card : cardNames) { + addCard(Zone.BATTLEFIELD, playerA, card, quantity); + addCard(Zone.BATTLEFIELD, playerB, card, quantity); + addCard(Zone.BATTLEFIELD, playerC, card, quantity); + addCard(Zone.BATTLEFIELD, playerD, card, quantity); + } + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Adversary of Tyrants", 1); // x4 loyalty addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 1); // x4 loyalty addCard(Zone.BATTLEFIELD, playerB, "Ajani Goldmane", 1); // x4 loyalty addCard(Zone.BATTLEFIELD, playerB, "Ajani, Inspiring Leader", 1); // x5 loyalty - // + // Players can't activate planeswalkers' loyalty abilities. addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI @@ -75,9 +83,9 @@ public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps { setStopAt(1, PhaseStep.END_TURN); execute(); - assertPermanentCount(playerA, "Deepglow Skate", cardsCount); - assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * boostMultiplier); - assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * boostMultiplier); + assertPermanentCount(playerA, "Deepglow Skate", 1); + assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * 2); + assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * 2); assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4); assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5); } diff --git a/Mage/src/main/java/mage/target/TargetOptimization.java b/Mage/src/main/java/mage/target/TargetOptimization.java index 879b32e5f57..5e9ab5128ac 100644 --- a/Mage/src/main/java/mage/target/TargetOptimization.java +++ b/Mage/src/main/java/mage/target/TargetOptimization.java @@ -26,54 +26,19 @@ public class TargetOptimization { // for up to or any amount - limit max game sims to analyse // (it's useless to calc all possible combinations on too much targets) - static public int AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE = 7; + static public int AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE = 18; public static void optimizePossibleTargets(Ability source, Game game, Set possibleTargets, int maxPossibleTargetsToSimulate) { // remove duplicated/same creatures // example: distribute 3 damage between 10+ same tokens // example: target x1 from x10 forests - it's useless to recalc each forest - if (possibleTargets.size() < maxPossibleTargetsToSimulate) { + if (possibleTargets.size() <= maxPossibleTargetsToSimulate) { return; } // split targets by groups - Map targetGroups = new HashMap<>(); - possibleTargets.forEach(id -> { - String groupKey = ""; - - // player - Player player = game.getPlayer(id); - if (player != null) { - groupKey = getTargetGroupKeyAsPlayer(player); - } - - // game object - MageObject object = game.getObject(id); - if (object != null) { - groupKey = object.getName(); - if (object instanceof Permanent) { - groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object); - } else if (object instanceof Card) { - groupKey += getTargetGroupKeyAsCard(game, (Card) object); - } else { - groupKey += getTargetGroupKeyAsOther(game, object); - } - } - - // unknown - use all - if (groupKey.isEmpty()) { - groupKey = id.toString(); - } - - targetGroups.put(id, groupKey); - }); - - Map> groups = new HashMap<>(); - targetGroups.forEach((id, groupKey) -> { - groups.computeIfAbsent(groupKey, k -> new ArrayList<>()); - groups.get(groupKey).add(id); - }); + Map> targetGroups = createGroups(game, possibleTargets, maxPossibleTargetsToSimulate, false); // optimize logic: // - use one target from each target group all the time @@ -81,7 +46,7 @@ public class TargetOptimization { // use one target per group Set newPossibleTargets = new HashSet<>(); - groups.forEach((groupKey, groupTargets) -> { + targetGroups.forEach((groupKey, groupTargets) -> { UUID targetId = RandomUtil.randomFromCollection(groupTargets); if (targetId != null) { newPossibleTargets.add(targetId); @@ -91,13 +56,13 @@ public class TargetOptimization { // use random target until fill condition while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) { - String groupKey = RandomUtil.randomFromCollection(groups.keySet()); + String groupKey = RandomUtil.randomFromCollection(targetGroups.keySet()); if (groupKey == null) { break; } - List groupTargets = groups.getOrDefault(groupKey, null); + List groupTargets = targetGroups.getOrDefault(groupKey, null); if (groupTargets == null || groupTargets.isEmpty()) { - groups.remove(groupKey); + targetGroups.remove(groupKey); continue; } UUID targetId = RandomUtil.randomFromCollection(groupTargets); @@ -112,6 +77,49 @@ public class TargetOptimization { possibleTargets.addAll(newPossibleTargets); } + private static Map> createGroups(Game game, Set possibleTargets, int maxPossibleTargetsToSimulate, boolean isLoose) { + Map> targetGroups = new HashMap<>(); + + possibleTargets.forEach(id -> { + String groupKey = ""; + + // player + Player player = game.getPlayer(id); + if (player != null) { + groupKey = getTargetGroupKeyAsPlayer(player); + } + + // game object + MageObject object = game.getObject(id); + if (object != null) { + groupKey = object.getName(); + if (object instanceof Permanent) { + groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object, isLoose); + } else if (object instanceof Card) { + groupKey += getTargetGroupKeyAsCard(game, (Card) object, isLoose); + } else { + groupKey += getTargetGroupKeyAsOther(game, object); + } + } + + // unknown - use all + if (groupKey.isEmpty()) { + groupKey = id.toString(); + } + + targetGroups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(id); + }); + + if (targetGroups.size() > maxPossibleTargetsToSimulate && !isLoose) { + // If too many possible target groups, regroup with less specific characteristics + return createGroups(game, possibleTargets, maxPossibleTargetsToSimulate, true); + } + + // Return appropriate target groups or, if still too many possible targets after loose grouping, + // allow optimizePossibleTargets (defined above) to choose random targets within limit + return targetGroups; + } + private static String getTargetGroupKeyAsPlayer(Player player) { // use all return String.join(";", Arrays.asList( @@ -120,38 +128,57 @@ public class TargetOptimization { )); } - private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) { + private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent, boolean isLoose) { // split by name and stats // TODO: rework and combine with PermanentEvaluator (to use battlefield score) // try to use short text/hash for lesser data on debug - return String.join(";", Arrays.asList( - permanent.getName(), - String.valueOf(permanent.getControllerId().hashCode()), - String.valueOf(permanent.getOwnerId().hashCode()), - String.valueOf(permanent.isTapped()), - String.valueOf(permanent.getPower().getValue()), - String.valueOf(permanent.getToughness().getValue()), - String.valueOf(permanent.getDamage()), - String.valueOf(permanent.getCardType(game).toString().hashCode()), - String.valueOf(permanent.getSubtype(game).toString().hashCode()), - String.valueOf(permanent.getCounters(game).getTotalCount()), - String.valueOf(permanent.getAbilities(game).size()), - String.valueOf(permanent.getRules(game).toString().hashCode()) - )); + if (!isLoose) { + return String.join(";", Arrays.asList( + permanent.getName(), + String.valueOf(permanent.getControllerId().hashCode()), + String.valueOf(permanent.getOwnerId().hashCode()), + String.valueOf(permanent.isTapped()), + String.valueOf(permanent.getPower().getValue()), + String.valueOf(permanent.getToughness().getValue()), + String.valueOf(permanent.getDamage()), + String.valueOf(permanent.getCardType(game).toString().hashCode()), + String.valueOf(permanent.getSubtype(game).toString().hashCode()), + String.valueOf(permanent.getCounters(game).getTotalCount()), + String.valueOf(permanent.getAbilities(game).size()), + String.valueOf(permanent.getRules(game).toString().hashCode()) + )); + } + else { + return String.join(";", Arrays.asList( + String.valueOf(permanent.getControllerId().hashCode()), + String.valueOf(permanent.getPower().getValue()), + String.valueOf(permanent.getToughness().getValue()), + String.valueOf(permanent.getDamage()), + String.valueOf(permanent.getCardType(game).toString().hashCode()) + )); + } } - private static String getTargetGroupKeyAsCard(Game game, Card card) { + private static String getTargetGroupKeyAsCard(Game game, Card card, boolean isLoose) { // split by name and stats - return String.join(";", Arrays.asList( - card.getName(), - String.valueOf(card.getOwnerId().hashCode()), - String.valueOf(card.getCardType(game).toString().hashCode()), - String.valueOf(card.getSubtype(game).toString().hashCode()), - String.valueOf(card.getCounters(game).getTotalCount()), - String.valueOf(card.getAbilities(game).size()), - String.valueOf(card.getRules(game).toString().hashCode()) - )); + if (!isLoose) { + return String.join(";", Arrays.asList( + card.getName(), + String.valueOf(card.getOwnerId().hashCode()), + String.valueOf(card.getCardType(game).toString().hashCode()), + String.valueOf(card.getSubtype(game).toString().hashCode()), + String.valueOf(card.getCounters(game).getTotalCount()), + String.valueOf(card.getAbilities(game).size()), + String.valueOf(card.getRules(game).toString().hashCode()) + )); + } + else { + return String.join(";", Arrays.asList( + String.valueOf(card.getOwnerId().hashCode()), + String.valueOf(card.getCardType(game).toString().hashCode()) + )); + } } private static String getTargetGroupKeyAsOther(Game game, MageObject item) {