test framework improves:

- now game logs will show stack ability on push and on resolve (before any choices);
- now game logs will show used choices made by cast/activate, setChoice, setMode and addTarget commands (not work for AI tests, part of #13832);
- improved choice logic for modes and yes/not dialogs (now it's use a more strictly checks, use TestPlayer.MODE_SKIP to stop mode selection);
- improved error logs and testable dialogs menu in cheat mode;
This commit is contained in:
Oleg Agafonov 2025-08-04 23:32:23 +04:00
parent a7a6ffd6f3
commit e866707912
17 changed files with 553 additions and 410 deletions

View file

@ -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);
}

View file

@ -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));
}
}
}

View file

@ -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<String, Integer> 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("<br>", 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("<br>", info)
);

View file

@ -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;
}

View file

@ -105,7 +105,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
return list;
}
protected void simulateOptions(Game game) {
private void simulateOptions(Game game) {
List<ActivatedAbility> 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<Ability> 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);

View file

@ -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;
}
}

View file

@ -47,7 +47,6 @@ public class PossibleTargetsSelector {
// collect new valid targets
List<MageItem> 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<MageItem> 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();
}
}

View file

@ -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<UUID> possibleTargets = target.possibleTargets(playerId, game);
private boolean chooseRandom(Target target, Ability source, Game game) {
Set<UUID> 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<String, Serializable> 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<UUID> possibleTargets = target.possibleTargets(playerId, cards, source, game);
Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game, cards);
if (possibleTargets.isEmpty()) {
return false;
}

View file

@ -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";
}
}

File diff suppressed because it is too large Load diff

View file

@ -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.
* <p>
* 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) {

View file

@ -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));
}
}

View file

@ -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)
* <p>
* 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);
}

View file

@ -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));
}
}

View file

@ -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
}
}

View file

@ -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()
));
}
}

View file

@ -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";
}
}