diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 55113d1e99e..2ad894e1a8c 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -346,7 +346,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { logger.debug("simulating -- node #:" + SimulationNode2.getCount() + " triggered ability option"); for (Target target : ability.getTargets()) { for (UUID targetId : target.getTargets()) { - newNode.getTargets().add(targetId); + newNode.getTargets().add(targetId); // save for info only (real targets in newNode.ability already) } } parent.children.add(newNode); diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 38358cda3bb..a70fcb4402b 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -70,7 +70,7 @@ public class ComputerPlayer extends PlayerImpl { protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available // debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout) - protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; + protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = DebugUtil.AI_ENABLE_DEBUG_MODE; // AI agents uses game simulation thread for all calcs and it's high CPU consumption // More AI threads - more parallel AI games can be calculate @@ -82,7 +82,7 @@ public class ComputerPlayer extends PlayerImpl { // * use your's CPU cores for best performance // TODO: add server config to control max AI threads (with CPU cores by default) // TODO: rework AI implementation to use multiple sims calculation instead one by one - final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5; + final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5; private final transient Map unplayable = new TreeMap<>(); private final transient List playableNonInstant = new ArrayList<>(); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java index 2bdd36e9bf0..106600945cb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/SimulationPerformanceAITest.java @@ -1,7 +1,10 @@ package org.mage.test.AI.basic; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.game.permanent.Permanent; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -11,6 +14,13 @@ import java.util.Arrays; import java.util.List; /** + * Possible problems: + * - big memory consumption on sims prepare (memory overflow) + * - too many sims to calculate (AI fail on time out and do nothing) + *

+ * 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) + * * @author JayDi85 */ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { @@ -21,7 +31,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { } @Test - public void test_AIvsAI_Simple() { + public void test_Simple_ShortGame() { // both must kill x2 bears by x2 bolts addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 2); @@ -39,7 +49,7 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { } @Test - public void test_AIvsAI_LongGame() { + public void test_Simple_LongGame() { // many bears and bolts must help to end game fast int maxTurn = 50; removeAllCardsFromLibrary(playerA); @@ -63,13 +73,27 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { Assert.assertTrue("One of player must won a game before turn " + maxTurn + ", but it ends on " + currentGame, currentGame.hasEnded()); } - private void runManyTargetOptionsTest(String info, int totalCreatures, int needDiedCreatures, int needPlayerLife) { + private void runManyTargetOptionsInTrigger(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) { // When Bogardan Hellkite enters, it deals 5 damage divided as you choose among any number of targets. addCard(Zone.HAND, playerA, "Bogardan Hellkite", 1); // {6}{R}{R} addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); // addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures); + if (isDamageRandomCreature) { + runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> { + Permanent creature = game.getBattlefield().getAllPermanents().stream() + .filter(p -> p.getName().equals("Balduvian Bears")) + .findAny() + .orElse(null); + Assert.assertNotNull(creature); + Ability fakeAbility = new SimpleStaticAbility(null); + fakeAbility.setControllerId(player.getId()); + fakeAbility.setSourceId(creature.getId()); + creature.damage(1, fakeAbility, game); + }); + } + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -80,34 +104,107 @@ public class SimulationPerformanceAITest extends CardTestPlayerBaseAI { } @Test - public void test_AIvsAI_ManyTargetOptions_Simple() { + public void test_ManyTargetOptions_Triggered_Single() { // 2 damage to bear and 3 damage to player B - runManyTargetOptionsTest("1 target creature", 1, 1, 20 - 3); + runManyTargetOptionsInTrigger("1 target creature", 1, 1, false, 20 - 3); } @Test - public void test_AIvsAI_ManyTargetOptions_Few() { + public void test_ManyTargetOptions_Triggered_Few() { // 4 damage to x2 bears and 1 damage to player B - runManyTargetOptionsTest("2 target creatures", 2, 2, 20 - 1); + runManyTargetOptionsInTrigger("2 target creatures", 2, 2, false, 20 - 1); } @Test - public void test_AIvsAI_ManyTargetOptions_Many() { + public void test_ManyTargetOptions_Triggered_Many() { // 4 damage to x2 bears and 1 damage to player B - runManyTargetOptionsTest("5 target creatures", 2, 2, 20 - 1); + runManyTargetOptionsInTrigger("5 target creatures", 5, 2, false, 20 - 1); } @Test - @Ignore // AI code must be improved - // TODO: need memory optimization - // TODO: need sims/options amount optimization (example: target name + score as unique param to reduce possible options) - // TODO: need best choice selection on timeout (AI must make any good/bad choice on timeout with game log - not a skip) - public void test_AIvsAI_ManyTargetOptions_TooMuch() { - // possible problems: - // - big memory consumption on sims prepare (memory overflow) - // - too many sims to calculate (AI fail on time out and do nothing) + public void test_ManyTargetOptions_Triggered_TooMuch() { + // warning, can be slow + + // make sure targets optimization works + // (must ignore same targets for faster calc) // 4 damage to x2 bears and 1 damage to player B - runManyTargetOptionsTest("50 target creatures", 50, 2, 20 - 1); + runManyTargetOptionsInTrigger("50 target creatures", 50, 2, false, 20 - 1); + } + + @Test + @Ignore // TODO: AI do not support game simulations for target options in triggers + public void test_ManyTargetOptions_Triggered_TargetGroups() { + // make sure targets optimization can find unique creatures, e.g. damaged + + // 4 damage to x2 bears and 1 damage to damaged bear + runManyTargetOptionsInTrigger("5 target creatures with one damaged", 5, 3, true, 20); + } + + private void runManyTargetOptionsInActivate(String info, int totalCreatures, int needDiedCreatures, boolean isDamageRandomCreature, int needPlayerLife) { + // Boulderfall deals 5 damage divided as you choose among any number of targets. + addCard(Zone.HAND, playerA, "Boulderfall", 1); // {6}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", totalCreatures); + + if (isDamageRandomCreature) { + runCode("damage creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (s, player, game) -> { + Permanent creature = game.getBattlefield().getAllPermanents().stream() + .filter(p -> p.getName().equals("Balduvian Bears")) + .findAny() + .orElse(null); + Assert.assertNotNull(creature); + Ability fakeAbility = new SimpleStaticAbility(null); + fakeAbility.setControllerId(player.getId()); + fakeAbility.setSourceId(creature.getId()); + creature.damage(1, fakeAbility, game); + }); + } + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Boulderfall", 1); // if fail then AI stops before all sims ends + assertGraveyardCount(playerB, "Balduvian Bears", needDiedCreatures); + assertLife(playerB, needPlayerLife); + } + + @Test + public void test_ManyTargetOptions_Activated_Single() { + // 2 damage to bear and 3 damage to player B + runManyTargetOptionsInActivate("1 target creature", 1, 1, false, 20 - 3); + } + + @Test + public void test_ManyTargetOptions_Activated_Few() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("2 target creatures", 2, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_Many() { + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("5 target creatures", 5, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_TooMuch() { + // warning, can be slow + + // make sure targets optimization works + // (must ignore same targets for faster calc) + + // 4 damage to x2 bears and 1 damage to player B + runManyTargetOptionsInActivate("50 target creatures", 50, 2, false, 20 - 1); + } + + @Test + public void test_ManyTargetOptions_Activated_TargetGroups() { + // make sure targets optimization can find unique creatures, e.g. damaged + + // 4 damage to x2 bears and 1 damage to damaged bear + runManyTargetOptionsInActivate("5 target creatures with one damaged", 5, 3, true, 20); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java index cb24db3af3d..51b5efea6c6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java @@ -31,6 +31,8 @@ public class TargetAmountAITest extends CardTestPlayerBaseWithAIHelps { @Test public void test_AI_SimulateTargets() { + // warning, test depends on targets list optimization by AI + // Distribute four +1/+1 counters among any number of target creatures. addCard(Zone.HAND, playerA, "Blessings of Nature", 1); // {4}{G} addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 55f2383cba4..b853646cebb 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -4457,11 +4457,7 @@ public abstract class PlayerImpl implements Player, Serializable { } /** - * Only used for AIs - * - * @param ability - * @param game - * @return + * AI related code */ @Override public List getPlayableOptions(Ability ability, Game game) { @@ -4482,6 +4478,9 @@ public abstract class PlayerImpl implements Player, Serializable { return options; } + /** + * AI related code + */ private void addModeOptions(List options, Ability option, Game game) { // TODO: support modal spells with more than one selectable mode (also must use max modes filter) for (Mode mode : option.getModes().values()) { @@ -4504,11 +4503,18 @@ public abstract class PlayerImpl implements Player, Serializable { } } + /** + * AI related code + */ protected void addVariableXOptions(List options, Ability option, int targetNum, Game game) { addTargetOptions(options, option, targetNum, game); } + /** + * AI related code + */ protected void addTargetOptions(List options, Ability option, int targetNum, Game game) { + // TODO: target options calculated for triggered ability too, but do not used in real game for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) { Ability newOption = option.copy(); if (target instanceof TargetAmount) { @@ -4521,7 +4527,7 @@ public abstract class PlayerImpl implements Player, Serializable { newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true); } } - if (targetNum < option.getTargets().size() - 2) { + if (targetNum < option.getTargets().size() - 2) { // wtf addTargetOptions(options, newOption, targetNum + 1, game); } else if (!option.getCosts().getTargets().isEmpty()) { addCostTargetOptions(options, newOption, 0, game); @@ -4531,6 +4537,9 @@ public abstract class PlayerImpl implements Player, Serializable { } } + /** + * AI related code + */ private void addCostTargetOptions(List options, Ability option, int targetNum, Game game) { for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) { Ability newOption = option.copy(); diff --git a/Mage/src/main/java/mage/target/Target.java b/Mage/src/main/java/mage/target/Target.java index 8ef2e97d69c..9664a0a10ca 100644 --- a/Mage/src/main/java/mage/target/Target.java +++ b/Mage/src/main/java/mage/target/Target.java @@ -79,6 +79,9 @@ public interface Target extends Serializable { boolean isLegal(Ability source, Game game); + /** + * AI related code. Returns all possible different target combinations + */ List getTargetOptions(Ability source, Game game); boolean canChoose(UUID sourceControllerId, Game game); diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index 68107f825a8..d128d38ec9b 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -1,11 +1,16 @@ package mage.target; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; +import mage.cards.Card; import mage.constants.Outcome; 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; @@ -118,45 +123,229 @@ public abstract class TargetAmount extends TargetImpl { } @Override - public List getTargetOptions(Ability source, Game game) { + final public List getTargetOptions(Ability source, Game game) { + if (!amountWasSet) { + setAmount(source, game); + } + List options = new ArrayList<>(); Set possibleTargets = possibleTargets(source.getControllerId(), source, game); - addTargets(this, possibleTargets, options, source, game); + // optimizations for less memory/cpu consumptions + printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false); + optimizePossibleTargets(source, game, possibleTargets); + printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false); - // debug target variations - //printTargetsVariations(possibleTargets, options); + // calc possible amount variations + addTargets(this, possibleTargets, options, source, game); + printTargetsTableAndVariations("after calc", game, possibleTargets, options, true); return options; } - private void printTargetsVariations(Set possibleTargets, List options) { - // debug target variations - // permanent index + amount - // example: 7 -> 2; 8 -> 3; 9 -> 1 + /** + * AI related, trying to reduce targets for simulations + */ + private void optimizePossibleTargets(Ability source, Game game, Set possibleTargets) { + // remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens) + + // it must have additional threshold to keep more variations for analyse + // + // bad example: + // - Blessings of Nature + // - 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 + + int maxPossibleTargetsToSimulate = this.remainingAmount * 2; + 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); + }); + + // 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 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 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 possibleTargets, List 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 list = new ArrayList<>(possibleTargets); + Collections.sort(list); HashMap 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 res = options .stream() .map(t -> t.getTargets() .stream() .map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id)) .sorted() - .collect(Collectors.joining("; "))) - .collect(Collectors.toList()); - Collections.sort(res); + .collect(Collectors.joining("; "))).sorted().collect(Collectors.toList()); System.out.println(); - System.out.println(res.stream().collect(Collectors.joining("\n"))); + System.out.println(String.format("Target variations (info): %d", options.size())); + System.out.println(String.join("\n", res)); System.out.println(); } - protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { - if (!amountWasSet) { - setAmount(source, game); - } + final protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { Set usedTargets = new HashSet<>(); for (UUID targetId : possibleTargets) { usedTargets.add(targetId); diff --git a/Mage/src/main/java/mage/target/TargetImpl.java b/Mage/src/main/java/mage/target/TargetImpl.java index 1a937a54ef4..0aae5458609 100644 --- a/Mage/src/main/java/mage/target/TargetImpl.java +++ b/Mage/src/main/java/mage/target/TargetImpl.java @@ -446,13 +446,6 @@ public abstract class TargetImpl implements Target { return !targets.isEmpty(); } - /** - * Returns all possible different target combinations - * - * @param source - * @param game - * @return - */ @Override public List getTargetOptions(Ability source, Game game) { List options = new ArrayList<>(); diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java index 51f029ca0d9..09ba1e89659 100644 --- a/Mage/src/main/java/mage/util/DebugUtil.java +++ b/Mage/src/main/java/mage/util/DebugUtil.java @@ -11,6 +11,12 @@ public class DebugUtil { public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands) + // AI + // 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 + public static boolean AI_ENABLE_DEBUG_MODE = false; + public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount + // cards basic (card panels) public static boolean GUI_CARD_DRAW_OUTER_BORDER = false; public static boolean GUI_CARD_DRAW_INNER_BORDER = false;