From 64e948e4b382a5db3725c2983b63446bfa24f98d Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 24 Dec 2020 15:02:28 +0400 Subject: [PATCH] * AI: improved game performance by x10 for cards with target amount; AI: added targeting name and amount info to simulation logs; AI: removed duplicated target variations from target amount simulations; --- .../src/mage/player/ai/ComputerPlayer6.java | 56 +++++++++++++++---- .../java/mage/player/ai/ComputerPlayer.java | 3 +- .../src/mage/cards/b/BlessingsOfNature.java | 8 +-- .../test/AI/basic/TargetAmountAITest.java | 53 ++++++++++++++++++ .../split/CastSplitCardsWithSpliceTest.java | 2 - .../main/java/mage/target/TargetAmount.java | 56 ++++++++++++++----- 6 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index caed86ffc6b..15b49248b98 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -1,5 +1,6 @@ package mage.player.ai; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.abilities.SpellAbility; @@ -29,6 +30,7 @@ import mage.player.ai.util.CombatInfo; import mage.player.ai.util.CombatUtil; import mage.players.Player; import mage.target.Target; +import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.Targets; import mage.util.RandomUtil; @@ -37,6 +39,7 @@ import org.apache.log4j.Logger; import java.io.File; import java.util.*; import java.util.concurrent.*; +import java.util.stream.Collectors; /** * @author nantuko @@ -155,7 +158,13 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { boolean usedStack = false; while (actions.peek() != null) { Ability ability = actions.poll(); - logger.info(new StringBuilder("===> Act [").append(game.getPlayer(playerId).getName()).append("] Action: ").append(ability.toString()).toString()); + // log example: ===> Act [PlayerA] Action: Cast Blessings of Nature (target 1; target 2) + logger.info(new StringBuilder("===> Act [") + .append(game.getPlayer(playerId).getName()) + .append("] Action: ") + .append(ability.toString()) + .append(listTargets(game, ability.getTargets(), " (targeting %s)", "")) + .toString()); if (!ability.getTargets().isEmpty()) { for (Target target : ability.getTargets()) { for (UUID id : target.getTargets()) { @@ -506,7 +515,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { StringBuilder sb = new StringBuilder("Sim Prio [").append(depth).append("] #").append(counter) .append(" <").append(val).append("> (").append(action) .append(action.isModal() ? " Mode = " + action.getModes().getMode().toString() : "") - .append(listTargets(game, action.getTargets())).append(')') + .append(listTargets(game, action.getTargets(), " (targeting %s)", "")).append(')') .append(logger.isTraceEnabled() ? " #" + newNode.hashCode() : ""); SimulationNode2 logNode = newNode; while (logNode.getChildren() != null @@ -541,7 +550,11 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { if (depth == maxDepth) { GameStateEvaluator2.PlayerEvaluateScore score = GameStateEvaluator2.evaluate(this.getId(), bestNode.game); String scoreInfo = " [" + score.getPlayerInfoShort() + "-" + score.getOpponentInfoShort() + "]"; - logger.info("Sim Prio [" + depth + "] -- Saved best node yet <" + bestNode.getScore() + scoreInfo + "> " + bestNode.getAbilities().toString()); + String abilitiesInfo = bestNode.getAbilities() + .stream() + .map(a -> a.toString() + listTargets(game, a.getTargets(), " (targeting %s)", "")) + .collect(Collectors.joining("; ")); + logger.info("Sim Prio [" + depth + "] -- Saved best node yet <" + bestNode.getScore() + scoreInfo + "> " + abilitiesInfo); node.children.clear(); node.children.add(bestNode); node.setScore(bestNode.getScore()); @@ -1009,17 +1022,36 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { return suggestedActions.size(); } - protected String listTargets(Game game, Targets targets) { - StringBuilder sb = new StringBuilder(); - if (targets != null) { - for (Target target : targets) { - sb.append('[').append(target.getTargetedName(game)).append(']'); - } - if (sb.length() > 0) { - sb.insert(0, " targeting "); + /** + * Return info about targets list (targeting objects) + * + * @param game + * @param targets + * @param format example: my %s in data + * @param emptyText default text for empty targets list + * @return + */ + protected String listTargets(Game game, Targets targets, String format, String emptyText) { + List res = new ArrayList<>(); + for (Target target : targets) { + for (UUID id : target.getTargets()) { + MageObject object = game.getObject(id); + if (object != null) { + String prefix = ""; + if (target instanceof TargetAmount) { + prefix = " " + target.getTargetAmount(id) + "x "; + } + res.add(prefix + object.getIdName()); + } } } - return sb.toString(); + String info = String.join("; ", res); + + if (info.isEmpty()) { + return emptyText; + } else { + return String.format(format, info); + } } @Override 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 37edf439bb2..b2c2ed02f05 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 @@ -1130,7 +1130,8 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } - log.warn("No proper AI target handling: " + target.getClass().getName()); + // it's ok on no targets available + log.warn("No proper AI target handling or can't find permanents/cards to target: " + target.getClass().getName()); return false; } diff --git a/Mage.Sets/src/mage/cards/b/BlessingsOfNature.java b/Mage.Sets/src/mage/cards/b/BlessingsOfNature.java index 43232753809..191c602983f 100644 --- a/Mage.Sets/src/mage/cards/b/BlessingsOfNature.java +++ b/Mage.Sets/src/mage/cards/b/BlessingsOfNature.java @@ -1,7 +1,5 @@ - package mage.cards.b; -import java.util.UUID; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.counter.DistributeCountersEffect; import mage.abilities.keyword.MiracleAbility; @@ -11,15 +9,15 @@ import mage.constants.CardType; import mage.counters.CounterType; import mage.target.common.TargetCreaturePermanentAmount; +import java.util.UUID; + /** - * * @author North */ public final class BlessingsOfNature extends CardImpl { public BlessingsOfNature(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{4}{G}"); - + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}"); // Distribute four +1/+1 counters among any number of target creatures. this.getSpellAbility().addEffect(new DistributeCountersEffect(CounterType.P1P1, 4, false, "any number of target creatures")); 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 new file mode 100644 index 00000000000..09776653e3e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TargetAmountAITest.java @@ -0,0 +1,53 @@ +package org.mage.test.AI.basic; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class TargetAmountAITest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_AI_ChooseTargets() { + // 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); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 4); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 4); // 2/2 + + // ai must choose by special dialog, not full simulation + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blessings of Nature"); + + setStopAt(1, PhaseStep.END_TURN); + //setStrictChooseMode(true); // ai must choose + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 4, 2 + 4); // boost one creature (it's just a choose code, so can be different from simulation results) + } + + @Test + public void test_AI_SimulateTargets() { + // 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); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 4); // 2/2 + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 4); // 2/2 + + // AI must put creatures on own permanents (all in one creature to boost it) + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 1, 2 + 1); // boost each possible creatures + assertPowerToughness(playerB, "Balduvian Bears", 2, 2); // no boost for enemy + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithSpliceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithSpliceTest.java index e9d2b3d5b1d..85b9b889479 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithSpliceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithSpliceTest.java @@ -79,8 +79,6 @@ public class CastSplitCardsWithSpliceTest extends CardTestPlayerBase { addTarget(playerA, "Bow of Nylea"); // target right // must used all mana - //showAvaileableMana("after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); - setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); diff --git a/Mage/src/main/java/mage/target/TargetAmount.java b/Mage/src/main/java/mage/target/TargetAmount.java index 41da56d5ea1..72012fe9d67 100644 --- a/Mage/src/main/java/mage/target/TargetAmount.java +++ b/Mage/src/main/java/mage/target/TargetAmount.java @@ -6,10 +6,7 @@ import mage.abilities.dynamicvalue.common.StaticValue; import mage.constants.Outcome; import mage.game.Game; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; /** @@ -52,10 +49,10 @@ public abstract class TargetAmount extends TargetImpl { @Override public boolean doneChosing() { - return amountWasSet - && (remainingAmount == 0 - || (getMinNumberOfTargets() < getMaxNumberOfTargets() - && getTargets().size() >= getMinNumberOfTargets())); + return amountWasSet + && (remainingAmount == 0 + || (getMinNumberOfTargets() < getMaxNumberOfTargets() + && getTargets().size() >= getMinNumberOfTargets())); } @Override @@ -100,14 +97,14 @@ public abstract class TargetAmount extends TargetImpl { } chosen = isChosen(); while (remainingAmount > 0) { - if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) { + if (!getTargetController(game, playerId).chooseTargetAmount(outcome, this, source, game)) { return chosen; } chosen = isChosen(); } return chosen = true; } - + @Override public List getTargetOptions(Ability source, Game game) { List options = new ArrayList<>(); @@ -115,21 +112,50 @@ public abstract class TargetAmount extends TargetImpl { addTargets(this, possibleTargets, options, source, game); + // debug target variations + //printTargetsVariations(possibleTargets, options); + return options; } - protected void addTargets(TargetAmount target, Set targets, List options, Ability source, Game game) { + private void printTargetsVariations(Set possibleTargets, List options) { + // debug target variations + // permanent index + amount + // example: 7 -> 2; 8 -> 3; 9 -> 1 + List list = new ArrayList<>(possibleTargets); + HashMap targetNumbers = new HashMap<>(); + for (int i = 0; i < list.size(); i++) { + targetNumbers.put(list.get(i), i); + } + 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); + System.out.println(); + System.out.println(res.stream().collect(Collectors.joining("\n"))); + System.out.println(); + } + + protected void addTargets(TargetAmount target, Set possibleTargets, List options, Ability source, Game game) { if (!amountWasSet) { setAmount(source, game); } - for (UUID targetId : targets) { + Set usedTargets = new HashSet<>(); + for (UUID targetId : possibleTargets) { + usedTargets.add(targetId); for (int n = 1; n <= target.remainingAmount; n++) { TargetAmount t = target.copy(); t.addTarget(targetId, n, source, game, true); if (t.remainingAmount > 0) { - if (targets.size() > 1) { - Set newTargets = targets.stream().filter(newTarget -> !newTarget.equals(targetId)).collect(Collectors.toSet()); - addTargets(t, newTargets, options, source, game); + if (possibleTargets.size() > 1) { + // don't use that target again + Set newPossibleTargets = possibleTargets.stream().filter(newTarget -> !usedTargets.contains(newTarget)).collect(Collectors.toSet()); + addTargets(t, newPossibleTargets, options, source, game); } } else { options.add(t);