mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
AI: improved performance on too many possible targets (fix game freezes and server crashes - see #9539, #9438, #9518, related to #11285, #5023);
This commit is contained in:
parent
384ce67cc3
commit
2833460e59
10 changed files with 397 additions and 234 deletions
|
|
@ -219,6 +219,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
}
|
}
|
||||||
// Condition to stop deeper simulation
|
// Condition to stop deeper simulation
|
||||||
if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) {
|
if (SimulationNode2.nodeCount > MAX_SIMULATED_NODES_PER_ERROR) {
|
||||||
|
// how-to fix: make sure you are disabled debug mode by COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false
|
||||||
throw new IllegalStateException("AI ERROR: too much nodes (possible actions)");
|
throw new IllegalStateException("AI ERROR: too much nodes (possible actions)");
|
||||||
}
|
}
|
||||||
if (depth <= 0
|
if (depth <= 0
|
||||||
|
|
@ -501,7 +502,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
}
|
}
|
||||||
logger.warn("Possible freeze chain:");
|
logger.warn("Possible freeze chain:");
|
||||||
if (root != null && chain.isEmpty()) {
|
if (root != null && chain.isEmpty()) {
|
||||||
logger.warn(" - unknown use case"); // maybe can't finish any calc, maybe related to target options, I don't know
|
logger.warn(" - unknown use case (too many possible targets?)"); // maybe can't finish any calc, maybe related to target options, I don't know
|
||||||
}
|
}
|
||||||
chain.forEach(s -> {
|
chain.forEach(s -> {
|
||||||
logger.warn(" - " + s);
|
logger.warn(" - " + s);
|
||||||
|
|
@ -642,7 +643,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
})
|
})
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d]<diff %s> (%s)",
|
logger.info(String.format("Sim Prio [%d] -> with possible choices: [%d]<diff %s> (%s)",
|
||||||
depth,
|
depth,
|
||||||
currentNode.getDepth(),
|
currentNode.getDepth(),
|
||||||
printDiffScore(currentScore - prevScore),
|
printDiffScore(currentScore - prevScore),
|
||||||
|
|
@ -651,14 +652,18 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
} else if (!currentNode.getChoices().isEmpty()) {
|
} else if (!currentNode.getChoices().isEmpty()) {
|
||||||
// ON CHOICES
|
// ON CHOICES
|
||||||
String choicesInfo = String.join(", ", currentNode.getChoices());
|
String choicesInfo = String.join(", ", currentNode.getChoices());
|
||||||
logger.info(String.format("Sim Prio [%d] -> with choices (TODO): [%d]<diff %s> (%s)",
|
logger.info(String.format("Sim Prio [%d] -> with possible choices (must not see that code): [%d]<diff %s> (%s)",
|
||||||
depth,
|
depth,
|
||||||
currentNode.getDepth(),
|
currentNode.getDepth(),
|
||||||
printDiffScore(currentScore - prevScore),
|
printDiffScore(currentScore - prevScore),
|
||||||
choicesInfo)
|
choicesInfo)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("AI CALC ERROR: unknown calculation result (no abilities, no targets, no choices)");
|
logger.info(String.format("Sim Prio [%d] -> with do nothing: [%d]<diff %s>",
|
||||||
|
depth,
|
||||||
|
currentNode.getDepth(),
|
||||||
|
printDiffScore(currentScore - prevScore))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
Ability ability = source.copy();
|
Ability ability = source.copy();
|
||||||
List<Ability> options = getPlayableOptions(ability, game);
|
List<Ability> options = getPlayableOptions(ability, game);
|
||||||
if (options.isEmpty()) {
|
if (options.isEmpty()) {
|
||||||
|
// no options - activate as is
|
||||||
logger.debug("simulating -- triggered ability:" + ability);
|
logger.debug("simulating -- triggered ability:" + ability);
|
||||||
game.getStack().push(game, new StackAbility(ability, playerId));
|
game.getStack().push(game, new StackAbility(ability, playerId));
|
||||||
if (ability.activate(game, false) && ability.isUsesStack()) {
|
if (ability.activate(game, false) && ability.isUsesStack()) {
|
||||||
|
|
@ -326,6 +327,8 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
game.applyEffects();
|
game.applyEffects();
|
||||||
game.getPlayers().resetPassed();
|
game.getPlayers().resetPassed();
|
||||||
} else {
|
} else {
|
||||||
|
// many options - activate and add to sims tree
|
||||||
|
// TODO: AI run all sims, but do not use best option for triggers yet
|
||||||
SimulationNode2 parent = (SimulationNode2) game.getCustomData();
|
SimulationNode2 parent = (SimulationNode2) game.getCustomData();
|
||||||
int depth = parent.getDepth() - 1;
|
int depth = parent.getDepth() - 1;
|
||||||
if (depth == 0) {
|
if (depth == 0) {
|
||||||
|
|
@ -350,7 +353,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option");
|
logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option");
|
||||||
for (Target target : ability.getTargets()) {
|
for (Target target : ability.getTargets()) {
|
||||||
for (UUID targetId : target.getTargets()) {
|
for (UUID targetId : target.getTargets()) {
|
||||||
newNode.getTargets().add(targetId); // save for info only (real targets in newNode.ability already)
|
newNode.getTargets().add(targetId); // save for info only (real targets in newNode.game.stack already)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent.children.add(newNode);
|
parent.children.add(newNode);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBaseAI;
|
import org.mage.test.serverside.base.CardTestPlayerBaseAI;
|
||||||
|
|
||||||
|
|
@ -21,7 +20,6 @@ import java.util.List;
|
||||||
* TODO: add tests and implement best choice selection on timeout
|
* TODO: add tests and implement best choice selection on timeout
|
||||||
* (AI must make any good/bad choice on timeout with game log - not a skip)
|
* (AI must make any good/bad choice on timeout with game log - not a skip)
|
||||||
* <p>
|
* <p>
|
||||||
* TODO: AI do not support game simulations for target options in triggered
|
|
||||||
*
|
*
|
||||||
* @author JayDi85
|
* @author JayDi85
|
||||||
*/
|
*/
|
||||||
|
|
@ -106,28 +104,24 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // enable after triggered supported or need performance test
|
|
||||||
public void test_ManyTargetOptions_Triggered_Single() {
|
public void test_ManyTargetOptions_Triggered_Single() {
|
||||||
// 2 damage to bear and 3 damage to player B
|
// 2 damage to bear and 3 damage to player B
|
||||||
runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3);
|
runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // enable after triggered supported or need performance test
|
|
||||||
public void test_ManyTargetOptions_Triggered_Few() {
|
public void test_ManyTargetOptions_Triggered_Few() {
|
||||||
// 4 damage to x2 bears and 1 damage to player B
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1);
|
runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // enable after triggered supported or need performance test
|
|
||||||
public void test_ManyTargetOptions_Triggered_Many() {
|
public void test_ManyTargetOptions_Triggered_Many() {
|
||||||
// 4 damage to x2 bears and 1 damage to player B
|
// 4 damage to x2 bears and 1 damage to player B
|
||||||
runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1);
|
runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // enable after triggered supported or need performance test
|
|
||||||
public void test_ManyTargetOptions_Triggered_TooMuch() {
|
public void test_ManyTargetOptions_Triggered_TooMuch() {
|
||||||
// warning, can be slow
|
// warning, can be slow
|
||||||
|
|
||||||
|
|
@ -139,7 +133,6 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore // enable after triggered supported or need performance test
|
|
||||||
public void test_ManyTargetOptions_Triggered_TargetGroups() {
|
public void test_ManyTargetOptions_Triggered_TargetGroups() {
|
||||||
// make sure targets optimization can find unique creatures, e.g. damaged
|
// make sure targets optimization can find unique creatures, e.g. damaged
|
||||||
|
|
||||||
|
|
@ -213,4 +206,29 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI {
|
||||||
// 4 damage to x2 bears and 1 damage to damaged bear
|
// 4 damage to x2 bears and 1 damage to damaged bear
|
||||||
runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20);
|
runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_ElderDeepFiend_TooManyUpToChoices() {
|
||||||
|
// bug: game freeze with 100% CPU usage
|
||||||
|
// https://github.com/magefree/mage/issues/9518
|
||||||
|
int cardsCount = 2; // 2+ cards will generate too much target options for simulations
|
||||||
|
|
||||||
|
// Boulderfall deals 5 damage divided as you choose among any number of targets.
|
||||||
|
// Flash
|
||||||
|
// Emerge {5}{U}{U} (You may cast this spell by sacrificing a creature and paying the emerge cost reduced by that creature's mana value.)
|
||||||
|
// When you cast this spell, tap up to four target permanents.
|
||||||
|
addCard(Zone.HAND, playerA, "Elder Deep-Fiend", cardsCount); // {8}
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 8 * cardsCount);
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Alpha Tyrranax", 2);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Abbey Griffin", 2);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertPermanentCount(playerA, "Elder Deep-Fiend", cardsCount); // ai must cast it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package org.mage.test.AI.basic;
|
||||||
|
|
||||||
|
import mage.constants.PhaseStep;
|
||||||
|
import mage.constants.Zone;
|
||||||
|
import mage.counters.CounterType;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure AI can simulate priority with triggers resolve
|
||||||
|
*
|
||||||
|
* @author JayDi85
|
||||||
|
*/
|
||||||
|
public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore
|
||||||
|
// TODO: trigger's target options supported on priority sim, but do not used for some reason
|
||||||
|
// see addTargetOptions, node.children, ComputerPlayer6->targets, etc
|
||||||
|
public void test_DeepglowSkate_MustBeSimulated() {
|
||||||
|
// make sure targets choosing on trigger use same game sims and best results
|
||||||
|
|
||||||
|
// 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", 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
|
||||||
|
//
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Adversary of Tyrants", 1); // x4 loyalty
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 1); // x4 loyalty
|
||||||
|
//
|
||||||
|
// This creature enters with a -1/-1 counter on it.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Bloodied Ghost", 1); // 3/3
|
||||||
|
//
|
||||||
|
// Players can't activate planeswalkers' loyalty abilities.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI
|
||||||
|
|
||||||
|
// AI must cast boost and ignore doubling of -1/-1 counters on own creatures due bad score
|
||||||
|
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
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(playerA, "Bloodied Ghost", CounterType.M1M1, 1); // make sure AI will not double bad counters
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
// 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.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
|
||||||
|
|
||||||
|
// AI must cast multiple booster spells and double only own counters and only good
|
||||||
|
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
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);
|
||||||
|
assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4);
|
||||||
|
assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4524,7 +4524,6 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
} else if (ability.getCosts().getTargets().getNextUnchosen(game) != null) {
|
} else if (ability.getCosts().getTargets().getNextUnchosen(game) != null) {
|
||||||
addCostTargetOptions(options, ability, 0, game);
|
addCostTargetOptions(options, ability, 0, game);
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4565,9 +4564,6 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
*/
|
*/
|
||||||
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||||
// TODO: target options calculated for triggered ability too, but do not used in real game
|
// TODO: target options calculated for triggered ability too, but do not used in real game
|
||||||
// TODO: there are rare errors with wrong targetNum - maybe multiple game sims can change same target object somehow?
|
|
||||||
// do not hide NullPointError here, research instead
|
|
||||||
|
|
||||||
if (targetNum >= option.getTargets().size()) {
|
if (targetNum >= option.getTargets().size()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -4592,6 +4588,8 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
|
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// don't forget about target's status (if it zero then must set skip choice too)
|
||||||
|
newOption.getTargets().get(targetNum).setSkipChoice(targetOption.isSkipChoice());
|
||||||
|
|
||||||
if (targetNum + 1 < option.getTargets().size()) {
|
if (targetNum + 1 < option.getTargets().size()) {
|
||||||
// fill more targets
|
// fill more targets
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ public interface Target extends Copyable<Target>, Serializable {
|
||||||
boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards);
|
boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game, Cards fromCards);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary status to work with "up to" targets (mark target that it was skip on selection)
|
* Tests and AI related for "up to" targets (mark target that it was skipped on selection, so new choose dialog will be called)
|
||||||
* TODO: remove after target.chooseXXX remove
|
* Example: AI sim possible target options
|
||||||
*/
|
*/
|
||||||
boolean isSkipChoice();
|
boolean isSkipChoice();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
package mage.target;
|
package mage.target;
|
||||||
|
|
||||||
import mage.MageObject;
|
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
import mage.abilities.dynamicvalue.DynamicValue;
|
import mage.abilities.dynamicvalue.DynamicValue;
|
||||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||||
import mage.cards.Card;
|
|
||||||
import mage.cards.Cards;
|
import mage.cards.Cards;
|
||||||
import mage.constants.Outcome;
|
import mage.constants.Outcome;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
import mage.game.permanent.Permanent;
|
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.util.DebugUtil;
|
import mage.util.CardUtil;
|
||||||
import mage.util.RandomUtil;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -19,7 +15,7 @@ import java.util.stream.Collectors;
|
||||||
/**
|
/**
|
||||||
* Distribute value between targets list (damage, counters, etc)
|
* Distribute value between targets list (damage, counters, etc)
|
||||||
*
|
*
|
||||||
* @author BetaSteward_at_googlemail.com
|
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||||
*/
|
*/
|
||||||
public abstract class TargetAmount extends TargetImpl {
|
public abstract class TargetAmount extends TargetImpl {
|
||||||
|
|
||||||
|
|
@ -208,217 +204,21 @@ public abstract class TargetAmount extends TargetImpl {
|
||||||
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
||||||
|
|
||||||
// optimizations for less memory/cpu consumptions
|
// optimizations for less memory/cpu consumptions
|
||||||
printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false);
|
TargetOptimization.printTargetsVariationsForTargetAmount("target amount - before optimize", game, possibleTargets, options, false);
|
||||||
optimizePossibleTargets(source, game, possibleTargets);
|
|
||||||
printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false);
|
|
||||||
|
|
||||||
// calc possible amount variations
|
|
||||||
addTargets(this, possibleTargets, options, source, game);
|
|
||||||
printTargetsTableAndVariations("after calc", game, possibleTargets, options, true);
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI related, trying to reduce targets for simulations
|
|
||||||
*/
|
|
||||||
private void optimizePossibleTargets(Ability source, Game game, Set<UUID> possibleTargets) {
|
|
||||||
// remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens)
|
|
||||||
|
|
||||||
// it must have additional threshold to keep more variations for analyse
|
// it must have additional threshold to keep more variations for analyse
|
||||||
//
|
|
||||||
// bad example:
|
// bad example:
|
||||||
// - Blessings of Nature
|
// - Blessings of Nature
|
||||||
// - Distribute four +1/+1 counters among any number of target creatures.
|
// - Distribute four +1/+1 counters among any number of target creatures.
|
||||||
// on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets
|
// on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets
|
||||||
|
int maxPossibleTargetsToSimulate = CardUtil.overflowMultiply(this.remainingAmount, 2);
|
||||||
|
TargetOptimization.optimizePossibleTargets(source, game, possibleTargets, maxPossibleTargetsToSimulate);
|
||||||
|
TargetOptimization.printTargetsVariationsForTargetAmount("target amount - after optimize", game, possibleTargets, options, false);
|
||||||
|
|
||||||
int maxPossibleTargetsToSimulate = this.remainingAmount * 2;
|
// calc possible amount variations
|
||||||
if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
|
addTargets(this, possibleTargets, options, source, game);
|
||||||
return;
|
TargetOptimization.printTargetsVariationsForTargetAmount("target amount - after calc", game, possibleTargets, options, true);
|
||||||
}
|
|
||||||
|
|
||||||
// split targets by groups
|
return options;
|
||||||
Map<UUID, String> 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<String, List<UUID>> groups = new HashMap<>();
|
|
||||||
targetGroups.forEach((id, groupKey) -> {
|
|
||||||
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
|
||||||
groups.get(groupKey).add(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// optimize logic:
|
|
||||||
// - use one target from each target group all the time
|
|
||||||
// - add random target from random group until fill all remainingAmount condition
|
|
||||||
|
|
||||||
// use one target per group
|
|
||||||
Set<UUID> newPossibleTargets = new HashSet<>();
|
|
||||||
groups.forEach((groupKey, groupTargets) -> {
|
|
||||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
|
||||||
if (targetId != null) {
|
|
||||||
newPossibleTargets.add(targetId);
|
|
||||||
groupTargets.remove(targetId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// use random target until fill condition
|
|
||||||
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
|
||||||
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
|
|
||||||
if (groupKey == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
|
|
||||||
if (groupTargets == null || groupTargets.isEmpty()) {
|
|
||||||
groups.remove(groupKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
|
||||||
if (targetId != null) {
|
|
||||||
newPossibleTargets.add(targetId);
|
|
||||||
groupTargets.remove(targetId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep final result
|
|
||||||
possibleTargets.clear();
|
|
||||||
possibleTargets.addAll(newPossibleTargets);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTargetGroupKeyAsPlayer(Player player) {
|
|
||||||
// use all
|
|
||||||
return String.join(";", Arrays.asList(
|
|
||||||
player.getName(),
|
|
||||||
String.valueOf(player.getId().hashCode())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
|
|
||||||
// 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())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTargetGroupKeyAsCard(Game game, Card card) {
|
|
||||||
// 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())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTargetGroupKeyAsOther(Game game, MageObject item) {
|
|
||||||
// use all
|
|
||||||
return String.join(";", Arrays.asList(
|
|
||||||
item.getName(),
|
|
||||||
String.valueOf(item.getId().hashCode())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug only. Print targets table and variations.
|
|
||||||
*/
|
|
||||||
private void printTargetsTableAndVariations(String info, Game game, Set<UUID> possibleTargets, List<TargetAmount> options, boolean isPrintOptions) {
|
|
||||||
if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return;
|
|
||||||
|
|
||||||
// output example:
|
|
||||||
//
|
|
||||||
// Targets (after optimize): 5
|
|
||||||
// 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2
|
|
||||||
// 1. PlayerA (SimulatedPlayer2)
|
|
||||||
//
|
|
||||||
// Target variations (info): 126
|
|
||||||
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1
|
|
||||||
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2
|
|
||||||
// 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2
|
|
||||||
|
|
||||||
// print table
|
|
||||||
List<UUID> list = new ArrayList<>(possibleTargets);
|
|
||||||
Collections.sort(list);
|
|
||||||
HashMap<UUID, Integer> targetNumbers = new HashMap<>();
|
|
||||||
System.out.println();
|
|
||||||
System.out.println(String.format("Targets (%s): %d", info, list.size()));
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
targetNumbers.put(list.get(i), i);
|
|
||||||
String targetName;
|
|
||||||
Player player = game.getPlayer(list.get(i));
|
|
||||||
if (player != null) {
|
|
||||||
targetName = player.toString();
|
|
||||||
} else {
|
|
||||||
MageObject object = game.getObject(list.get(i));
|
|
||||||
if (object != null) {
|
|
||||||
targetName = object.toString();
|
|
||||||
} else {
|
|
||||||
targetName = "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.out.println(String.format("%d. %s", i, targetName));
|
|
||||||
}
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
if (!isPrintOptions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// print amount variations
|
|
||||||
List<String> res = options
|
|
||||||
.stream()
|
|
||||||
.map(t -> t.getTargets()
|
|
||||||
.stream()
|
|
||||||
.map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id))
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.joining("; "))).sorted().collect(Collectors.toList());
|
|
||||||
System.out.println();
|
|
||||||
System.out.println(String.format("Target variations (info): %d", options.size()));
|
|
||||||
System.out.println(String.join("\n", res));
|
|
||||||
System.out.println();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
final protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import mage.util.RandomUtil;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author BetaSteward_at_googlemail.com
|
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||||
*/
|
*/
|
||||||
public abstract class TargetImpl implements Target {
|
public abstract class TargetImpl implements Target {
|
||||||
|
|
||||||
|
|
@ -321,7 +321,7 @@ public abstract class TargetImpl implements Target {
|
||||||
@Override
|
@Override
|
||||||
public boolean isChoiceSelected() {
|
public boolean isChoiceSelected() {
|
||||||
// min = max = 0 - for abilities with X=0, e.g. nothing to choose
|
// min = max = 0 - for abilities with X=0, e.g. nothing to choose
|
||||||
return chosen || getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0;
|
return chosen || getMaxNumberOfTargets() == 0 && getMinNumberOfTargets() == 0 || isSkipChoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -586,11 +586,24 @@ public abstract class TargetImpl implements Target {
|
||||||
@Override
|
@Override
|
||||||
public List<? extends TargetImpl> getTargetOptions(Ability source, Game game) {
|
public List<? extends TargetImpl> getTargetOptions(Ability source, Game game) {
|
||||||
List<TargetImpl> options = new ArrayList<>();
|
List<TargetImpl> options = new ArrayList<>();
|
||||||
List<UUID> possibleTargets = new ArrayList<>(possibleTargets(source.getControllerId(), source, game));
|
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
||||||
|
|
||||||
|
// optimizations for less memory/cpu consumptions
|
||||||
|
int maxPossibleTargetsToSimulate = Math.min(TargetOptimization.AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE, possibleTargets.size()); // see TargetAmount
|
||||||
|
if (getMinNumberOfTargets() > 0) {
|
||||||
|
maxPossibleTargetsToSimulate = Math.max(maxPossibleTargetsToSimulate, getMinNumberOfTargets());
|
||||||
|
}
|
||||||
|
TargetOptimization.printTargetsVariationsForTarget("target - before optimize", game, possibleTargets, options, false);
|
||||||
|
TargetOptimization.optimizePossibleTargets(source, game, possibleTargets, maxPossibleTargetsToSimulate);
|
||||||
|
TargetOptimization.printTargetsVariationsForTarget("target - after optimize", game, possibleTargets, options, false);
|
||||||
|
|
||||||
|
// calc all optimized combinations
|
||||||
|
// TODO: replace by google/apache lib to generate all combinations
|
||||||
|
List<UUID> needPossibleTargets = new ArrayList<>(possibleTargets);
|
||||||
|
|
||||||
// get the length of the array
|
// get the length of the array
|
||||||
// e.g. for {'A','B','C','D'} => N = 4
|
// e.g. for {'A','B','C','D'} => N = 4
|
||||||
int N = possibleTargets.size();
|
int N = needPossibleTargets.size();
|
||||||
// not enough targets, return no option
|
// not enough targets, return no option
|
||||||
if (N < getMinNumberOfTargets()) {
|
if (N < getMinNumberOfTargets()) {
|
||||||
return options;
|
return options;
|
||||||
|
|
@ -598,6 +611,7 @@ public abstract class TargetImpl implements Target {
|
||||||
// not target but that's allowed, return one empty option
|
// not target but that's allowed, return one empty option
|
||||||
if (N == 0) {
|
if (N == 0) {
|
||||||
TargetImpl target = this.copy();
|
TargetImpl target = this.copy();
|
||||||
|
target.setSkipChoice(true);
|
||||||
options.add(target);
|
options.add(target);
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
@ -617,6 +631,7 @@ public abstract class TargetImpl implements Target {
|
||||||
int minK = getMinNumberOfTargets();
|
int minK = getMinNumberOfTargets();
|
||||||
if (getMinNumberOfTargets() == 0) { // add option without targets if possible
|
if (getMinNumberOfTargets() == 0) { // add option without targets if possible
|
||||||
TargetImpl target = this.copy();
|
TargetImpl target = this.copy();
|
||||||
|
target.setSkipChoice(true);
|
||||||
options.add(target);
|
options.add(target);
|
||||||
minK = 1;
|
minK = 1;
|
||||||
}
|
}
|
||||||
|
|
@ -645,7 +660,7 @@ public abstract class TargetImpl implements Target {
|
||||||
//add the new target option
|
//add the new target option
|
||||||
TargetImpl target = this.copy();
|
TargetImpl target = this.copy();
|
||||||
for (int i = 0; i < combination.length; i++) {
|
for (int i = 0; i < combination.length; i++) {
|
||||||
target.addTarget(possibleTargets.get(combination[i]), source, game, true);
|
target.addTarget(needPossibleTargets.get(combination[i]), source, game, true);
|
||||||
}
|
}
|
||||||
options.add(target);
|
options.add(target);
|
||||||
index++;
|
index++;
|
||||||
|
|
@ -664,6 +679,9 @@ public abstract class TargetImpl implements Target {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TargetOptimization.printTargetsVariationsForTarget("target - after calc", game, possibleTargets, options, true);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
237
Mage/src/main/java/mage/target/TargetOptimization.java
Normal file
237
Mage/src/main/java/mage/target/TargetOptimization.java
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
package mage.target;
|
||||||
|
|
||||||
|
import mage.MageObject;
|
||||||
|
import mage.abilities.Ability;
|
||||||
|
import mage.cards.Card;
|
||||||
|
import mage.game.Game;
|
||||||
|
import mage.game.permanent.Permanent;
|
||||||
|
import mage.players.Player;
|
||||||
|
import mage.util.DebugUtil;
|
||||||
|
import mage.util.RandomUtil;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to optimize possible targets list for AI and playable calcs
|
||||||
|
* <p>
|
||||||
|
* Features:
|
||||||
|
* - less possible targets, less combinations, less CPU/memory usage for sims
|
||||||
|
* - group all possible targets by same characteristics;
|
||||||
|
* - fill target one by one from each group
|
||||||
|
*
|
||||||
|
* @author JayDi85
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
public static void optimizePossibleTargets(Ability source, Game game, Set<UUID> 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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split targets by groups
|
||||||
|
Map<UUID, String> 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<String, List<UUID>> groups = new HashMap<>();
|
||||||
|
targetGroups.forEach((id, groupKey) -> {
|
||||||
|
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
||||||
|
groups.get(groupKey).add(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// optimize logic:
|
||||||
|
// - use one target from each target group all the time
|
||||||
|
// - add random target from random group until fill all remainingAmount condition
|
||||||
|
|
||||||
|
// use one target per group
|
||||||
|
Set<UUID> newPossibleTargets = new HashSet<>();
|
||||||
|
groups.forEach((groupKey, groupTargets) -> {
|
||||||
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
|
if (targetId != null) {
|
||||||
|
newPossibleTargets.add(targetId);
|
||||||
|
groupTargets.remove(targetId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// use random target until fill condition
|
||||||
|
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||||
|
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
|
||||||
|
if (groupKey == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
|
||||||
|
if (groupTargets == null || groupTargets.isEmpty()) {
|
||||||
|
groups.remove(groupKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||||
|
if (targetId != null) {
|
||||||
|
newPossibleTargets.add(targetId);
|
||||||
|
groupTargets.remove(targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep final result
|
||||||
|
possibleTargets.clear();
|
||||||
|
possibleTargets.addAll(newPossibleTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTargetGroupKeyAsPlayer(Player player) {
|
||||||
|
// use all
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
player.getName(),
|
||||||
|
String.valueOf(player.getId().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
|
||||||
|
// 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())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTargetGroupKeyAsCard(Game game, Card card) {
|
||||||
|
// 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())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTargetGroupKeyAsOther(Game game, MageObject item) {
|
||||||
|
// use all
|
||||||
|
return String.join(";", Arrays.asList(
|
||||||
|
item.getName(),
|
||||||
|
String.valueOf(item.getId().hashCode())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void printTargetsVariationsForTarget(String info, Game game, Set<UUID> possibleTargets, List<TargetImpl> options, boolean isPrintOptions) {
|
||||||
|
List<Target> usedOptions = options.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(TargetImpl.class::cast)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
printTargetsTableAndVariationsInner(info, game, possibleTargets, usedOptions, isPrintOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void printTargetsVariationsForTargetAmount(String info, Game game, Set<UUID> possibleTargets, List<TargetAmount> options, boolean isPrintOptions) {
|
||||||
|
List<Target> usedOptions = options.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(TargetImpl.class::cast)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
printTargetsTableAndVariationsInner(info, game, possibleTargets, usedOptions, isPrintOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printTargetsTableAndVariationsInner(String info, Game game, Set<UUID> possibleTargets, List<Target> options, boolean isPrintOptions) {
|
||||||
|
if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return;
|
||||||
|
|
||||||
|
// output example:
|
||||||
|
//
|
||||||
|
// Targets (after optimize): 5
|
||||||
|
// 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2
|
||||||
|
// 1. PlayerA (SimulatedPlayer2)
|
||||||
|
//
|
||||||
|
// Target variations (info): 126
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2
|
||||||
|
// 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2
|
||||||
|
|
||||||
|
// print table
|
||||||
|
List<UUID> list = new ArrayList<>(possibleTargets);
|
||||||
|
Collections.sort(list);
|
||||||
|
HashMap<UUID, Integer> targetNumbers = new HashMap<>();
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(String.format("Targets (%s): %d", info, list.size()));
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
targetNumbers.put(list.get(i), i);
|
||||||
|
String targetName;
|
||||||
|
Player player = game.getPlayer(list.get(i));
|
||||||
|
if (player != null) {
|
||||||
|
targetName = player.toString();
|
||||||
|
} else {
|
||||||
|
MageObject object = game.getObject(list.get(i));
|
||||||
|
if (object != null) {
|
||||||
|
targetName = object.toString();
|
||||||
|
} else {
|
||||||
|
targetName = "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(String.format("%d. %s", i, targetName));
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
if (!isPrintOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// print amount variations
|
||||||
|
List<String> res = options
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getTargets()
|
||||||
|
.stream()
|
||||||
|
.map(id -> targetNumbers.get(id) + (t instanceof TargetAmount ? " -> " + t.getTargetAmount(id) : ""))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.joining("; "))).sorted().collect(Collectors.toList());
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(String.format("Target variations (info): %d", options.size()));
|
||||||
|
System.out.println(String.join("\n", res));
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ public class DebugUtil {
|
||||||
// game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout
|
// game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout
|
||||||
// so AI debug mode will make single simulation thread without any timeouts
|
// so AI debug mode will make single simulation thread without any timeouts
|
||||||
public static boolean AI_ENABLE_DEBUG_MODE = false;
|
public static boolean AI_ENABLE_DEBUG_MODE = false;
|
||||||
public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount
|
public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target and target amount calculations
|
||||||
|
|
||||||
// SERVER
|
// SERVER
|
||||||
// data collectors - enable additional logs and data collection for better AI and human games debugging
|
// data collectors - enable additional logs and data collection for better AI and human games debugging
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue