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.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.TargetPlayer;
import mage.target.common.TargetPermanentOrPlayer; import mage.target.common.TargetPermanentOrPlayer;
/** /**
@ -76,6 +77,10 @@ abstract class BaseTestableDialog implements TestableDialog {
return createAnyTarget(min, max, false); 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) { private static Target createAnyTarget(int min, int max, boolean notTarget) {
return new TargetPermanentOrPlayer(min, max).withNotTarget(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 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 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)); 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.game.Game;
import mage.players.Player; import mage.players.Player;
import java.util.Collection; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -120,10 +117,8 @@ public class TestableDialogsRunner {
choice = prepareSelectDialogChoice(needGroup); choice = prepareSelectDialogChoice(needGroup);
player.choose(Outcome.Benefit, choice, game); player.choose(Outcome.Benefit, choice, game);
if (choice.getChoiceKey() != null) { if (choice.getChoiceKey() != null) {
int needIndex = Integer.parseInt(choice.getChoiceKey()); int needRegNumber = Integer.parseInt(choice.getChoiceKey());
if (needIndex < this.dialogs.size()) { needDialog = this.dialogs.getOrDefault(needRegNumber, null);
needDialog = this.dialogs.get(needIndex);
}
} }
} }
if (needDialog == null) { if (needDialog == null) {
@ -144,15 +139,20 @@ public class TestableDialogsRunner {
Choice choice = new ChoiceImpl(false); Choice choice = new ChoiceImpl(false);
choice.setMessage("Choose dialogs group to run"); 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 // main groups
int recNumber = 0;
for (int i = 0; i < groups.size(); i++) { for (int i = 0; i < groups.size(); i++) {
recNumber++;
String group = groups.get(i); String group = groups.get(i);
Integer groupMinNumber = groupNumber.getOrDefault(group, 0);
choice.withItem( choice.withItem(
String.valueOf(i), String.valueOf(i),
String.format("%02d. %s", recNumber, group), String.format("%02d. %s", groupMinNumber, group),
recNumber, groupMinNumber,
ChoiceHintType.TEXT, ChoiceHintType.TEXT,
String.join("<br>", group) String.join("<br>", group)
); );
@ -186,18 +186,15 @@ public class TestableDialogsRunner {
private Choice prepareSelectDialogChoice(String needGroup) { private Choice prepareSelectDialogChoice(String needGroup) {
Choice choice = new ChoiceImpl(false); Choice choice = new ChoiceImpl(false);
choice.setMessage("Choose game dialog to run from " + needGroup); choice.setMessage("Choose game dialog to run from " + needGroup);
int recNumber = 0; for (TestableDialog dialog : this.dialogs.values()) {
for (int i = 0; i < this.dialogs.size(); i++) {
TestableDialog dialog = this.dialogs.get(i);
if (!dialog.getGroup().equals(needGroup)) { if (!dialog.getGroup().equals(needGroup)) {
continue; continue;
} }
recNumber++;
String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription()); String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription());
choice.withItem( choice.withItem(
String.valueOf(i), String.valueOf(dialog.getRegNumber()),
String.format("%02d. %s", recNumber, info), String.format("%02d. %s", dialog.getRegNumber(), info),
recNumber, dialog.getRegNumber(),
ChoiceHintType.TEXT, ChoiceHintType.TEXT,
String.join("<br>", info) String.join("<br>", info)
); );

View file

@ -405,13 +405,13 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (effect != null if (effect != null
&& stackObject.getControllerId().equals(playerId)) { && stackObject.getControllerId().equals(playerId)) {
Target target = effect.getTarget(); 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)) { for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) {
Game sim = game.createSimulationForAI(); Game sim = game.createSimulationForAI();
StackAbility newAbility = (StackAbility) stackObject.copy(); StackAbility newAbility = (StackAbility) stackObject.copy();
SearchEffect newEffect = getSearchEffect(newAbility); SearchEffect newEffect = getSearchEffect(newAbility);
newEffect.getTarget().addTarget(targetId, newAbility, sim); newEffect.getTarget().addTarget(targetId, newAbility, sim);
sim.getStack().push(newAbility); sim.getStack().push(sim, newAbility);
SimulationNode2 newNode = new SimulationNode2(node, sim, depth, stackObject.getControllerId()); SimulationNode2 newNode = new SimulationNode2(node, sim, depth, stackObject.getControllerId());
node.children.add(newNode); node.children.add(newNode);
newNode.getTargets().add(targetId); newNode.getTargets().add(targetId);
@ -886,10 +886,10 @@ public class ComputerPlayer6 extends ComputerPlayer {
} }
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
if (!target.isChoiceCompleted(abilityControllerId, source, game)) { if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) {
for (UUID targetId : targets) { for (UUID targetId : targets) {
target.addTarget(targetId, source, game); target.addTarget(targetId, source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) {
targets.clear(); targets.clear();
return true; return true;
} }
@ -906,10 +906,10 @@ public class ComputerPlayer6 extends ComputerPlayer {
} }
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
if (!target.isChoiceCompleted(abilityControllerId, source, game)) { if (!target.isChoiceCompleted(abilityControllerId, source, game, cards)) {
for (UUID targetId : targets) { for (UUID targetId : targets) {
target.add(targetId, game); target.add(targetId, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, cards)) {
targets.clear(); targets.clear();
return true; return true;
} }

View file

@ -105,7 +105,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
return list; return list;
} }
protected void simulateOptions(Game game) { private void simulateOptions(Game game) {
List<ActivatedAbility> playables = game.getPlayer(playerId).getPlayable(game, isSimulatedPlayer); List<ActivatedAbility> playables = game.getPlayer(playerId).getPlayable(game, isSimulatedPlayer);
for (ActivatedAbility ability : playables) { for (ActivatedAbility ability : playables) {
if (ability.isManaAbility()) { if (ability.isManaAbility()) {
@ -176,6 +176,10 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
return options; 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) { if (AI_SIMULATE_ALL_BAD_AND_GOOD_TARGETS) {
return options; return options;
} }
@ -315,7 +319,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
List<Ability> options = getPlayableOptions(ability, game); List<Ability> options = getPlayableOptions(ability, game);
if (options.isEmpty()) { if (options.isEmpty()) {
logger.debug("simulating -- triggered ability:" + ability); 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()) { if (ability.activate(game, false) && ability.isUsesStack()) {
game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); 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) { protected void addAbilityNode(SimulationNode2 parent, Ability ability, int depth, Game game) {
Game sim = game.createSimulationForAI(); 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()) { 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(); sim.applyEffects();
SimulationNode2 newNode = new SimulationNode2(parent, sim, depth, playerId); SimulationNode2 newNode = new SimulationNode2(parent, sim, depth, playerId);

View file

@ -142,17 +142,27 @@ public class ComputerPlayer extends PlayerImpl {
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
// nothing to choose, e.g. X=0 // nothing to choose, e.g. X=0
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) {
return false; return false;
} }
// default logic for any targets
PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game); PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
possibleTargetsSelector.findNewTargets(fromCards); 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 // good targets -- choose as much as possible
for (MageItem item : possibleTargetsSelector.getGoodTargets()) { for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
target.add(item.getId(), game); target.add(item.getId(), game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, fromCards)) {
return true; return true;
} }
} }
@ -226,7 +236,7 @@ public class ComputerPlayer extends PlayerImpl {
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId()); UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
// nothing to choose, e.g. X=0 // nothing to choose, e.g. X=0
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return false; return false;
} }
@ -238,6 +248,11 @@ public class ComputerPlayer extends PlayerImpl {
return false; return false;
} }
// can't choose
if (!possibleTargetsSelector.hasMinNumberOfTargets()) {
return false;
}
// KILL PRIORITY // KILL PRIORITY
if (outcome == Outcome.Damage) { if (outcome == Outcome.Damage) {
// opponent first // opponent first
@ -251,7 +266,7 @@ public class ComputerPlayer extends PlayerImpl {
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
target.addTarget(item.getId(), leftLife, source, game); target.addTarget(item.getId(), leftLife, source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }
@ -268,7 +283,7 @@ public class ComputerPlayer extends PlayerImpl {
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
if (leftLife > 0 && leftLife <= target.getAmountRemaining()) { if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
target.addTarget(item.getId(), leftLife, source, game); target.addTarget(item.getId(), leftLife, source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }
@ -283,7 +298,7 @@ public class ComputerPlayer extends PlayerImpl {
continue; continue;
} }
target.addTarget(item.getId(), target.getAmountRemaining(), source, game); target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }
@ -303,7 +318,7 @@ public class ComputerPlayer extends PlayerImpl {
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game); int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
if (leftLife > 1) { if (leftLife > 1) {
target.addTarget(item.getId(), Math.min(leftLife - 1, target.getAmountRemaining()), source, game); 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; return true;
} }
} }
@ -322,7 +337,7 @@ public class ComputerPlayer extends PlayerImpl {
return !target.getTargets().isEmpty(); return !target.getTargets().isEmpty();
} }
target.addTarget(item.getId(), target.getAmountRemaining(), source, game); target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }
@ -339,7 +354,7 @@ public class ComputerPlayer extends PlayerImpl {
continue; continue;
} }
target.addTarget(item.getId(), target.getAmountRemaining(), source, game); target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }
@ -355,7 +370,7 @@ public class ComputerPlayer extends PlayerImpl {
return !target.getTargets().isEmpty(); return !target.getTargets().isEmpty();
} }
target.addTarget(item.getId(), target.getAmountRemaining(), source, game); target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
if (target.isChoiceCompleted(abilityControllerId, source, game)) { if (target.isChoiceCompleted(abilityControllerId, source, game, null)) {
return true; return true;
} }
} }

View file

@ -47,7 +47,6 @@ public class PossibleTargetsSelector {
// collect new valid targets // collect new valid targets
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream() List<MageItem> found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream()
.filter(id -> !target.contains(id)) .filter(id -> !target.contains(id))
.filter(id -> target.canTarget(abilityControllerId, id, source, game))
.map(id -> { .map(id -> {
Player player = game.getPlayer(id); Player player = game.getPlayer(id);
if (player != null) { 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) { public static boolean isMyItem(UUID abilityControllerId, MageItem item) {
if (item instanceof Player) { if (item instanceof Player) {
return item.getId().equals(abilityControllerId); return item.getId().equals(abilityControllerId);
@ -181,7 +184,12 @@ public class PossibleTargetsSelector {
return false; return false;
} }
boolean hasAnyTargets() { public boolean hasAnyTargets() {
return !this.any.isEmpty(); 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()) { if (ability.isUsesStack()) {
game.getStack().push(new StackAbility(ability, playerId)); game.getStack().push(game, new StackAbility(ability, playerId));
if (ability.activate(game, false)) { if (ability.activate(game, false)) {
game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId())); game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId()));
actionCount++; actionCount++;
@ -187,8 +187,8 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
abort = true; abort = true;
} }
protected boolean chooseRandom(Target target, Game game) { private boolean chooseRandom(Target target, Ability source, Game game) {
Set<UUID> possibleTargets = target.possibleTargets(playerId, game); Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game);
if (possibleTargets.isEmpty()) { if (possibleTargets.isEmpty()) {
return false; return false;
} }
@ -233,7 +233,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
@Override @Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game) { public boolean choose(Outcome outcome, Target target, Ability source, Game game) {
if (this.isHuman()) { if (this.isHuman()) {
return chooseRandom(target, game); return chooseRandom(target, source, game);
} }
return super.choose(outcome, target, source, game); return super.choose(outcome, target, source, game);
} }
@ -241,7 +241,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
@Override @Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) { public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) {
if (this.isHuman()) { if (this.isHuman()) {
return chooseRandom(target, game); return chooseRandom(target, source, game);
} }
return super.choose(outcome, target, source, game, options); return super.choose(outcome, target, source, game, options);
} }
@ -252,7 +252,7 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return false; return false;
} }
Set<UUID> possibleTargets = target.possibleTargets(playerId, cards, source, game); Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game, cards);
if (possibleTargets.isEmpty()) { if (possibleTargets.isEmpty()) {
return false; return false;
} }

View file

@ -6,6 +6,7 @@ import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.continuous.PlayAdditionalLandsAllEffect; import mage.abilities.effects.common.continuous.PlayAdditionalLandsAllEffect;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.ConsoleUtil;
import mage.utils.testers.TestableDialog; import mage.utils.testers.TestableDialog;
import mage.utils.testers.TestableDialogsRunner; import mage.utils.testers.TestableDialogsRunner;
import org.junit.Assert; import org.junit.Assert;
@ -107,7 +108,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
@Ignore // debug only - run single dialog by reg number @Ignore // debug only - run single dialog by reg number
public void test_RunSingle_Debugging() { public void test_RunSingle_Debugging() {
int needRegNumber = 557; int needRegNumber = 5;
prepareCards(); prepareCards();
@ -233,15 +234,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
if (resAssert == null) { if (resAssert == null) {
totalUnknown++; totalUnknown++;
status = "?"; status = "?";
coloredTexts.put("?", asYellow("?")); coloredTexts.put("?", ConsoleUtil.asYellow("?"));
} else if (resAssert.isEmpty()) { } else if (resAssert.isEmpty()) {
totalGood++; totalGood++;
status = "OK"; status = "OK";
coloredTexts.put("OK", asGreen("OK")); coloredTexts.put("OK", ConsoleUtil.asGreen("OK"));
} else { } else {
totalBad++; totalBad++;
status = "FAIL"; status = "FAIL";
coloredTexts.put("FAIL", asRed("FAIL")); coloredTexts.put("FAIL", ConsoleUtil.asRed("FAIL"));
assertError = resAssert; assertError = resAssert;
} }
if (!assertError.isEmpty()) { if (!assertError.isEmpty()) {
@ -256,8 +257,8 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
// print dialog error // print dialog error
if (!assertError.isEmpty()) { if (!assertError.isEmpty()) {
coloredTexts.clear(); coloredTexts.clear();
coloredTexts.put(resAssert, asRed(resAssert)); coloredTexts.put(resAssert, ConsoleUtil.asRed(resAssert));
coloredTexts.put(resDebugSource, asRed(resDebugSource)); coloredTexts.put(resDebugSource, ConsoleUtil.asRed(resDebugSource));
String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert); String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert);
String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource); String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource);
if (firstBadDialog == null) { if (firstBadDialog == null) {
@ -283,15 +284,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
String badStats = String.format("%d bad", totalBad); String badStats = String.format("%d bad", totalBad);
String unknownStats = String.format("%d unknown", totalUnknown); String unknownStats = String.format("%d unknown", totalUnknown);
coloredTexts.clear(); coloredTexts.clear();
coloredTexts.put(goodStats, String.format("%s good", asGreen(String.valueOf(totalGood)))); coloredTexts.put(goodStats, String.format("%s good", ConsoleUtil.asGreen(String.valueOf(totalGood))));
coloredTexts.put(badStats, String.format("%s bad", asRed(String.valueOf(totalBad)))); coloredTexts.put(badStats, String.format("%s bad", ConsoleUtil.asRed(String.valueOf(totalBad))));
coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown)))); 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", System.out.print(getColoredRow(totalsLeftFormat, coloredTexts, String.format("Total results: %s, %s, %s",
goodStats, badStats, unknownStats))); goodStats, badStats, unknownStats)));
// first error for fast access in big list // first error for fast access in big list
if (totalDialogs > 1 && firstBadDialog != null) { if (totalDialogs > 1 && firstBadDialog != null) {
System.out.println(horizontalBorder); 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(getColoredRow(totalsRightFormat, coloredTexts, firstBadDialog.getName() + " - " + firstBadDialog.getDescription()));
System.out.print(firstBadAssert); System.out.print(firstBadAssert);
System.out.print(firstBadDebugSource); System.out.print(firstBadDebugSource);
@ -313,16 +314,4 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
} }
return line; 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 { private void assertAllCommandsUsed() throws AssertionError {
for (Player player : currentGame.getPlayers().values()) { for (Player player : currentGame.getPlayers().values()) {
TestPlayer testPlayer = (TestPlayer) player; 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); assertActionsMustBeEmpty(testPlayer);
assertChoicesCount(testPlayer, 0); assertChoicesCount(testPlayer, 0);
assertTargetsCount(testPlayer, 0); assertTargetsCount(testPlayer, 0);
@ -2008,7 +2013,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param step * @param step
* @param player * @param player
* @param cardName * @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 ^; * multiple targets can be separated by ^;
* no target marks as TestPlayer.NO_TARGET; * no target marks as TestPlayer.NO_TARGET;
* warning, do not support cards with target adjusters - use addTarget instead * 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 * spell mode can be used only once like Demonic Pact, the
* value has to be set to the number of the remaining modes * 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). * (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) { public void setModeChoice(TestPlayer player, String choice) {
player.addModeChoice(choice); player.addModeChoice(choice);
@ -2439,6 +2445,26 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
gameOptions.skipInitShuffling = true; 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) { public void assertDamageReceived(Player player, String cardName, int expected) {
Permanent p = getPermanent(cardName, player); Permanent p = getPermanent(cardName, player);
if (p != null) { if (p != null) {

View file

@ -130,4 +130,9 @@ public class Mode implements Serializable {
public int getPawPrintValue() { public int getPawPrintValue() {
return pawPrintValue; 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.Game;
import mage.game.Table; import mage.game.Table;
import mage.players.Player;
import java.util.UUID; import java.util.UUID;
@ -11,10 +12,12 @@ import java.util.UUID;
* Supported features: * Supported features:
* - [x] collect and print game logs in server output, including unit tests * - [x] collect and print game logs in server output, including unit tests
* - [x] collect and save full games history and decks * - [x] collect and save full games history and decks
* - [ ] collect and print performance metrics like ApplyEffects calc time or inform players time (pings) * - [ ] TODO: 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 * - [ ] TODO: collect and send metrics to third party tools like prometheus + grafana
* - [ ] prepare "attachable" game data for bug reports * - [x] tests: print used selections (choices, targets, modes, skips) TODO: add yes/no, replacement effect, coins, other choices
* - [ ] record game replays data (GameView history) * - [ ] 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> * <p>
* How-to enable or disable: * How-to enable or disable:
* - use java params like -Dxmage.dataCollectors.saveGameHistory=true * - use java params like -Dxmage.dataCollectors.saveGameHistory=true
@ -62,7 +65,27 @@ public interface DataCollector {
void onChatTable(UUID tableId, String userName, String message); 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); 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.collectors.services.SaveGameHistoryDataCollector;
import mage.game.Game; import mage.game.Game;
import mage.game.Table; import mage.game.Table;
import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -140,4 +141,28 @@ final public class DataCollectorServices implements DataCollector {
public void onChatGame(UUID gameId, String userName, String message) { public void onChatGame(UUID gameId, String userName, String message) {
activeServices.forEach(c -> c.onChatGame(gameId, userName, 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.collectors.DataCollector;
import mage.game.Game; import mage.game.Game;
import mage.game.Table; import mage.game.Table;
import mage.players.Player;
import java.util.UUID; import java.util.UUID;
@ -67,4 +68,24 @@ public abstract class EmptyDataCollector implements DataCollector {
public void onChatGame(UUID gameId, String userName, String message) { public void onChatGame(UUID gameId, String userName, String message) {
// nothing // 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; package mage.collectors.services;
import mage.game.Game; import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.ConsoleUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -40,9 +42,47 @@ public class PrintGameLogsDataCollector extends EmptyDataCollector {
@Override @Override
public void onGameLog(Game game, String message) { public void onGameLog(Game game, String message) {
String needMessage = Jsoup.parse(message).text(); String needMessage = Jsoup.parse(message).text();
writeLog("GAME", "LOG", String.format("%s: %s", writeLog("LOG", "GAME", String.format("%s: %s",
CardUtil.getTurnInfo(game), CardUtil.getTurnInfo(game),
needMessage 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";
}
}