diff --git a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java index 2ae8d4cae79..27c89d98863 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java +++ b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java @@ -7,6 +7,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.Target; import mage.target.TargetPermanent; +import mage.target.TargetPlayer; import mage.target.common.TargetPermanentOrPlayer; /** @@ -76,6 +77,10 @@ abstract class BaseTestableDialog implements TestableDialog { return createAnyTarget(min, max, false); } + static Target createPlayerTarget(int min, int max, boolean notTarget) { + return new TargetPlayer(min, max, notTarget); + } + private static Target createAnyTarget(int min, int max, boolean notTarget) { return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget); } diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java index 017aa055b27..3a949ee5350 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java +++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java @@ -126,6 +126,19 @@ class ChooseTargetTestableDialog extends BaseTestableDialog { runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE)).aiMustChoose(false, 0)); + // + // additional tests for 2 possible options limitation + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0", createPlayerTarget(0, 0, notTarget)).aiMustChoose(false, 0)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-1", createPlayerTarget(0, 1, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-2", createPlayerTarget(0, 2, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 0-5", createPlayerTarget(0, 5, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1", createPlayerTarget(1, 1, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1-2", createPlayerTarget(1, 2, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 1-5", createPlayerTarget(1, 5, notTarget)).aiMustChoose(true, 1)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 2", createPlayerTarget(2, 2, notTarget)).aiMustChoose(true, 2)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 2-5", createPlayerTarget(2, 5, notTarget)).aiMustChoose(true, 2)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 3", createPlayerTarget(3, 3, notTarget)).aiMustChoose(false, 0)); + runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "player 3-5", createPlayerTarget(3, 5, notTarget)).aiMustChoose(false, 0)); } } } diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java index 0970d8ecc94..f4805f2184e 100644 --- a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java +++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java @@ -8,10 +8,7 @@ import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -120,10 +117,8 @@ public class TestableDialogsRunner { choice = prepareSelectDialogChoice(needGroup); player.choose(Outcome.Benefit, choice, game); if (choice.getChoiceKey() != null) { - int needIndex = Integer.parseInt(choice.getChoiceKey()); - if (needIndex < this.dialogs.size()) { - needDialog = this.dialogs.get(needIndex); - } + int needRegNumber = Integer.parseInt(choice.getChoiceKey()); + needDialog = this.dialogs.getOrDefault(needRegNumber, null); } } if (needDialog == null) { @@ -144,15 +139,20 @@ public class TestableDialogsRunner { Choice choice = new ChoiceImpl(false); choice.setMessage("Choose dialogs group to run"); + // use min reg number for groups + Map groupNumber = new HashMap<>(); + this.dialogs.values().forEach(dialog -> { + groupNumber.put(dialog.getGroup(), Math.min(groupNumber.getOrDefault(dialog.getGroup(), Integer.MAX_VALUE), dialog.getRegNumber())); + }); + // main groups - int recNumber = 0; for (int i = 0; i < groups.size(); i++) { - recNumber++; String group = groups.get(i); + Integer groupMinNumber = groupNumber.getOrDefault(group, 0); choice.withItem( String.valueOf(i), - String.format("%02d. %s", recNumber, group), - recNumber, + String.format("%02d. %s", groupMinNumber, group), + groupMinNumber, ChoiceHintType.TEXT, String.join("
", group) ); @@ -186,18 +186,15 @@ public class TestableDialogsRunner { private Choice prepareSelectDialogChoice(String needGroup) { Choice choice = new ChoiceImpl(false); choice.setMessage("Choose game dialog to run from " + needGroup); - int recNumber = 0; - for (int i = 0; i < this.dialogs.size(); i++) { - TestableDialog dialog = this.dialogs.get(i); + for (TestableDialog dialog : this.dialogs.values()) { if (!dialog.getGroup().equals(needGroup)) { continue; } - recNumber++; String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription()); choice.withItem( - String.valueOf(i), - String.format("%02d. %s", recNumber, info), - recNumber, + String.valueOf(dialog.getRegNumber()), + String.format("%02d. %s", dialog.getRegNumber(), info), + dialog.getRegNumber(), ChoiceHintType.TEXT, String.join("
", info) ); 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 f25152f9a90..5459acab7b0 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 @@ -405,13 +405,13 @@ public class ComputerPlayer6 extends ComputerPlayer { if (effect != null && stackObject.getControllerId().equals(playerId)) { Target target = effect.getTarget(); - if (!target.isChoiceCompleted(getId(), (StackAbility) stackObject, game)) { + if (!target.isChoiceCompleted(getId(), (StackAbility) stackObject, game, null)) { for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) { Game sim = game.createSimulationForAI(); StackAbility newAbility = (StackAbility) stackObject.copy(); SearchEffect newEffect = getSearchEffect(newAbility); newEffect.getTarget().addTarget(targetId, newAbility, sim); - sim.getStack().push(newAbility); + sim.getStack().push(sim, newAbility); SimulationNode2 newNode = new SimulationNode2(node, sim, depth, stackObject.getControllerId()); node.children.add(newNode); newNode.getTargets().add(targetId); @@ -886,10 +886,10 @@ public class ComputerPlayer6 extends ComputerPlayer { } UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); - if (!target.isChoiceCompleted(abilityControllerId, source, game)) { + if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) { for (UUID targetId : targets) { target.addTarget(targetId, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { targets.clear(); return true; } @@ -906,10 +906,10 @@ public class ComputerPlayer6 extends ComputerPlayer { } UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); - if (!target.isChoiceCompleted(abilityControllerId, source, game)) { + if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) { for (UUID targetId : targets) { target.add(targetId, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) { targets.clear(); return true; } 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 4452e3e682e..d787cd2f5a2 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 @@ -105,7 +105,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { return list; } - protected void simulateOptions(Game game) { + private void simulateOptions(Game game) { List playables = game.getPlayer(playerId).getPlayable(game, isSimulatedPlayer); for (ActivatedAbility ability : playables) { if (ability.isManaAbility()) { @@ -176,6 +176,10 @@ public final class SimulatedPlayer2 extends ComputerPlayer { return options; } + // remove invalid targets + // TODO: is it useless cause it already filtered before? + options.removeIf(option -> !option.getTargets().isChosen(game)); + if (AI_SIMULATE_ALL_BAD_AND_GOOD_TARGETS) { return options; } @@ -315,7 +319,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer { List options = getPlayableOptions(ability, game); if (options.isEmpty()) { logger.debug("simulating -- triggered ability:" + ability); - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); if (ability.activate(game, false) && ability.isUsesStack()) { game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); } @@ -337,9 +341,9 @@ public final class SimulatedPlayer2 extends ComputerPlayer { protected void addAbilityNode(SimulationNode2 parent, Ability ability, int depth, Game game) { Game sim = game.createSimulationForAI(); - sim.getStack().push(new StackAbility(ability, playerId)); + sim.getStack().push(sim, new StackAbility(ability, playerId)); if (ability.activate(sim, false) && ability.isUsesStack()) { - game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); + sim.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); } sim.applyEffects(); SimulationNode2 newNode = new SimulationNode2(parent, sim, depth, playerId); 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 20a8114e4f2..441a45bbfb4 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 @@ -142,17 +142,27 @@ public class ComputerPlayer extends PlayerImpl { UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); // nothing to choose, e.g. X=0 - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { return false; } - // default logic for any targets PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game); possibleTargetsSelector.findNewTargets(fromCards); + + // nothing to choose, e.g. no valid targets + if (!possibleTargetsSelector.hasAnyTargets()) { + return false; + } + + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + // good targets -- choose as much as possible for (MageItem item : possibleTargetsSelector.getGoodTargets()) { target.add(item.getId(), game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { return true; } } @@ -226,7 +236,7 @@ public class ComputerPlayer extends PlayerImpl { UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); // nothing to choose, e.g. X=0 - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return false; } @@ -238,6 +248,11 @@ public class ComputerPlayer extends PlayerImpl { return false; } + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + // KILL PRIORITY if (outcome == Outcome.Damage) { // opponent first @@ -251,7 +266,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { target.addTarget(item.getId(), leftLife, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -268,7 +283,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { target.addTarget(item.getId(), leftLife, source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -283,7 +298,7 @@ public class ComputerPlayer extends PlayerImpl { continue; } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -303,7 +318,7 @@ public class ComputerPlayer extends PlayerImpl { int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); if (leftLife > 1) { target.addTarget(item.getId(), Math.min(leftLife - 1, target.getAmountRemaining()), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -322,7 +337,7 @@ public class ComputerPlayer extends PlayerImpl { return !target.getTargets().isEmpty(); } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -339,7 +354,7 @@ public class ComputerPlayer extends PlayerImpl { continue; } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } @@ -355,7 +370,7 @@ public class ComputerPlayer extends PlayerImpl { return !target.getTargets().isEmpty(); } target.addTarget(item.getId(), target.getAmountRemaining(), source, game); - if (target.isChoiceCompleted(abilityControllerId, source, game)) { + if (target.isChoiceCompleted(abilityControllerId, source, game, null)) { return true; } } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java index c3d9f25ea22..cd3184b2c55 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java @@ -47,7 +47,6 @@ public class PossibleTargetsSelector { // collect new valid targets List found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream() .filter(id -> !target.contains(id)) - .filter(id -> target.canTarget(abilityControllerId, id, source, game)) .map(id -> { Player player = game.getPlayer(id); if (player != null) { @@ -137,6 +136,10 @@ public class PossibleTargetsSelector { } } + public List getAny() { + return this.any; + } + public static boolean isMyItem(UUID abilityControllerId, MageItem item) { if (item instanceof Player) { return item.getId().equals(abilityControllerId); @@ -181,7 +184,12 @@ public class PossibleTargetsSelector { return false; } - boolean hasAnyTargets() { + public boolean hasAnyTargets() { return !this.any.isEmpty(); } + + public boolean hasMinNumberOfTargets() { + return this.target.getMinNumberOfTargets() == 0 + || this.any.size() >= this.target.getMinNumberOfTargets(); + } } \ No newline at end of file diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java index f3dd92bb7be..0649b72732e 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/SimulatedPlayerMCTS.java @@ -121,7 +121,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { } } if (ability.isUsesStack()) { - game.getStack().push(new StackAbility(ability, playerId)); + game.getStack().push(game, new StackAbility(ability, playerId)); if (ability.activate(game, false)) { game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); actionCount++; @@ -187,8 +187,8 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { abort = true; } - protected boolean chooseRandom(Target target, Game game) { - Set possibleTargets = target.possibleTargets(playerId, game); + private boolean chooseRandom(Target target, Ability source, Game game) { + Set possibleTargets = target.possibleTargets(playerId, source, game); if (possibleTargets.isEmpty()) { return false; } @@ -233,7 +233,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game) { if (this.isHuman()) { - return chooseRandom(target, game); + return chooseRandom(target, source, game); } return super.choose(outcome, target, source, game); } @@ -241,7 +241,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { if (this.isHuman()) { - return chooseRandom(target, game); + return chooseRandom(target, source, game); } return super.choose(outcome, target, source, game, options); } @@ -252,7 +252,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer { if (cards.isEmpty()) { return false; } - Set possibleTargets = target.possibleTargets(playerId, cards, source, game); + Set possibleTargets = target.possibleTargets(playerId, source, game, cards); if (possibleTargets.isEmpty()) { return false; } diff --git a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java index 7a41de3d740..406f8580e8b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/dialogs/TestableDialogsTest.java @@ -6,6 +6,7 @@ import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.PlayAdditionalLandsAllEffect; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.util.ConsoleUtil; import mage.utils.testers.TestableDialog; import mage.utils.testers.TestableDialogsRunner; import org.junit.Assert; @@ -107,7 +108,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { @Test @Ignore // debug only - run single dialog by reg number public void test_RunSingle_Debugging() { - int needRegNumber = 557; + int needRegNumber = 5; prepareCards(); @@ -233,15 +234,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { if (resAssert == null) { totalUnknown++; status = "?"; - coloredTexts.put("?", asYellow("?")); + coloredTexts.put("?", ConsoleUtil.asYellow("?")); } else if (resAssert.isEmpty()) { totalGood++; status = "OK"; - coloredTexts.put("OK", asGreen("OK")); + coloredTexts.put("OK", ConsoleUtil.asGreen("OK")); } else { totalBad++; status = "FAIL"; - coloredTexts.put("FAIL", asRed("FAIL")); + coloredTexts.put("FAIL", ConsoleUtil.asRed("FAIL")); assertError = resAssert; } if (!assertError.isEmpty()) { @@ -256,8 +257,8 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { // print dialog error if (!assertError.isEmpty()) { coloredTexts.clear(); - coloredTexts.put(resAssert, asRed(resAssert)); - coloredTexts.put(resDebugSource, asRed(resDebugSource)); + coloredTexts.put(resAssert, ConsoleUtil.asRed(resAssert)); + coloredTexts.put(resDebugSource, ConsoleUtil.asRed(resDebugSource)); String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert); String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource); if (firstBadDialog == null) { @@ -283,15 +284,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { String badStats = String.format("%d bad", totalBad); String unknownStats = String.format("%d unknown", totalUnknown); coloredTexts.clear(); - coloredTexts.put(goodStats, String.format("%s good", asGreen(String.valueOf(totalGood)))); - coloredTexts.put(badStats, String.format("%s bad", asRed(String.valueOf(totalBad)))); - coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown)))); + coloredTexts.put(goodStats, String.format("%s good", ConsoleUtil.asGreen(String.valueOf(totalGood)))); + coloredTexts.put(badStats, String.format("%s bad", ConsoleUtil.asRed(String.valueOf(totalBad)))); + coloredTexts.put(unknownStats, String.format("%s unknown", ConsoleUtil.asYellow(String.valueOf(totalUnknown)))); System.out.print(getColoredRow(totalsLeftFormat, coloredTexts, String.format("Total results: %s, %s, %s", goodStats, badStats, unknownStats))); // first error for fast access in big list if (totalDialogs > 1 && firstBadDialog != null) { System.out.println(horizontalBorder); - System.out.print(getColoredRow(totalsRightFormat, coloredTexts, "First bad dialog: " + firstBadDialog.getRegNumber())); + System.out.print(getColoredRow(totalsRightFormat, coloredTexts, "First bad dialog: " + firstBadDialog.getRegNumber() + " (debug it by test_RunSingle_Debugging)")); System.out.print(getColoredRow(totalsRightFormat, coloredTexts, firstBadDialog.getName() + " - " + firstBadDialog.getDescription())); System.out.print(firstBadAssert); System.out.print(firstBadDebugSource); @@ -313,16 +314,4 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps { } return line; } - - private String asRed(String text) { - return "\u001B[31m" + text + "\u001B[0m"; - } - - private String asGreen(String text) { - return "\u001B[32m" + text + "\u001B[0m"; - } - - private String asYellow(String text) { - return "\u001B[33m" + text + "\u001B[0m"; - } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ca3a223ea3b..7d322a5f17f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -16,6 +16,7 @@ import mage.cards.Cards; import mage.cards.CardsImpl; import mage.cards.decks.Deck; import mage.choices.Choice; +import mage.collectors.DataCollectorServices; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; @@ -41,6 +42,7 @@ import mage.game.stack.StackAbility; import mage.game.stack.StackObject; import mage.game.tournament.Tournament; import mage.player.ai.ComputerPlayer; +import mage.player.ai.PossibleTargetsSelector; import mage.players.*; import mage.players.net.UserData; import mage.target.*; @@ -51,7 +53,6 @@ import mage.util.RandomUtil; import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; import org.apache.log4j.Logger; import org.junit.Assert; -import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; import java.io.Serializable; import java.util.*; @@ -59,6 +60,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; + /** * Basic implementation of testable player * @@ -72,6 +75,7 @@ public class TestPlayer implements Player { public static final String TARGET_SKIP = "[target_skip]"; // stop/skip targeting public static final String CHOICE_SKIP = "[choice_skip]"; // stop/skip choice + public static final String MODE_SKIP = "[mode_skip]"; // stop/skip mode selection public static final String CHOICE_NORMAL_COST = "Cast with no alternative cost: "; // when there is the possibility for an alternative cost, use the normal cost instead. public static final String MANA_CANCEL = "[mana_cancel]"; // cancel payment public static final String SKIP_FAILED_COMMAND = "[skip_failed_command]"; // skip next command in player's queue (can remove cast commands after try to activate) @@ -101,7 +105,10 @@ public class TestPlayer implements Player { private final List choices = new ArrayList<>(); // choices stack for choice private final List targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command) private final Map aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use) - private final List modesSet = new ArrayList<>(); + private final List modes = new ArrayList<>(); + + // debug only: ignore all next commands so choose dialog will raise error with full info + private boolean isSkipAllNextChooseCommands = false; private final ComputerPlayer computerPlayer; // real player @@ -144,11 +151,12 @@ public class TestPlayer implements Player { this.choices.addAll(testPlayer.choices); this.targets.addAll(testPlayer.targets); this.aliases.putAll(testPlayer.aliases); - this.modesSet.addAll(testPlayer.modesSet); + this.modes.addAll(testPlayer.modes); this.computerPlayer = testPlayer.computerPlayer.copy(); if (testPlayer.groupsForTargetHandling != null) { this.groupsForTargetHandling = testPlayer.groupsForTargetHandling.clone(); } + this.isSkipAllNextChooseCommands = testPlayer.isSkipAllNextChooseCommands; this.strictChooseMode = testPlayer.strictChooseMode; } @@ -160,6 +168,10 @@ public class TestPlayer implements Player { Assert.assertNotEquals("Choice can't be empty", "", choice); choice = EmptyNames.replaceTestCommandByObjectName(choice); + if (this.isSkipAllNextChooseCommands) { + return; + } + choices.add(choice); } @@ -184,7 +196,10 @@ public class TestPlayer implements Player { } public void addModeChoice(String mode) { - modesSet.add(mode); + if (this.isSkipAllNextChooseCommands) { + return; + } + modes.add(mode); } public void addTarget(String target) { @@ -194,6 +209,10 @@ public class TestPlayer implements Player { target = EmptyNames.replaceTestCommandByObjectName(target); + if (this.isSkipAllNextChooseCommands) { + return; + } + targets.add(target); } @@ -366,6 +385,10 @@ public class TestPlayer implements Player { return true; // targetAmount have to be set by setTargetAmount in the test script } ability.getTargets().get(0).addTarget(player.getId(), ability, game); + + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, player, target, reason); + targetsSet++; break; } @@ -464,9 +487,12 @@ public class TestPlayer implements Player { int index = 0; int targetsSet = 0; for (String targetName : targetList) { + + // choose target for specific mode Mode selectedMode; if (targetName.startsWith("mode=")) { - int modeNr = Integer.parseInt(targetName.substring(5, 6)); + String modeStr = targetName.substring(5, 6); + int modeNr = Integer.parseInt(modeStr); if (modeNr == 0 || modeNr > (ability.getModes().isMayChooseSameModeMoreThanOnce() ? ability.getModes().getSelectedModes().size() : ability.getModes().size())) { throw new UnsupportedOperationException("Given mode number (" + modeNr + ") not available for " + ability.toString()); } @@ -489,18 +515,23 @@ public class TestPlayer implements Player { if (index >= selectedMode.getTargets().size()) { break; // this can happen if targets should be set but can't be used because of hexproof e.g. } + + // choose player Target currentTarget = selectedMode.getTargets().get(index); if (targetName.startsWith("targetPlayer=")) { target = targetName.substring(targetName.indexOf("targetPlayer=") + 13); for (Player player : game.getPlayers().values()) { if (player.getName().equals(target)) { currentTarget.addTarget(player.getId(), ability, game); + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, this, target, reason); index++; targetsSet++; break; } } } else { + // choose object boolean originOnly = false; boolean copyOnly = false; if (targetName.endsWith("]")) { @@ -551,6 +582,10 @@ public class TestPlayer implements Player { currentTarget.addTarget(id, ability, game); targetsSet++; } + + String reason = CardUtil.getSourceLogName(game, "by cast/activate, ability: ", ability, "", ""); + DataCollectorServices.getInstance().onTestsTargetUse(game, this, targetName, reason); + if (currentTarget.getTargets().size() == currentTarget.getMaxNumberOfTargets()) { index++; } @@ -628,7 +663,7 @@ public class TestPlayer implements Player { // skip failed command if (!choices.isEmpty() && choices.get(0).equals(SKIP_FAILED_COMMAND)) { actions.remove(action); - choices.remove(0); + choicesRemoveCurrent(game, "on priority"); return true; } } @@ -1267,7 +1302,7 @@ public class TestPlayer implements Player { .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") + c.getIdName() + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") - + " class " + c.getMainCard().getClass().getSimpleName() + "" + + " class " + getSimpleClassName(c.getMainCard().getClass()) + "" + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + (c.isPlaneswalker(game) ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + ", " + (c.isTapped() ? "Tapped" : "Untapped") @@ -1307,7 +1342,7 @@ public class TestPlayer implements Player { + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified + (a.toString().length() > 0 ? CardUtil.substring(a.toString(), 40, "...") - : a.getClass().getSimpleName()) + : getSimpleClassName(a.getClass())) )) .sorted() .collect(Collectors.toList()); @@ -2091,13 +2126,13 @@ public class TestPlayer implements Player { } private String getInfo(MageObject o) { - return "Object: " + (o != null ? o.getClass().getSimpleName() + ": " + o.getName() : "null"); + return "Object: " + (o != null ? getSimpleClassName(o.getClass()) + ": " + o.getName() : "null"); } private String getInfo(Ability o, Game game) { if (o != null) { MageObject object = o.getSourceObject(game); - return "Ability: " + (object == null ? "" : object.getIdName() + " - " + o.getClass().getSimpleName() + ": " + o.getRule()); + return "Ability: " + (object == null ? "" : object.getIdName() + " - " + getSimpleClassName(o.getClass()) + ": " + o.getRule()); } return "Ability: null"; } @@ -2109,8 +2144,8 @@ public class TestPlayer implements Player { UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); Set possibleTargets = target.possibleTargets(abilityControllerId, source, game, cards); - return "Target: selected " + target.getSize() + ", possible " + possibleTargets.size() - + ", " + target.getClass().getSimpleName() + ": " + target.getMessage(game); + return "Target: selected " + target.getSize() + ", more possible " + possibleTargets.size() + + ", " + getSimpleClassName(target.getClass()) + ": " + target.getMessage(game); } private void assertAliasSupportInChoices(boolean methodSupportAliases) { @@ -2166,6 +2201,13 @@ public class TestPlayer implements Player { }); printEnd(); } + if (choiceType.equals("mode")) { + printStart(game, "Unused modes"); + if (!modes.isEmpty()) { + System.out.println(String.join("\n", modes)); + } + printEnd(); + } Assert.fail("Missing " + choiceType.toUpperCase(Locale.ENGLISH) + " def for" + " turn " + game.getTurnNum() + ", step " + (game.getStep() != null ? game.getTurnStepType().name() : "not started") @@ -2176,23 +2218,8 @@ public class TestPlayer implements Player { @Override public Mode chooseMode(Modes modes, Ability source, Game game) { - if (!modesSet.isEmpty() && modes.getMaxModes(game, source) > modes.getSelectedModes().size()) { - // set mode to null to select less than maximum modes if multiple modes are allowed - if (modesSet.get(0) == null) { - modesSet.remove(0); - return null; - } - int needMode = Integer.parseInt(modesSet.get(0)); - int i = 1; - for (Mode mode : modes.getAvailableModes(source, game)) { - if (i == needMode) { - modesSet.remove(0); - return mode; - } - i++; - } - } - if (modes.getMinModes() <= modes.getSelectedModes().size()) { + if (modes.getSelectedModes().size() >= modes.getMaxModes(game, source)) { + // TODO: no needs here cause min/max mode must be checked by parent code? try to remove it from here return null; } @@ -2207,6 +2234,27 @@ public class TestPlayer implements Player { i++; } + if (!this.modes.isEmpty()) { + + // skip modes + if (tryToSkipSelection(game, null, this.modes, MODE_SKIP)) { + return null; + } + + String needModeStr = this.modes.get(0); + int needModeNumber = Integer.parseInt(needModeStr); + i = 1; + for (Mode mode : modes.getAvailableModes(source, game)) { + if (i == needModeNumber) { + modesRemoveCurrent(game, mode); + return mode; + } + i++; + } + + Assert.fail("Can't use mode: " + needModeNumber + "\n" + getInfo(source, game) + modesInfo); + } + this.chooseStrictModeFailed("mode", game, getInfo(source, game) + modesInfo); return computerPlayer.chooseMode(modes, source, game); } @@ -2219,9 +2267,8 @@ public class TestPlayer implements Player { String possibleChoice = choices.get(0); // skip choices - if (possibleChoice.equals(CHOICE_SKIP)) { - choices.remove(0); - return false; // false - stop to choose + if (tryToSkipSelection(game, null, choices, CHOICE_SKIP)) { + return false; } if (choice.setChoiceByAnswers(choices, true)) { @@ -2254,7 +2301,7 @@ public class TestPlayer implements Player { int index = 0; for (Map.Entry entry : effectsMap.entrySet()) { if (entry.getValue().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose replacements"); // TODO: add short lists? return index; } index++; @@ -2269,219 +2316,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { - - // choose itself for starting player all the time - if (target.getMessage(game).equals("Select a starting player")) { - target.add(this.getId(), game); - return true; - } - - UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); - - // TODO: warning, some cards call player.choose methods instead target.choose, see #8254 - // most use cases - discard and other cost with choice like that method - // must migrate all choices.remove(xxx) to choices.remove(0), takeMaxTargetsPerChoose can help to find it - - boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish - - assertAliasSupportInChoices(true); - if (!choices.isEmpty()) { - - // skip choices - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - Assert.assertTrue("found skip choice, but it require more choices, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", - target.getTargets().size() >= target.getMinNumberOfTargets()); - return false; // false - stop to choose - } - - // TODO: Allow to choose a player with TargetPermanentOrPlayer - if ((target.getOriginalTarget() instanceof TargetPermanent) - || (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet - FilterPermanent filterPermanent; - if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) { - filterPermanent = ((TargetPermanentOrPlayer) target.getOriginalTarget()).getFilterPermanent(); - } else { - filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter(); - } - while (!choices.isEmpty()) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String choiceRecord = choices.get(0); - String[] targetList = choiceRecord.split("\\^"); - isAddedSomething = false; - for (String targetName : targetList) { - boolean originOnly = false; - boolean copyOnly = false; - if (targetName.endsWith("]")) { - if (targetName.endsWith("[no copy]")) { - originOnly = true; - targetName = targetName.substring(0, targetName.length() - 9); - } - if (targetName.endsWith("[only copy]")) { - copyOnly = true; - targetName = targetName.substring(0, targetName.length() - 11); - } - } - for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, abilityControllerId, source, game)) { - if (target.contains(permanent.getId())) { - continue; - } - if (hasObjectTargetNameOrAlias(permanent, targetName)) { - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { - if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { - target.add(permanent.getId(), game); - isAddedSomething = true; - break; - } - } - } else if ((permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code? - if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) { - if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) { - target.add(permanent.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - } - - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // choices - } - - if (target instanceof TargetPlayer) { - while (!choices.isEmpty()) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String choiceRecord = choices.get(0); - isAddedSomething = false; - for (Player player : game.getPlayers().values()) { - if (player.getName().equals(choiceRecord)) { - if (target.canTarget(abilityControllerId, player.getId(), source, game) && !target.contains(player.getId())) { - target.add(player.getId(), game); - isAddedSomething = true; - } - } - } - - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // while choices - } - - // TODO: add same choices fixes for other target types (one choice must uses only one time for one target) - if (target.getOriginalTarget() instanceof TargetCard) { - // one choice per target - // only unique targets - //TargetCard targetFull = ((TargetCard) target); - - for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - isAddedSomething = false; - String[] possibleChoices = choiceRecord.split("\\^"); - - for (String possibleChoice : possibleChoices) { - Set possibleCards = target.possibleTargets(abilityControllerId, source, game); - for (UUID targetId : possibleCards) { - MageObject targetObject = game.getCard(targetId); - if (hasObjectTargetNameOrAlias(targetObject, possibleChoice)) { - if (target.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { - target.add(targetObject.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(0); - } - return isAddedSomething; - } // for choices - } - - if (target.getOriginalTarget() instanceof TargetSource) { - TargetSource t = ((TargetSource) target.getOriginalTarget()); - Set possibleTargets = t.possibleTargets(abilityControllerId, source, game); - // TODO: enable choices.get first instead all - for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - return false; // stop dialog - } - String[] targetList = choiceRecord.split("\\^"); - isAddedSomething = false; - for (String targetName : targetList) { - for (UUID targetId : possibleTargets) { - MageObject targetObject = game.getObject(targetId); - if (targetObject != null) { - if (hasObjectTargetNameOrAlias(targetObject, targetName)) { - if (t.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) { - target.add(targetObject.getId(), game); - isAddedSomething = true; - break; - } - } - } - } - } - try { - if (isAddedSomething) { - if (target.isChoiceCompleted(abilityControllerId, source, game)) { - return true; - } - } else { - failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); - } - } finally { - choices.remove(choiceRecord); - } - return isAddedSomething; - } // for choices - } - - // TODO: enable fail checks and fix tests - if (!target.getTargetName().equals("starting player")) { - assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list"); - } - } - - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, null)); - return computerPlayer.choose(outcome, target, source, game, options); + return makeChoose(outcome, target, source, game, null); } private void checkTargetDefinitionMarksSupport(Target needTarget, String targetDefinition, String canSupportChars) { @@ -2502,7 +2337,7 @@ public class TestPlayer implements Player { // how to fix: change target definition for addTarget in test's code or update choose from targets implementation in TestPlayer if ((foundMulti && !canMulti) || (foundSpecialStart && !canSpecialStart) || (foundSpecialClose && !canSpecialClose) || (foundEquals && !canEquals)) { Assert.fail(this.getName() + " - Targets list was setup by addTarget with " + targets + ", but target definition [" + targetDefinition + "]" - + " is not supported by [" + canSupportChars + "] for target class " + needTarget.getClass().getSimpleName()); + + " is not supported by [" + canSupportChars + "] for target class " + getSimpleClassName(needTarget.getClass())); } } @@ -2514,12 +2349,11 @@ public class TestPlayer implements Player { if (!targets.isEmpty()) { // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } Set targetCardZonesChecked = new HashSet<>(); // control miss implementation @@ -2538,7 +2372,7 @@ public class TestPlayer implements Player { && target.canTarget(abilityControllerId, player.getId(), source, game) && !target.contains(player.getId())) { target.addTarget(player.getId(), source, game); - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - player"); return true; } } @@ -2590,7 +2424,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - permanent"); return true; } } @@ -2618,7 +2452,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - hand"); return true; } } @@ -2637,7 +2471,7 @@ public class TestPlayer implements Player { } if (filter == null) { Assert.fail("Unsupported exile target filter in TestPlayer: " - + target.getOriginalTarget().getClass().getCanonicalName()); + + getSimpleClassName(target.getOriginalTarget().getClass())); } for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) { @@ -2656,7 +2490,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - exile"); return true; } } @@ -2681,7 +2515,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - card in battlefield"); return true; } } @@ -2735,7 +2569,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - graveyard"); return true; } } @@ -2762,7 +2596,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target - stack"); return true; } } @@ -2772,13 +2606,13 @@ public class TestPlayer implements Player { if (target.getOriginalTarget() instanceof TargetCardInLibrary || (target.getOriginalTarget() instanceof TargetCard && target.getOriginalTarget().getZone() == Zone.LIBRARY)) { // user don't have access to library, so it must be targeted through list/revealed cards - Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + target.getOriginalTarget().getClass().getCanonicalName()); + Assert.fail("Library zone is private, you must target through cards list, e.g. revealed: " + getSimpleClassName(target.getOriginalTarget().getClass())); } // uninplemented TargetCard's zone if (target.getOriginalTarget() instanceof TargetCard && !targetCardZonesChecked.contains(target.getOriginalTarget().getZone())) { Assert.fail("Found unimplemented TargetCard's zone or TargetCard's extented class: " - + target.getOriginalTarget().getClass().getCanonicalName() + + getSimpleClassName(target.getOriginalTarget().getClass()) + ", zone " + target.getOriginalTarget().getZone() + ", from " + (source == null ? "unknown source" : source.getSourceObject(game))); } @@ -2793,14 +2627,14 @@ public class TestPlayer implements Player { if (source != null) { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: " + source.getSourceObject(game) - + "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")" - + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nAbility: " + getSimpleClassName(source.getClass()) + " (" + source.getRule() + ")" + + "\nTarget: selected " + target.getSize() + ", more possible " + possibleTargets.size() + ", " + getSimpleClassName(target.getClass()) + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } else { message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used" + "\nCard: unknown source" + "\nAbility: unknown source" - + "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")" + + "\nTarget: selected " + target.getSize() + ", more possible " + possibleTargets.size() + ", " + getSimpleClassName(target.getClass()) + " (" + target.getMessage(game) + ")" + "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets"; } Assert.fail(message); @@ -2816,13 +2650,13 @@ public class TestPlayer implements Player { assertAliasSupportInTargets(false); if (!targets.isEmpty()) { + // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) { String[] targetList = targetDefinition.split("\\^"); @@ -2839,7 +2673,7 @@ public class TestPlayer implements Player { } } if (targetFound) { - targets.remove(targetDefinition); + targetsRemoveCurrent(targetDefinition, game, "on choose target from cards"); return true; } } @@ -2861,7 +2695,7 @@ public class TestPlayer implements Player { for (TriggeredAbility ability : abilities) { if (ability.toString().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose triggers"); // TODO: add short lists? return ability; } } @@ -2890,11 +2724,11 @@ public class TestPlayer implements Player { String choice = choices.get(0); if (choice.equals("No")) { - choices.remove(0); + choicesRemoveCurrent(game, source); return false; } if (choice.equals("Yes")) { - choices.remove(0); + choicesRemoveCurrent(game, source); return true; } @@ -2921,7 +2755,7 @@ public class TestPlayer implements Player { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); assertXMinMaxValue(game, source, xValue, min, max); - choices.remove(0); + choicesRemoveCurrent(game, source); return xValue; } } @@ -2960,7 +2794,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); - choices.remove(0); + choicesRemoveCurrent(game, source); return xValue; } } @@ -2986,17 +2820,17 @@ public class TestPlayer implements Player { // must fill all possible choices or skip it for (int i = 0; i < messages.size(); i++) { if (!choices.isEmpty()) { + // skip + if (tryToSkipSelection(game, null, choices, CHOICE_SKIP)) { + break; + } + // normal choice if (choices.get(0).startsWith("X=")) { answer.set(i, Integer.parseInt(choices.get(0).substring(2))); - choices.remove(0); + choicesRemoveCurrent(game, "on multi amount"); // TODO: add info? continue; } - // skip - if (choices.get(0).equals(CHOICE_SKIP)) { - choices.remove(0); - break; - } } Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i, messages)); } @@ -3054,7 +2888,7 @@ public class TestPlayer implements Player { @Override public void restore(Player player) { if (!(player instanceof TestPlayer)) { - throw new IllegalArgumentException("Wrong code usage: can't restore from player class " + player.getClass().getName()); + throw new IllegalArgumentException("Wrong code usage: can't restore from player class " + getSimpleClassName(player.getClass())); } // no rollback for test player metadata (modesSet, actions, choices, targets, aliases, etc) @@ -3799,10 +3633,10 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { String nextResult = choices.get(0); if (nextResult.equals(FLIPCOIN_RESULT_TRUE)) { - choices.remove(0); + choicesRemoveCurrent(game, "on flip coin"); return true; } else if (nextResult.equals(FLIPCOIN_RESULT_FALSE)) { - choices.remove(0); + choicesRemoveCurrent(game, "on flip coin"); return false; } } @@ -3823,7 +3657,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { String nextResult = choices.get(0); if (nextResult.startsWith(DIE_ROLL)) { - choices.remove(0); + choicesRemoveCurrent(game, "on roll die"); return Integer.parseInt(nextResult.substring(DIE_ROLL.length())); } } @@ -4266,62 +4100,143 @@ public class TestPlayer implements Player { return choose(outcome, target, source, game, null); } - private boolean tryToSkipSelection(List selections, String selectionMark) { - if (!selections.isEmpty() && selections.get(0).equals(selectionMark)) { + public void skipAllNextChooseCommands() { + this.isSkipAllNextChooseCommands = true; + } + + public boolean isSkipAllNextChooseCommands() { + return this.isSkipAllNextChooseCommands; + } + + private boolean tryToSkipSelection(Game game, Target target, List selections, String skipCommand) { + if (!selections.isEmpty() && selections.get(0).equals(skipCommand)) { + + // mark target as skip, so choose dialog can be stopped on partly selection + if (target != null) { + if (target.getMaxNumberOfTargets() > target.getMinNumberOfTargets()) { + target.setSkipChoice(true); + } else { + Assert.fail("Wrong skip command found - it can be used with up to targets, but used in " + target); + } + } + selections.remove(0); + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, skipCommand, "by skip command"); + return true; } return false; } - @Override - public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { - assertAliasSupportInChoices(false); - if (!choices.isEmpty()) { + private boolean makeChoose(Outcome outcome, Target target, Ability source, Game game, Cards fromCards) { + assertAliasSupportInChoices(true); + // choose itself for starting player all the time + if (target.getMessage(game).equals("Select a starting player")) { + target.add(this.getId(), game); + return true; + } + + + UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId()); + + PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game); + possibleTargetsSelector.findNewTargets(fromCards); + + // nothing to choose, e.g. no valid targets + if (!possibleTargetsSelector.hasAnyTargets()) { + return false; + } + + // can't choose + if (!possibleTargetsSelector.hasMinNumberOfTargets()) { + return false; + } + + while (!choices.isEmpty()) { // skip choices - if (tryToSkipSelection(choices, CHOICE_SKIP)) { - if (cards.isEmpty()) { - // cancel button forced in GUI on no possible choices - // TODO: need research - return false; - } else { - Assert.assertTrue("found skip choice, but it require more choices, needs " - + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", - target.getTargets().size() >= target.getMinNumberOfTargets()); - return false; // stop dialog - } + if (tryToSkipSelection(game, target, choices, CHOICE_SKIP)) { + Assert.assertTrue("found skip choice, but it require more choices, needs " + + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + target.getTargets().size() >= target.getMinNumberOfTargets()); + return target.getSize() > 0 && target.isChosen(game); } - for (String choose2 : new ArrayList<>(choices)) { - // TODO: More targetting to fix - String[] targetList = choose2.split("\\^"); - boolean targetFound = false; - for (String targetName : targetList) { - for (Card card : cards.getCards(game)) { - if (target.contains(card.getId())) { - continue; - } - if (hasObjectTargetNameOrAlias(card, targetName)) { - if (target.isNotTarget() || target.canTarget(card.getId(), source, game)) { - target.add(card.getId(), game); - targetFound = true; + String choiceRecord = choices.get(0); + String[] targetList = choiceRecord.split("\\^"); + boolean isAddedSomething = false; + for (String targetName : targetList) { + boolean originOnly = false; + boolean copyOnly = false; + if (targetName.endsWith("]")) { + if (targetName.endsWith("[no copy]")) { + originOnly = true; + targetName = targetName.substring(0, targetName.length() - 9); + } + if (targetName.endsWith("[only copy]")) { + copyOnly = true; + targetName = targetName.substring(0, targetName.length() - 11); + } + } + + for (MageItem item : possibleTargetsSelector.getAny()) { + if (target.contains(item.getId())) { + continue; + } + if (item instanceof MageObject) { + MageObject object = (MageObject) item; + if (hasObjectTargetNameOrAlias(object, targetName)) { + if ((object.isCopy() && !originOnly) || (!object.isCopy() && !copyOnly)) { + target.add(object.getId(), game); + isAddedSomething = true; + break; + } + } else if ((object.getName() + '-' + object.getExpansionSetCode()).equals(targetName)) { // TODO: remove search by exp code? + if ((object.isCopy() && !originOnly) || (!object.isCopy() && !copyOnly)) { + target.add(object.getId(), game); + isAddedSomething = true; break; } } + } else if (item instanceof Player) { + Player player = (Player) item; + if (player.getName().equals(choiceRecord)) { + target.add(player.getId(), game); + isAddedSomething = true; + break; + } + } else { + throw new IllegalArgumentException("Unknown possible target type: " + getSimpleClassName(item.getClass())); } } - if (targetFound) { - choices.remove(choose2); - return true; - } } - assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list"); + try { + if (isAddedSomething) { + if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) { + return true; + } + // must refresh possible targets after each "add" cause there are dynamic targets like TargetCardAndOrCard from Sludge Titan + possibleTargetsSelector.findNewTargets(fromCards); + } else { + failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command"); + } + } finally { + choicesRemoveCurrent(game, source); + } } - this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, cards)); - return computerPlayer.choose(outcome, cards, target, source, game); + this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game, fromCards)); + if (fromCards != null) { + return computerPlayer.choose(outcome, fromCards, (TargetCard) target, source, game); + } else { + return computerPlayer.choose(outcome, target, source, game); + } + } + + @Override + public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { + return makeChoose(outcome, target, source, game, cards); } @Override @@ -4347,12 +4262,11 @@ public class TestPlayer implements Player { while (!targets.isEmpty()) { // skip targets - if (targets.get(0).equals(TARGET_SKIP)) { + if (tryToSkipSelection(game, target, targets, TARGET_SKIP)) { Assert.assertTrue("found skip target, but it require more targets, needs " + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", target.getTargets().size() >= target.getMinNumberOfTargets()); - targets.remove(0); - return false; // false - stop to choose + return target.getSize() > 0 && target.isChosen(game); } // only target amount needs @@ -4376,7 +4290,7 @@ public class TestPlayer implements Player { Assert.assertTrue("target amount must be <= remaining = " + target.getAmountRemaining() + " " + targetInfo, targetAmount <= target.getAmountRemaining()); if (target.getAmountRemaining() > 0) { - for (UUID possibleTarget : target.possibleTargets(source.getControllerId(), source, game)) { + for (UUID possibleTarget : target.possibleTargets(abilityControllerId, source, game)) { boolean foundTarget = false; // permanent @@ -4392,10 +4306,10 @@ public class TestPlayer implements Player { } if (foundTarget) { - if (!target.contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) { + if (!target.contains(possibleTarget) && target.canTarget(abilityControllerId, possibleTarget, source, game)) { // can select target.addTarget(possibleTarget, targetAmount, source, game); - targets.remove(0); + targetsRemoveCurrent(null, game, "on choose target amount"); // allow test player to choose as much as possible until skip command if (target.getAmountRemaining() <= 0) { return true; @@ -4437,7 +4351,7 @@ public class TestPlayer implements Player { // simulate cancel on mana payment (e.g. user press on cancel button) if (choices.get(0).equals(MANA_CANCEL)) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? return false; } @@ -4487,7 +4401,7 @@ public class TestPlayer implements Player { for (SpecialAction specialAction : specialActions.values()) { if (specialAction.getRule(true).startsWith(choice)) { if (specialAction.canActivate(this.getId(), game).canActivate()) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? choiceRemoved = true; if (activateAbility(specialAction, game)) { choiceUsed = true; @@ -4501,7 +4415,7 @@ public class TestPlayer implements Player { if (choiceUsed) { if (!choiceRemoved) { - choices.remove(0); + choicesRemoveCurrent(game, "on play mana"); // TODO: add info? } return true; } else { @@ -4682,7 +4596,7 @@ public class TestPlayer implements Player { String choice = choices.get(0); for (SpellAbility ability : useable.values()) { if (ability.toString().startsWith(choice)) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose ability for cast - " + card.getName()); return ability; } } @@ -4714,7 +4628,7 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { for (ActivatedAbility ability : useable.values()) { if (ability.toString().startsWith(choices.get(0))) { - choices.remove(0); + choicesRemoveCurrent(game, "on choose land or spell ability - " + card.getName()); return ability; } } @@ -4762,13 +4676,52 @@ public class TestPlayer implements Player { } private void assertWrongChoiceUsage(String choice) { - // TODO: enable fail checks and fix tests, it's a part of setStrictChooseMode's implementation to all tests - //Assert.fail("Wrong choice command: " + choice); - LOGGER.warn("Wrong choice command: " + choice); + // TODO: enable and fix all failed tests + assertWrongChoiceUsage(choice, false); + } + + private void assertWrongChoiceUsage(String choice, boolean isRaiseError) { + if (isRaiseError) { + Assert.fail("Wrong choice command: " + choice); + } else { + LOGGER.warn("Wrong choice command: " + choice); + } } @Override public Player getRealPlayer() { return this.computerPlayer; } + + private void choicesRemoveCurrent(Game game, Ability source) { + choicesRemoveCurrent(game, CardUtil.getSourceLogName(game, "ability: ", source, "", "")); + } + + private void choicesRemoveCurrent(Game game, String reason) { + String realReason = reason.isEmpty() ? "SBA" : reason; + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, choices.get(0), "by setChoice, " + realReason); + choices.remove(0); + } + + // TODO: implement after takeMaxTargetsPerChoose fix/remove (targets must use while logic all the time - same as choices) + // TODO: make same logs for AI choices + private void targetsRemoveCurrent(String targetToRemove, Game game, String reason) { + String realReason = reason.isEmpty() ? "SBA" : reason; + DataCollectorServices.getInstance().onTestsTargetUse(game, this, targets.get(0), "by addTarget, " + realReason); + if (targetToRemove == null) { + targets.remove(0); + } else { + targets.remove(targetToRemove); // TODO: must be targets.remove(0), see todos above + } + } + + private void modesRemoveCurrent(Game game, Mode mode) { + DataCollectorServices.getInstance().onTestsChoiceUse(game, this, modes.get(0), "by setMode, mode: " + mode.getEffects().getText(mode)); + modes.remove(0); + } + + private String getSimpleClassName(Class clazz) { + // anon classes don't have names, so return name instead simple + return clazz.getSimpleName().isEmpty() ? clazz.getName() : clazz.getSimpleName(); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 436daf44d55..be58f93c598 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1706,6 +1706,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement private void assertAllCommandsUsed() throws AssertionError { for (Player player : currentGame.getPlayers().values()) { TestPlayer testPlayer = (TestPlayer) player; + + if (testPlayer.isSkipAllNextChooseCommands()) { + Assert.fail(testPlayer.getName() + " used skip next choose commands, but game do not call any choose dialog after it. Skip must be removed after debug."); + } + assertActionsMustBeEmpty(testPlayer); assertChoicesCount(testPlayer, 0); assertTargetsCount(testPlayer, 0); @@ -2008,7 +2013,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * @param step * @param player * @param cardName - * @param targetName for modes you can add "mode=3" before target name; + * @param targetName for non default mode you can add target by "mode=3target_name" style; * multiple targets can be separated by ^; * no target marks as TestPlayer.NO_TARGET; * warning, do not support cards with target adjusters - use addTarget instead @@ -2315,6 +2320,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * spell mode can be used only once like Demonic Pact, the * value has to be set to the number of the remaining modes * (e.g. if only 2 are left the number need to be 1 or 2). + * If you need to partly select then use TestPlayer.MODE_SKIP */ public void setModeChoice(TestPlayer player, String choice) { player.addModeChoice(choice); @@ -2439,6 +2445,26 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement gameOptions.skipInitShuffling = true; } + /** + * Debug only: skip all choose commands after that command. + *

+ * Alternative to comment/uncomment all test commands: + * - insert skip before first choice command; + * - run test and look at error message about miss choice; + * - make sure test use correct choice; + * - move skip command to next test's choice and repeat; + */ + protected void skipAllNextChooseCommands() { + playerA.skipAllNextChooseCommands(); + playerB.skipAllNextChooseCommands(); + if (playerC != null) { + playerC.skipAllNextChooseCommands(); + } + if (playerD != null) { + playerD.skipAllNextChooseCommands(); + } + } + public void assertDamageReceived(Player player, String cardName, int expected) { Permanent p = getPermanent(cardName, player); if (p != null) { diff --git a/Mage/src/main/java/mage/abilities/Mode.java b/Mage/src/main/java/mage/abilities/Mode.java index 0cc7e2b1add..20b9bb1c2e5 100644 --- a/Mage/src/main/java/mage/abilities/Mode.java +++ b/Mage/src/main/java/mage/abilities/Mode.java @@ -130,4 +130,9 @@ public class Mode implements Serializable { public int getPawPrintValue() { return pawPrintValue; } + + @Override + public String toString() { + return String.format("%s", this.getEffects().getText(this)); + } } diff --git a/Mage/src/main/java/mage/collectors/DataCollector.java b/Mage/src/main/java/mage/collectors/DataCollector.java index db3471a4acd..399b304b815 100644 --- a/Mage/src/main/java/mage/collectors/DataCollector.java +++ b/Mage/src/main/java/mage/collectors/DataCollector.java @@ -2,6 +2,7 @@ package mage.collectors; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import java.util.UUID; @@ -11,10 +12,12 @@ import java.util.UUID; * Supported features: * - [x] collect and print game logs in server output, including unit tests * - [x] collect and save full games history and decks - * - [ ] collect and print performance metrics like ApplyEffects calc time or inform players time (pings) - * - [ ] collect and send metrics to third party tools like prometheus + grafana - * - [ ] prepare "attachable" game data for bug reports - * - [ ] record game replays data (GameView history) + * - [ ] TODO: collect and print performance metrics like ApplyEffects calc time or inform players time (pings) + * - [ ] TODO: collect and send metrics to third party tools like prometheus + grafana + * - [x] tests: print used selections (choices, targets, modes, skips) TODO: add yes/no, replacement effect, coins, other choices + * - [ ] TODO: tests: print additional info like current resolve ability? + * - [ ] TODO: prepare "attachable" game data for bug reports + * - [ ] TODO: record game replays data (GameView history) *

* How-to enable or disable: * - use java params like -Dxmage.dataCollectors.saveGameHistory=true @@ -62,7 +65,27 @@ public interface DataCollector { void onChatTable(UUID tableId, String userName, String message); /** - * @param gameId chat sessings don't have full game access, so use onGameStart event to find game's ID before chat + * @param gameId chat session don't have full game access, so use onGameStart event to find game's ID before chat */ void onChatGame(UUID gameId, String userName, String message); + + /** + * Tests only: on any non-target choice like yes/no, mode, etc + */ + void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason); + + /** + * Tests only: on any target choice + */ + void onTestsTargetUse(Game game, Player player, String usingTarget, String reason); + + /** + * Tests only: on push object to stack (calls before activate and make any choice/announce) + */ + void onTestsStackPush(Game game); + + /** + * Tests only: on stack object resolve (calls before starting resolve) + */ + void onTestsStackResolve(Game game); } diff --git a/Mage/src/main/java/mage/collectors/DataCollectorServices.java b/Mage/src/main/java/mage/collectors/DataCollectorServices.java index dc2740f078a..52a18713f37 100644 --- a/Mage/src/main/java/mage/collectors/DataCollectorServices.java +++ b/Mage/src/main/java/mage/collectors/DataCollectorServices.java @@ -4,6 +4,7 @@ import mage.collectors.services.PrintGameLogsDataCollector; import mage.collectors.services.SaveGameHistoryDataCollector; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import org.apache.log4j.Logger; import java.util.LinkedHashSet; @@ -140,4 +141,28 @@ final public class DataCollectorServices implements DataCollector { public void onChatGame(UUID gameId, String userName, String message) { activeServices.forEach(c -> c.onChatGame(gameId, userName, message)); } + + @Override + public void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsChoiceUse(game, player, usingChoice, reason)); + } + + @Override + public void onTestsTargetUse(Game game, Player player, String usingTarget, String reason) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsTargetUse(game, player, usingTarget, reason)); + } + + @Override + public void onTestsStackPush(Game game) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsStackPush(game)); + } + + @Override + public void onTestsStackResolve(Game game) { + if (game.isSimulation()) return; + activeServices.forEach(c -> c.onTestsStackResolve(game)); + } } diff --git a/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java b/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java index 2e5f1793e81..c2e59fe7806 100644 --- a/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java +++ b/Mage/src/main/java/mage/collectors/services/EmptyDataCollector.java @@ -3,6 +3,7 @@ package mage.collectors.services; import mage.collectors.DataCollector; import mage.game.Game; import mage.game.Table; +import mage.players.Player; import java.util.UUID; @@ -67,4 +68,24 @@ public abstract class EmptyDataCollector implements DataCollector { public void onChatGame(UUID gameId, String userName, String message) { // nothing } + + @Override + public void onTestsChoiceUse(Game game, Player player, String usingChoice, String reason) { + // nothing + } + + @Override + public void onTestsTargetUse(Game game, Player player, String usingTarget, String reason) { + // nothing + } + + @Override + public void onTestsStackPush(Game game) { + // nothing + } + + @Override + public void onTestsStackResolve(Game game) { + // nothing + } } diff --git a/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java b/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java index f8fc9a8966a..c6c8741505c 100644 --- a/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java +++ b/Mage/src/main/java/mage/collectors/services/PrintGameLogsDataCollector.java @@ -1,7 +1,9 @@ package mage.collectors.services; import mage.game.Game; +import mage.players.Player; import mage.util.CardUtil; +import mage.util.ConsoleUtil; import org.apache.log4j.Logger; import org.jsoup.Jsoup; @@ -40,9 +42,47 @@ public class PrintGameLogsDataCollector extends EmptyDataCollector { @Override public void onGameLog(Game game, String message) { String needMessage = Jsoup.parse(message).text(); - writeLog("GAME", "LOG", String.format("%s: %s", + writeLog("LOG", "GAME", String.format("%s: %s", CardUtil.getTurnInfo(game), needMessage )); } + + @Override + public void onTestsChoiceUse(Game game, Player player, String choice, String reason) { + String needReason = Jsoup.parse(reason).text(); + writeLog("LOG", "GAME", ConsoleUtil.asYellow(String.format("%s: %s using choice: %s%s", + CardUtil.getTurnInfo(game), + player.getName(), + choice, + reason.isEmpty() ? "" : " (" + needReason + ")" + ))); + } + + @Override + public void onTestsTargetUse(Game game, Player player, String target, String reason) { + String needReason = Jsoup.parse(reason).text(); + writeLog("LOG", "GAME", ConsoleUtil.asYellow(String.format("%s: %s using target: %s%s", + CardUtil.getTurnInfo(game), + player.getName(), + target, + reason.isEmpty() ? "" : " (" + needReason + ")" + ))); + } + + @Override + public void onTestsStackPush(Game game) { + writeLog("LOG", "GAME", String.format("%s: Stack push: %s", + CardUtil.getTurnInfo(game), + game.getStack().toString() + )); + } + + @Override + public void onTestsStackResolve(Game game) { + writeLog("LOG", "GAME", String.format("%s: Stack resolve: %s", + CardUtil.getTurnInfo(game), + game.getStack().toString() + )); + } } diff --git a/Mage/src/main/java/mage/util/ConsoleUtil.java b/Mage/src/main/java/mage/util/ConsoleUtil.java new file mode 100644 index 00000000000..cb143692785 --- /dev/null +++ b/Mage/src/main/java/mage/util/ConsoleUtil.java @@ -0,0 +1,19 @@ +package mage.util; + +/** + * Helper class to work with console logs + */ +public class ConsoleUtil { + + public static String asRed(String text) { + return "\u001B[31m" + text + "\u001B[0m"; + } + + public static String asGreen(String text) { + return "\u001B[32m" + text + "\u001B[0m"; + } + + public static String asYellow(String text) { + return "\u001B[33m" + text + "\u001B[0m"; + } +}