mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
This commit is contained in:
parent
4a458800aa
commit
b839c7bf87
2 changed files with 112 additions and 77 deletions
|
|
@ -50,21 +50,29 @@ public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_DeepglowSkate_PerformanceOnTooManyChoices() {
|
public void test_DeepglowSkate_PerformanceOnTooManyChoices() {
|
||||||
// bug: game freeze with 100% CPU usage
|
|
||||||
// https://github.com/magefree/mage/issues/9438
|
// https://github.com/magefree/mage/issues/9438
|
||||||
int cardsCount = 2; // 2+ cards will generate too much target options for simulations
|
int quantity = 1;
|
||||||
int boostMultiplier = (int) Math.pow(2, cardsCount);
|
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
|
// When Deepglow Skate enters the battlefield, double the number of each kind of counter on any number
|
||||||
// of target permanents.
|
// of target permanents.
|
||||||
addCard(Zone.HAND, playerA, "Deepglow Skate", cardsCount); // {4}{U}
|
addCard(Zone.HAND, playerA, "Deepglow Skate", 1); // {4}{U}
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Island", 5 * cardsCount);
|
// 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, Adversary of Tyrants", 1); // x4 loyalty
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 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 Goldmane", 1); // x4 loyalty
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Ajani, Inspiring Leader", 1); // x5 loyalty
|
addCard(Zone.BATTLEFIELD, playerB, "Ajani, Inspiring Leader", 1); // x5 loyalty
|
||||||
//
|
|
||||||
// Players can't activate planeswalkers' loyalty abilities.
|
// Players can't activate planeswalkers' loyalty abilities.
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI
|
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);
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertPermanentCount(playerA, "Deepglow Skate", cardsCount);
|
assertPermanentCount(playerA, "Deepglow Skate", 1);
|
||||||
assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * boostMultiplier);
|
assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * 2);
|
||||||
assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * boostMultiplier);
|
assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * 2);
|
||||||
assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4);
|
assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4);
|
||||||
assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5);
|
assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,54 +26,19 @@ public class TargetOptimization {
|
||||||
|
|
||||||
// for up to or any amount - limit max game sims to analyse
|
// for up to or any amount - limit max game sims to analyse
|
||||||
// (it's useless to calc all possible combinations on too much targets)
|
// (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<UUID> possibleTargets, int maxPossibleTargetsToSimulate) {
|
public static void optimizePossibleTargets(Ability source, Game game, Set<UUID> possibleTargets, int maxPossibleTargetsToSimulate) {
|
||||||
// remove duplicated/same creatures
|
// remove duplicated/same creatures
|
||||||
// example: distribute 3 damage between 10+ same tokens
|
// example: distribute 3 damage between 10+ same tokens
|
||||||
// example: target x1 from x10 forests - it's useless to recalc each forest
|
// example: target x1 from x10 forests - it's useless to recalc each forest
|
||||||
|
|
||||||
if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
|
if (possibleTargets.size() <= maxPossibleTargetsToSimulate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// split targets by groups
|
// split targets by groups
|
||||||
Map<UUID, String> targetGroups = new HashMap<>();
|
Map<String, ArrayList<UUID>> targetGroups = createGroups(game, possibleTargets, maxPossibleTargetsToSimulate, false);
|
||||||
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<String, List<UUID>> groups = new HashMap<>();
|
|
||||||
targetGroups.forEach((id, groupKey) -> {
|
|
||||||
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
|
||||||
groups.get(groupKey).add(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// optimize logic:
|
// optimize logic:
|
||||||
// - use one target from each target group all the time
|
// - use one target from each target group all the time
|
||||||
|
|
@ -81,7 +46,7 @@ public class TargetOptimization {
|
||||||
|
|
||||||
// use one target per group
|
// use one target per group
|
||||||
Set<UUID> newPossibleTargets = new HashSet<>();
|
Set<UUID> newPossibleTargets = new HashSet<>();
|
||||||
groups.forEach((groupKey, groupTargets) -> {
|
targetGroups.forEach((groupKey, groupTargets) -> {
|
||||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
if (targetId != null) {
|
if (targetId != null) {
|
||||||
newPossibleTargets.add(targetId);
|
newPossibleTargets.add(targetId);
|
||||||
|
|
@ -91,13 +56,13 @@ public class TargetOptimization {
|
||||||
|
|
||||||
// use random target until fill condition
|
// use random target until fill condition
|
||||||
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||||
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
|
String groupKey = RandomUtil.randomFromCollection(targetGroups.keySet());
|
||||||
if (groupKey == null) {
|
if (groupKey == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
|
List<UUID> groupTargets = targetGroups.getOrDefault(groupKey, null);
|
||||||
if (groupTargets == null || groupTargets.isEmpty()) {
|
if (groupTargets == null || groupTargets.isEmpty()) {
|
||||||
groups.remove(groupKey);
|
targetGroups.remove(groupKey);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
|
|
@ -112,6 +77,49 @@ public class TargetOptimization {
|
||||||
possibleTargets.addAll(newPossibleTargets);
|
possibleTargets.addAll(newPossibleTargets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, ArrayList<UUID>> createGroups(Game game, Set<UUID> possibleTargets, int maxPossibleTargetsToSimulate, boolean isLoose) {
|
||||||
|
Map<String, ArrayList<UUID>> 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) {
|
private static String getTargetGroupKeyAsPlayer(Player player) {
|
||||||
// use all
|
// use all
|
||||||
return String.join(";", Arrays.asList(
|
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
|
// split by name and stats
|
||||||
// TODO: rework and combine with PermanentEvaluator (to use battlefield score)
|
// TODO: rework and combine with PermanentEvaluator (to use battlefield score)
|
||||||
|
|
||||||
// try to use short text/hash for lesser data on debug
|
// try to use short text/hash for lesser data on debug
|
||||||
return String.join(";", Arrays.asList(
|
if (!isLoose) {
|
||||||
permanent.getName(),
|
return String.join(";", Arrays.asList(
|
||||||
String.valueOf(permanent.getControllerId().hashCode()),
|
permanent.getName(),
|
||||||
String.valueOf(permanent.getOwnerId().hashCode()),
|
String.valueOf(permanent.getControllerId().hashCode()),
|
||||||
String.valueOf(permanent.isTapped()),
|
String.valueOf(permanent.getOwnerId().hashCode()),
|
||||||
String.valueOf(permanent.getPower().getValue()),
|
String.valueOf(permanent.isTapped()),
|
||||||
String.valueOf(permanent.getToughness().getValue()),
|
String.valueOf(permanent.getPower().getValue()),
|
||||||
String.valueOf(permanent.getDamage()),
|
String.valueOf(permanent.getToughness().getValue()),
|
||||||
String.valueOf(permanent.getCardType(game).toString().hashCode()),
|
String.valueOf(permanent.getDamage()),
|
||||||
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
|
String.valueOf(permanent.getCardType(game).toString().hashCode()),
|
||||||
String.valueOf(permanent.getCounters(game).getTotalCount()),
|
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
|
||||||
String.valueOf(permanent.getAbilities(game).size()),
|
String.valueOf(permanent.getCounters(game).getTotalCount()),
|
||||||
String.valueOf(permanent.getRules(game).toString().hashCode())
|
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
|
// split by name and stats
|
||||||
return String.join(";", Arrays.asList(
|
if (!isLoose) {
|
||||||
card.getName(),
|
return String.join(";", Arrays.asList(
|
||||||
String.valueOf(card.getOwnerId().hashCode()),
|
card.getName(),
|
||||||
String.valueOf(card.getCardType(game).toString().hashCode()),
|
String.valueOf(card.getOwnerId().hashCode()),
|
||||||
String.valueOf(card.getSubtype(game).toString().hashCode()),
|
String.valueOf(card.getCardType(game).toString().hashCode()),
|
||||||
String.valueOf(card.getCounters(game).getTotalCount()),
|
String.valueOf(card.getSubtype(game).toString().hashCode()),
|
||||||
String.valueOf(card.getAbilities(game).size()),
|
String.valueOf(card.getCounters(game).getTotalCount()),
|
||||||
String.valueOf(card.getRules(game).toString().hashCode())
|
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) {
|
private static String getTargetGroupKeyAsOther(Game game, MageObject item) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue