AI: purged whole targeting code and replaced with simple and shared logic (part of #13638) (#13766)

- refactor: removed outdated/unused code from AI, battlefield score, targeting, etc;
- ai: removed all targeting and filters related logic from ComputerPlayer;
- ai: improved targeting with shared and simple logic due effect's outcome and possible targets priority;
- ai: improved get amount selection (now AI will not choose big and useless values);
- ai: improved spell selection on free cast (now AI will try to cast spells with valid targets only);
- tests: improved result table with first bad dialog info for fast access (part of #13638);
This commit is contained in:
Oleg Agafonov 2025-06-18 20:03:58 +03:00 committed by GitHub
parent 1fe0d92c86
commit ffe902d25e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 777 additions and 1615 deletions

View file

@ -103,11 +103,11 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
for (boolean isTargetChoice : isTargetChoices) { for (boolean isTargetChoice : isTargetChoices) {
for (boolean isPlayerChoice : isPlayerChoices) { for (boolean isPlayerChoice : isPlayerChoices) {
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0)).aiMustChoose(true, 0)); // simulate X=0 runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0)).aiMustChoose(false, 0)); // simulate X=0
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1)).aiMustChoose(true, 1)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3)).aiMustChoose(true, 3)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5)).aiMustChoose(true, 5)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE)).aiMustChoose(true, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE)).aiMustChoose(true, 6 + 1)); // 6 own cards + 1 own player
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1)).aiMustChoose(true, 1)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3)).aiMustChoose(true, 3)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5)).aiMustChoose(true, 5)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5)).aiMustChoose(true, 5));
@ -116,7 +116,7 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5)).aiMustChoose(true, 5)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5)).aiMustChoose(true, 5)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5)).aiMustChoose(true, 5)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5)).aiMustChoose(true, 5)); // impossible on 3 targets runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5)).aiMustChoose(true, 5));
// //
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1)).aiMustChoose(false, 0)); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1)).aiMustChoose(false, 0));

View file

@ -23,6 +23,7 @@ import mage.game.stack.StackAbility;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.player.ai.ma.optimizers.TreeOptimizer; import mage.player.ai.ma.optimizers.TreeOptimizer;
import mage.player.ai.ma.optimizers.impl.*; import mage.player.ai.ma.optimizers.impl.*;
import mage.player.ai.score.GameStateEvaluator2;
import mage.player.ai.util.CombatInfo; import mage.player.ai.util.CombatInfo;
import mage.player.ai.util.CombatUtil; import mage.player.ai.util.CombatUtil;
import mage.players.Player; import mage.players.Player;

View file

@ -3,6 +3,7 @@ package mage.player.ai;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.constants.RangeOfInfluence; import mage.constants.RangeOfInfluence;
import mage.game.Game; import mage.game.Game;
import mage.player.ai.score.GameStateEvaluator2;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.Date; import java.util.Date;

View file

@ -13,7 +13,7 @@ import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep; import mage.game.turn.CombatDamageStep;
import mage.game.turn.EndOfCombatStep; import mage.game.turn.EndOfCombatStep;
import mage.game.turn.Step; import mage.game.turn.Step;
import mage.player.ai.GameStateEvaluator2; import mage.player.ai.score.GameStateEvaluator2;
import mage.players.Player; import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -0,0 +1,156 @@
package mage.player.ai;
import mage.MageItem;
import mage.MageObject;
import mage.cards.Card;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.player.ai.score.GameStateEvaluator2;
import mage.players.PlayableObjectsList;
import mage.players.Player;
import java.util.Comparator;
import java.util.UUID;
/**
* AI related code - compare and sort possible targets due target/effect type
*
* @author JayDi85
*/
public class PossibleTargetsComparator {
UUID abilityControllerId;
Game game;
PlayableObjectsList playableItems = new PlayableObjectsList();
public PossibleTargetsComparator(UUID abilityControllerId, Game game) {
this.abilityControllerId = abilityControllerId;
this.game = game;
}
public void findPlayableItems() {
this.playableItems = this.game.getPlayer(this.abilityControllerId).getPlayableObjects(this.game, Zone.ALL);
}
private int getScoreFromBattlefield(MageItem item) {
if (item instanceof Permanent) {
// use battlefield score instead simple life
return GameStateEvaluator2.evaluatePermanent((Permanent) item, game, false);
} else {
return getScoreFromLife(item);
}
}
private String getName(MageItem item) {
if (item instanceof Player) {
return ((Player) item).getName();
} else if (item instanceof MageObject) {
return ((MageObject) item).getName();
} else {
return "unknown";
}
}
private int getScoreFromLife(MageItem item) {
// TODO: replace permanent/card life by battlefield score?
int res = 0;
if (item instanceof Player) {
res = ((Player) item).getLife();
} else if (item instanceof Card) {
Card card = (Card) item;
if (card.isPlaneswalker(game)) {
res = card.getCounters(game).getCount(CounterType.LOYALTY);
} else if (card.isBattle(game)) {
res = card.getCounters(game).getCount(CounterType.DEFENSE);
} else {
int damage = 0;
if (card instanceof Permanent) {
damage = ((Permanent) card).getDamage();
}
res = Math.max(0, card.getToughness().getValue() - damage);
}
// instant
if (res == 0) {
res = card.getManaValue();
}
}
return res;
}
private boolean isMyItem(MageItem item) {
return PossibleTargetsSelector.isMyItem(this.abilityControllerId, item);
}
// sort by name-id at the end, so AI will use same choices in all simulations
private final Comparator<MageItem> BY_NAME = (o1, o2) -> getName(o2).compareTo(getName(o1));
private final Comparator<MageItem> BY_ID = Comparator.comparing(MageItem::getId);
private final Comparator<MageItem> BY_ME = (o1, o2) -> Boolean.compare(
isMyItem(o2),
isMyItem(o1)
);
private final Comparator<MageItem> BY_BIGGER_SCORE = (o1, o2) -> Integer.compare(
getScoreFromBattlefield(o2),
getScoreFromBattlefield(o1)
);
private final Comparator<MageItem> BY_PLAYABLE = (o1, o2) -> Boolean.compare(
this.playableItems.containsObject(o2.getId()),
this.playableItems.containsObject(o1.getId())
);
private final Comparator<MageItem> BY_LAND = (o1, o2) -> {
boolean isLand1 = o1 instanceof MageObject && ((MageObject) o1).isLand(game);
boolean isLand2 = o2 instanceof MageObject && ((MageObject) o2).isLand(game);
return Boolean.compare(isLand2, isLand1);
};
private final Comparator<MageItem> BY_TYPE_PLAYER = (o1, o2) -> Boolean.compare(
o2 instanceof Player,
o1 instanceof Player
);
private final Comparator<MageItem> BY_TYPE_PLANESWALKER = (o1, o2) -> {
boolean isPlaneswalker1 = o1 instanceof MageObject && ((MageObject) o1).isPlaneswalker(game);
boolean isPlaneswalker2 = o2 instanceof MageObject && ((MageObject) o2).isPlaneswalker(game);
return Boolean.compare(isPlaneswalker2, isPlaneswalker1);
};
private final Comparator<MageItem> BY_TYPE_BATTLE = (o1, o2) -> {
boolean isBattle1 = o1 instanceof MageObject && ((MageObject) o1).isBattle(game);
boolean isBattle2 = o2 instanceof MageObject && ((MageObject) o2).isBattle(game);
return Boolean.compare(isBattle2, isBattle1);
};
private final Comparator<MageItem> BY_TYPES = BY_TYPE_PLANESWALKER
.thenComparing(BY_TYPE_BATTLE)
.thenComparing(BY_TYPE_PLAYER);
/**
* Default sorting for good effects - put the biggest items to the top
*/
public final Comparator<MageItem> ANY_MOST_VALUABLE_FIRST = BY_TYPES
.thenComparing(BY_BIGGER_SCORE)
.thenComparing(BY_NAME)
.thenComparing(BY_ID);
public final Comparator<MageItem> ANY_MOST_VALUABLE_LAST = ANY_MOST_VALUABLE_FIRST.reversed();
/**
* Default sorting for good effects - put own and biggest items to the top
*/
public final Comparator<MageItem> MY_MOST_VALUABLE_FIRST = BY_ME
.thenComparing(ANY_MOST_VALUABLE_FIRST);
public final Comparator<MageItem> MY_MOST_VALUABLE_LAST = MY_MOST_VALUABLE_FIRST.reversed();
/**
* Sorting for discard effects - put the biggest unplayable at the top, lands at the end anyway
*/
public final Comparator<MageItem> ANY_UNPLAYABLE_AND_USELESS = BY_LAND.reversed()
.thenComparing(BY_PLAYABLE.reversed())
.thenComparing(ANY_MOST_VALUABLE_FIRST);
}

View file

@ -0,0 +1,184 @@
package mage.player.ai;
import mage.MageItem;
import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.ControllableOrOwnerable;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
import mage.target.common.TargetDiscard;
import java.util.*;
import java.util.stream.Collectors;
/**
* AI related code - find possible targets and sort it due priority
*
* @author JayDi85
*/
public class PossibleTargetsSelector {
Outcome outcome;
Target target;
UUID abilityControllerId;
Ability source;
Game game;
PossibleTargetsComparator comparators;
// possible targets lists
List<MageItem> me = new ArrayList<>();
List<MageItem> opponents = new ArrayList<>();
List<MageItem> any = new ArrayList<>(); // for outcomes with any target like copy
public PossibleTargetsSelector(Outcome outcome, Target target, UUID abilityControllerId, Ability source, Game game) {
this.outcome = outcome;
this.target = target;
this.abilityControllerId = abilityControllerId;
this.source = source;
this.game = game;
this.comparators = new PossibleTargetsComparator(abilityControllerId, game);
}
public void findNewTargets(Set<UUID> fromTargetsList) {
// collect new valid targets
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game).stream()
.filter(id -> !target.contains(id))
.filter(id -> fromTargetsList == null || fromTargetsList.contains(id))
.filter(id -> target.canTarget(abilityControllerId, id, source, game))
.map(id -> {
Player player = game.getPlayer(id);
if (player != null) {
return player;
} else {
return game.getObject(id);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// split targets between me and opponents
found.forEach(item -> {
if (isMyItem(abilityControllerId, item)) {
this.me.add(item);
} else {
this.opponents.add(item);
}
this.any.add(item);
});
if (target instanceof TargetDiscard) {
// sort due unplayable
sortByUnplayableAndUseless();
} else {
// sort due good/bad outcome
sortByMostValuableTargets();
}
}
/**
* Sorting for any good/bad effects
*/
private void sortByMostValuableTargets() {
if (isGoodEffect()) {
// for good effect must choose the biggest objects
this.me.sort(comparators.MY_MOST_VALUABLE_FIRST);
this.opponents.sort(comparators.MY_MOST_VALUABLE_LAST);
this.any.sort(comparators.ANY_MOST_VALUABLE_FIRST);
} else {
// for bad effect must choose the smallest objects
this.me.sort(comparators.MY_MOST_VALUABLE_LAST);
this.opponents.sort(comparators.MY_MOST_VALUABLE_FIRST);
this.any.sort(comparators.ANY_MOST_VALUABLE_LAST);
}
}
/**
* Sorting for discard
*/
private void sortByUnplayableAndUseless() {
// used
// no good or bad effect - you must choose
comparators.findPlayableItems();
this.me.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
this.opponents.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
this.any.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
}
/**
* Priority targets. Try to use as much as possible.
*/
public List<MageItem> getGoodTargets() {
if (isAnyEffect()) {
return this.any;
}
if (isGoodEffect()) {
return this.me;
} else {
return this.opponents;
}
}
/**
* Optional targets. Try to ignore bad targets (e.g. opponent's creatures for your good effect).
*/
public List<MageItem> getBadTargets() {
if (isAnyEffect()) {
return Collections.emptyList();
}
if (isGoodEffect()) {
return this.opponents;
} else {
return this.me;
}
}
public static boolean isMyItem(UUID abilityControllerId, MageItem item) {
if (item instanceof Player) {
return item.getId().equals(abilityControllerId);
} else if (item instanceof ControllableOrOwnerable) {
return ((ControllableOrOwnerable) item).getControllerOrOwnerId().equals(abilityControllerId);
}
return false;
}
private boolean isAnyEffect() {
boolean isAnyEffect = outcome.anyTargetHasSameValue();
if (hasGoodExile()) {
isAnyEffect = true;
}
return isAnyEffect;
}
private boolean isGoodEffect() {
boolean isGoodEffect = outcome.isGood();
if (hasGoodExile()) {
isGoodEffect = true;
}
return isGoodEffect;
}
private boolean hasGoodExile() {
// exile workaround: exile is bad, but exile from library or graveyard in most cases is good
// (more exiled -- more good things you get, e.g. delve's pay or search cards with same name)
if (outcome == Outcome.Exile) {
if (Zone.GRAVEYARD.match(target.getZone())
|| Zone.LIBRARY.match(target.getZone())) {
// TargetCardInGraveyardBattlefieldOrStack - used for additional payment like Craft, so do not allow big cards for it
if (!(target instanceof TargetCardInGraveyardBattlefieldOrStack)) {
return true;
}
}
}
return false;
}
}

View file

@ -1,4 +1,4 @@
package mage.player.ai.ma; package mage.player.ai.score;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;

View file

@ -1,8 +1,7 @@
package mage.player.ai; package mage.player.ai.score;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.player.ai.ma.ArtificialScoringSystem;
import mage.players.Player; import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -1,4 +1,4 @@
package mage.player.ai.ma; package mage.player.ai.score;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.keyword.*; import mage.abilities.keyword.*;
@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* TODO: outdated, replace by edh or commander brackets ability score
* @author nantuko * @author nantuko
*/ */
public final class MagicAbility { public final class MagicAbility {

View file

@ -1,65 +0,0 @@
package mage.player.ai.simulators;
import mage.abilities.ActivatedAbility;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.player.ai.ComputerPlayer;
import mage.player.ai.PermanentEvaluator;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class ActionSimulator {
private ComputerPlayer player;
private List<Card> playableInstants = new ArrayList<>();
private List<ActivatedAbility> playableAbilities = new ArrayList<>();
private Game game;
public ActionSimulator(ComputerPlayer player) {
this.player = player;
}
public void simulate(Game game) {
}
public int evaluateState() {
// must find all leaved opponents
Player opponent = game.getPlayer(game.getOpponents(player.getId(), false).stream().findFirst().orElse(null));
if (opponent == null) {
return Integer.MAX_VALUE;
}
if (game.checkIfGameIsOver()) {
if (player.hasLost() || opponent.hasWon()) {
return Integer.MIN_VALUE;
}
if (opponent.hasLost() || player.hasWon()) {
return Integer.MAX_VALUE;
}
}
int value = player.getLife();
value -= opponent.getLife();
PermanentEvaluator evaluator = new PermanentEvaluator();
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
value += evaluator.evaluate(permanent, game);
}
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
value -= evaluator.evaluate(permanent, game);
}
value += player.getHand().size();
value -= opponent.getHand().size();
return value;
}
}

View file

@ -1,7 +1,5 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.AttachEffect;
@ -10,30 +8,30 @@ import mage.abilities.keyword.EnchantAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.constants.SubType;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/** /**
*
* @author Alvin * @author Alvin
*/ */
public final class DeadWeight extends CardImpl { public final class DeadWeight extends CardImpl {
public DeadWeight(UUID ownerId, CardSetInfo setInfo) { public DeadWeight(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}"); super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}");
this.subtype.add(SubType.AURA); this.subtype.add(SubType.AURA);
// Enchant creature // Enchant creature
TargetPermanent auraTarget = new TargetCreaturePermanent(); TargetPermanent auraTarget = new TargetCreaturePermanent();
this.getSpellAbility().addTarget(auraTarget); this.getSpellAbility().addTarget(auraTarget);
this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment)); this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment));
Ability ability = new EnchantAbility(auraTarget); Ability ability = new EnchantAbility(auraTarget);
this.addAbility(ability); this.addAbility(ability);
// Enchanted creature gets -2/-2. // Enchanted creature gets -2/-2.
this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(-2, -2, Duration.WhileOnBattlefield))); this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(-2, -2, Duration.WhileOnBattlefield)));
} }

View file

@ -62,7 +62,7 @@ public final class SepulchralPrimordial extends CardImpl {
class SepulchralPrimordialEffect extends OneShotEffect { class SepulchralPrimordialEffect extends OneShotEffect {
SepulchralPrimordialEffect() { SepulchralPrimordialEffect() {
super(Outcome.PutCreatureInPlay); super(Outcome.GainControl);
this.staticText = "for each opponent, you may put up to one target creature card from that player's graveyard onto the battlefield under your control"; this.staticText = "for each opponent, you may put up to one target creature card from that player's graveyard onto the battlefield under your control";
} }

View file

@ -0,0 +1,149 @@
package org.mage.test.AI.basic;
import mage.MageItem;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect;
import mage.constants.Outcome;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.player.ai.PossibleTargetsSelector;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetDiscard;
import mage.target.common.TargetPermanentOrPlayer;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author JayDi85
*/
public class PossibleTargetsSelectorAITest extends CardTestPlayerBase {
@Test
public void test_SortByOutcome() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerA, "Spectral Bears", 1); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); // land
addCard(Zone.BATTLEFIELD, playerA, "Gideon, Martial Paragon", 1); // planeswalker
//
addCard(Zone.BATTLEFIELD, playerB, "Forest", 1); // land
addCard(Zone.BATTLEFIELD, playerB, "Goblin Brigand", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Battering Sliver", 1); // 4/4
runCode("check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// most valuable (planeswalker -> player -> bigger -> smaller)
// good effect
PossibleTargetsSelector selector = prepareAnyTargetSelector(Outcome.Benefit);
selector.findNewTargets(null);
assertTargets("good effect must return my most valuable and biggest as priority", selector.getGoodTargets(), Arrays.asList(
"Gideon, Martial Paragon", // pw
"PlayerA", // p
"Spectral Bears", // 3/3
"Balduvian Bears", // 2/2
"Arbor Elf", // 1/1
"Forest" // l
));
assertTargets("good effect must return opponent's lowest as optional", selector.getBadTargets(), Arrays.asList(
"Forest", // l
"Goblin Brigand", // 2/2
"Battering Sliver", // 4/4
"PlayerB" // p
));
// bad effect - must be inverted
selector = prepareAnyTargetSelector(Outcome.Detriment);
selector.findNewTargets(null);
assertTargets("bad effect must return opponent's most valuable and biggest as priority", selector.getGoodTargets(), Arrays.asList(
"PlayerB", // p
"Battering Sliver", // 4/4
"Goblin Brigand", // 1/1
"Forest" // l
));
assertTargets("bad effect must return my lowest as optional", selector.getBadTargets(), Arrays.asList(
"Forest", // l
"Arbor Elf", // 1/1
"Balduvian Bears", // 2/2
"Spectral Bears", // 3/3
"PlayerA", // p
"Gideon, Martial Paragon" // pw
));
});
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
}
@Test
public void test_SortByPlayable() {
addCard(Zone.HAND, playerA, "Balduvian Bears", 1); // 2/2, {1}{G}
addCard(Zone.HAND, playerA, "Arbor Elf", 1); // 1/1, {G}, playable
addCard(Zone.HAND, playerA, "Spectral Bears", 1); // 3/3, {1}{G}
addCard(Zone.HAND, playerA, "Forest", 1); // land
addCard(Zone.HAND, playerA, "Gideon, Martial Paragon", 1); // planeswalker, {4}{W}
//
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
runCode("check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
// discard logic (remove the biggest unplayable first, land at the end)
PossibleTargetsSelector selector = prepareDiscardCardSelector();
selector.findNewTargets(null);
assertTargets("discard must return biggest unplayable first", selector.getGoodTargets(), Arrays.asList(
"Gideon, Martial Paragon", // pw
"Spectral Bears", // 3/3
"Balduvian Bears", // 2/2
"Arbor Elf", // 1/1 - playable
"Forest" // l
));
});
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
}
private PossibleTargetsSelector prepareAnyTargetSelector(Outcome outcome) {
Target target = new TargetPermanentOrPlayer();
Ability fakeAbility = new SimpleStaticAbility(new InfoEffect("fake"));
return new PossibleTargetsSelector(outcome, target, playerA.getId(), fakeAbility, currentGame);
}
private PossibleTargetsSelector prepareDiscardCardSelector() {
Target target = new TargetDiscard(playerA.getId());
Ability fakeAbility = new SimpleStaticAbility(new InfoEffect("fake"));
// discard sorting do not use outcome
return new PossibleTargetsSelector(Outcome.Benefit, target, playerA.getId(), fakeAbility, currentGame);
}
private void assertTargets(String info, List<MageItem> targets, List<String> needTargets) {
List<String> currentTargets = targets.stream()
.map(item -> {
if (item instanceof Player) {
return ((Player) item).getName();
} else if (item instanceof MageObject) {
return ((MageObject) item).getName();
} else {
return "unknown item";
}
})
.collect(Collectors.toList());
String current = String.join("\n", currentTargets);
String need = String.join("\n", needTargets);
if (!current.equals(need)) {
Assert.fail(info + "\n\n"
+ "NEED targets:\n" + need + "\n\n"
+ "FOUND targets:\n" + current + "\n");
}
}
}

View file

@ -125,9 +125,9 @@ public class SearchNameExileTests extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Test of Talents", 1); addCard(Zone.HAND, playerA, "Test of Talents", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2); addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 1); addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 2);
addCard(Zone.HAND, playerB, "Ready // Willing", 2); addCard(Zone.HAND, playerB, "Ready // Willing", 2);
addCard(Zone.LIBRARY, playerB, "Ready // Willing", 1); addCard(Zone.LIBRARY, playerB, "Ready // Willing", 2);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 2); addCard(Zone.BATTLEFIELD, playerB, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2);
@ -137,7 +137,7 @@ public class SearchNameExileTests extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Ready // Willing", "Ready // Willing"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Ready // Willing", "Ready // Willing");
// TODO: a non strict cause a good AI choice test - make strict and duplicate as really AI test? // TODO: a non strict cause a good AI choice test - make strict and duplicate as really AI test?
// in non strict mode AI must choose as much as possible in good "up to" target and half in bad target // in non strict mode AI must choose as much as possible from grave/library due good exile effect/cost
setStrictChooseMode(false); setStrictChooseMode(false);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
@ -149,6 +149,6 @@ public class SearchNameExileTests extends CardTestPlayerBase {
assertHandCount(playerB, "Ready // Willing", 0); assertHandCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, 1); //add 2, cast 1, last is exiled+redrawn assertHandCount(playerB, 1); //add 2, cast 1, last is exiled+redrawn
assertExileCount(playerB, "Ready // Willing", 4); assertExileCount(playerB, "Ready // Willing", 6);
} }
} }

View file

@ -63,20 +63,23 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
public void testAuraTokenRedirect() { public void testAuraTokenRedirect() {
makeTester(); makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser"); addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser"); // 3/3
addCard(Zone.BATTLEFIELD, playerB, "Hill Giant"); addCard(Zone.BATTLEFIELD, playerB, "Serra Angel"); // 4/4
addCard(Zone.HAND, playerA, "Dead Weight"); addCard(Zone.HAND, playerA, "Dead Weight");
setChoice(playerA, true); setChoice(playerA, true); // use new target
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dead Weight", "Centaur Courser"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dead Weight", "Centaur Courser");
// it's bad/unboost effect
// allow AI make a choice for new target of copied spell (it will be angel as a opponent's bigger creature for bad effect)
setStrictChooseMode(false);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
assertPermanentCount(playerB, "Centaur Courser", 1); assertPermanentCount(playerB, "Centaur Courser", 1);
assertPowerToughness(playerB, "Centaur Courser", 1, 1); assertPowerToughness(playerB, "Centaur Courser", 3 - 2, 3 - 2);
assertPermanentCount(playerB, "Hill Giant", 1); assertPermanentCount(playerB, "Serra Angel", 1);
assertPowerToughness(playerB, "Hill Giant", 1, 1); assertPowerToughness(playerB, "Serra Angel", 4 - 2, 4 - 2);
assertPermanentCount(playerA, "Dead Weight", 2); assertPermanentCount(playerA, "Dead Weight", 2);
} }
@ -152,23 +155,26 @@ public class CopyPermanentSpellTest extends CardTestPlayerBase {
public void testBestowRedirect() { public void testBestowRedirect() {
makeTester(); makeTester();
addCard(Zone.BATTLEFIELD, playerA, "Island", 5); addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf"); // 1/1
addCard(Zone.HAND, playerA, "Nimbus Naiad"); addCard(Zone.HAND, playerA, "Nimbus Naiad");
setChoice(playerA, true); setChoice(playerA, true); // change target
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nimbus Naiad using bestow", "Grizzly Bears"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nimbus Naiad using bestow", "Grizzly Bears");
// it's good/boost effect
// allow AI make a choice for new target of copied spell (it will be bear as a bigger creature for good effect)
setStrictChooseMode(false);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
assertPermanentCount(playerA, "Grizzly Bears", 1); assertPermanentCount(playerA, "Grizzly Bears", 1);
assertPowerToughness(playerA, "Grizzly Bears", 4, 4); assertPowerToughness(playerA, "Grizzly Bears", 2 + 2 + 2, 2 + 2 + 2);
assertAbility(playerA, "Grizzly Bears", FlyingAbility.getInstance(), true); assertAbility(playerA, "Grizzly Bears", FlyingAbility.getInstance(), true);
assertPermanentCount(playerA, "Silvercoat Lion", 1); assertPermanentCount(playerA, "Arbor Elf", 1);
assertPowerToughness(playerA, "Silvercoat Lion", 4, 4); assertPowerToughness(playerA, "Arbor Elf", 1, 1);
assertAbility(playerA, "Silvercoat Lion", FlyingAbility.getInstance(), true); assertAbility(playerA, "Arbor Elf", FlyingAbility.getInstance(), false);
assertPermanentCount(playerA, "Nimbus Naiad", 2); assertPermanentCount(playerA, "Nimbus Naiad", 2);
} }

View file

@ -67,7 +67,7 @@ public class JaceTest extends CardTestPlayerBase {
setStopAt(3, PhaseStep.BEGIN_COMBAT); setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertGraveyardCount(playerA, "Pillarfield Ox", 1); assertGraveyardCount(playerA, "Pillarfield Ox", 1); // must discard a creature and keep land card
assertExileCount("Jace, Vryn's Prodigy", 0); assertExileCount("Jace, Vryn's Prodigy", 0);
assertPermanentCount(playerA, "Jace, Telepath Unbound", 1); assertPermanentCount(playerA, "Jace, Telepath Unbound", 1);
} }

View file

@ -9,11 +9,25 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
* @author TheElk801 * @author TheElk801
*/ */
public class BeamsplitterMageTest extends CardTestPlayerBase { public class BeamsplitterMageTest extends CardTestPlayerBase {
/**
* 2/2
* <p>
* Whenever you cast an instant or sorcery spell that targets only Beamsplitter Mage,
* if you control one or more creatures that spell could target, choose one of those
* creatures. Copy that spell. The copy targets the chosen creature.
*/
private static final String bsm = "Beamsplitter Mage"; private static final String bsm = "Beamsplitter Mage";
/**
* Target creature gets +1/+1 until end of turn.
* Target creature gets +1/+1 until end of turn.
* Target creature gets +1/+1 until end of turn.
*/
private static final String seeds = "Seeds of Strength";
private static final String lion = "Silvercoat Lion"; private static final String lion = "Silvercoat Lion";
private static final String duelist = "Deft Duelist"; private static final String duelist = "Deft Duelist";
private static final String bolt = "Lightning Bolt"; private static final String bolt = "Lightning Bolt";
private static final String seeds = "Seeds of Strength";
@Test @Test
public void testLightningBolt() { public void testLightningBolt() {
@ -24,7 +38,9 @@ public class BeamsplitterMageTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, bolt); addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, bsm); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, bsm);
setChoice(playerA, lion); // target for copied bolt
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
@ -45,12 +61,18 @@ public class BeamsplitterMageTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, bsm); addCard(Zone.BATTLEFIELD, playerA, bsm);
addCard(Zone.HAND, playerA, seeds); addCard(Zone.HAND, playerA, seeds);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, seeds, bsm); // put x3 targets to bsm and copy it to lion
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, seeds);
addTarget(playerA, bsm);
addTarget(playerA, bsm);
addTarget(playerA, bsm);
setChoice(playerA, lion);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute(); execute();
assertPowerToughness(playerA, bsm, 5, 5); assertPowerToughness(playerA, bsm, 2 + 3, 2 + 3);
assertPowerToughness(playerA, lion, 5, 5); assertPowerToughness(playerA, lion, 2 + 3, 2 + 3);
} }
} }

View file

@ -1,23 +1,22 @@
package org.mage.test.cards.single.znr; package org.mage.test.cards.single.znr;
import mage.cards.decks.Deck;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import org.assertj.core.api.Assertions;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
/** /**
* {@link mage.cards.y.YasharnImplacableEarth Yasharn, Implacable Earth}
* When Yasharn enters the battlefield, search your library for a basic Forest card and a basic Plains card, reveal those cards, put them into your hand, then shuffle.
* Players cant pay life or sacrifice nonland permanents to cast spells or activate abilities.
*
* @author Alex-Vasile * @author Alex-Vasile
*/ */
public class YasharnImplacableEarthTest extends CardTestPlayerBase { public class YasharnImplacableEarthTest extends CardTestPlayerBase {
/**
* {@link mage.cards.y.YasharnImplacableEarth Yasharn, Implacable Earth}
* When Yasharn enters the battlefield, search your library for a basic Forest card and a basic Plains card, reveal those cards, put them into your hand, then shuffle.
* Players cant pay life or sacrifice nonland permanents to cast spells or activate abilities.
*/
private static final String yasharn = "Yasharn, Implacable Earth"; private static final String yasharn = "Yasharn, Implacable Earth";
/** /**
@ -143,6 +142,8 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase {
*/ */
@Test @Test
public void canSacrificeLandToActivate() { public void canSacrificeLandToActivate() {
removeAllCardsFromLibrary(playerA);
addCard(Zone.BATTLEFIELD, playerA, yasharn); addCard(Zone.BATTLEFIELD, playerA, yasharn);
addCard(Zone.BATTLEFIELD, playerA, "Evolving Wilds"); addCard(Zone.BATTLEFIELD, playerA, "Evolving Wilds");
addCard(Zone.LIBRARY, playerA, "Island"); addCard(Zone.LIBRARY, playerA, "Island");

View file

@ -77,7 +77,8 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase {
// test NPE error while AI targeting battlefield with tokens // test NPE error while AI targeting battlefield with tokens
// Flying // Flying
// When Angel of Serenity enters the battlefield, you may exile up to three other target creatures from the battlefield and/or creature cards from graveyards. // When Angel of Serenity enters the battlefield, you may exile up to three other target creatures
// from the battlefield and/or creature cards from graveyards.
addCard(Zone.HAND, playerA, "Angel of Serenity"); addCard(Zone.HAND, playerA, "Angel of Serenity");
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
// //
@ -98,6 +99,7 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity");
setChoice(playerA, true); setChoice(playerA, true);
//addTarget(playerA, "Silvercoat Lion^Balduvian Bears"); // AI must target //addTarget(playerA, "Silvercoat Lion^Balduvian Bears"); // AI must target
//addTarget(playerA, TestPlayer.TARGET_SKIP);
setStrictChooseMode(false); // AI must target setStrictChooseMode(false); // AI must target
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -3,6 +3,7 @@ package org.mage.test.dialogs;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
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.utils.testers.TestableDialog; import mage.utils.testers.TestableDialog;
@ -33,8 +34,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
public void test_RunSingle_Manual() { public void test_RunSingle_Manual() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); prepareCards();
addCard(Zone.HAND, playerA, "Forest", 6);
runCode("run single", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { runCode("run single", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialog(runner, "target.choose(you, target)", "any 0-3"); TestableDialog dialog = findDialog(runner, "target.choose(you, target)", "any 0-3");
@ -57,8 +57,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
public void test_RunSingle_AI() { public void test_RunSingle_AI() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); prepareCards();
addCard(Zone.HAND, playerA, "Forest", 6);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
@ -75,37 +74,13 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
assertAndPrintRunnerResults(false, true); assertAndPrintRunnerResults(false, true);
} }
@Test
@Ignore // debug only - run single dialog by reg number
public void test_RunSingle_Debugging() {
int needRegNumber = 7;
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.HAND, playerA, "Forest", 6);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
runCode("run by number", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialog(runner, needRegNumber);
dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAndPrintRunnerResults(false, true);
}
@Test @Test
@Ignore // TODO: enable and fix all failed dialogs @Ignore // TODO: enable and fix all failed dialogs
public void test_RunAll_AI() { public void test_RunAll_AI() {
// it's impossible to setup 700+ dialogs, so all choices made by AI // it's impossible to setup 700+ dialogs, so all choices made by AI
// current AI uses only simple choices in dialogs, not simulations // current AI uses only simple choices in dialogs, not simulations
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); prepareCards();
addCard(Zone.HAND, playerA, "Forest", 6);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
@ -130,6 +105,43 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
assertAndPrintRunnerResults(true, true); assertAndPrintRunnerResults(true, true);
} }
@Test
@Ignore // debug only - run single dialog by reg number
public void test_RunSingle_Debugging() {
int needRegNumber = 93;
prepareCards();
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
runCode("run by number", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialog(runner, needRegNumber);
dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAndPrintRunnerResults(false, true);
}
private void prepareCards() {
// runner calls dialogs for both A and B, so players must have same cards
removeAllCardsFromLibrary(playerA);
removeAllCardsFromLibrary(playerB);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.HAND, playerA, "Forest", 6);
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 6);
addCard(Zone.HAND, playerB, "Forest", 6);
runCode("restrict lands", 1, PhaseStep.UPKEEP, playerA, (info, player, game) -> {
// restrict any lands play, so AI will keep lands in hand
game.addEffect(new PlayAdditionalLandsAllEffect(-1), fakeAbility);
});
}
private TestableDialog findDialog(TestableDialogsRunner runner, String byGroup, String byName) { private TestableDialog findDialog(TestableDialogsRunner runner, String byGroup, String byName) {
List<TestableDialog> res = runner.getDialogs().stream() List<TestableDialog> res = runner.getDialogs().stream()
.filter(dialog -> dialog.getGroup().equals(byGroup)) .filter(dialog -> dialog.getGroup().equals(byGroup))
@ -194,6 +206,9 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
int totalGood = 0; int totalGood = 0;
int totalBad = 0; int totalBad = 0;
int totalUnknown = 0; int totalUnknown = 0;
TestableDialog firstBadDialog = null;
String firstBadAssert = "";
String firstBadDebugSource = "";
boolean usedHorizontalBorder = true; // mark that last print used horizontal border (fix duplicates) boolean usedHorizontalBorder = true; // mark that last print used horizontal border (fix duplicates)
Map<String, String> coloredTexts = new HashMap<>(); // must colorize after string format to keep pretty table Map<String, String> coloredTexts = new HashMap<>(); // must colorize after string format to keep pretty table
for (TestableDialog dialog : runner.getDialogs()) { for (TestableDialog dialog : runner.getDialogs()) {
@ -244,8 +259,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
coloredTexts.clear(); coloredTexts.clear();
coloredTexts.put(resAssert, asRed(resAssert)); coloredTexts.put(resAssert, asRed(resAssert));
coloredTexts.put(resDebugSource, asRed(resDebugSource)); coloredTexts.put(resDebugSource, asRed(resDebugSource));
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resAssert)); String badAssert = getColoredRow(totalsRightFormat, coloredTexts, resAssert);
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resDebugSource)); String badDebugSource = getColoredRow(totalsRightFormat, coloredTexts, resDebugSource);
if (firstBadDialog == null) {
firstBadDialog = dialog;
firstBadAssert = badAssert;
firstBadDebugSource = badDebugSource;
}
System.out.print(badAssert);
System.out.print(badDebugSource);
System.out.println(horizontalBorder); System.out.println(horizontalBorder);
usedHorizontalBorder = true; usedHorizontalBorder = true;
} }
@ -267,6 +289,15 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown)))); coloredTexts.put(unknownStats, String.format("%s unknown", 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
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, firstBadDialog.getName() + " - " + firstBadDialog.getDescription()));
System.out.print(firstBadAssert);
System.out.print(firstBadDebugSource);
}
// table end // table end
System.out.println(horizontalBorder); System.out.println(horizontalBorder);
usedHorizontalBorder = true; usedHorizontalBorder = true;

View file

@ -47,23 +47,23 @@ public enum Outcome {
private final boolean good; private final boolean good;
// no different between own or opponent targets (example: copy must choose from all permanents) // no different between own or opponent targets (example: copy must choose from all permanents)
private boolean canTargetAll; private boolean anyTargetHasSameValue;
Outcome(boolean good) { Outcome(boolean good) {
this.good = good; this.good = good;
} }
Outcome(boolean good, boolean canTargetAll) { Outcome(boolean good, boolean anyTargetHasSameValue) {
this.good = good; this.good = good;
this.canTargetAll = canTargetAll; this.anyTargetHasSameValue = anyTargetHasSameValue;
} }
public boolean isGood() { public boolean isGood() {
return good; return good;
} }
public boolean isCanTargetAll() { public boolean anyTargetHasSameValue() {
return canTargetAll; return anyTargetHasSameValue;
} }
public static Outcome inverse(Outcome outcome) { public static Outcome inverse(Outcome outcome) {

View file

@ -4,6 +4,8 @@ import mage.constants.CardType;
import mage.filter.predicate.Predicates; import mage.filter.predicate.Predicates;
/** /**
* Warning, it's a filter for damage effects only (ignore lands, artifacts and other non-damageable objects)
*
* @author TheElk801 * @author TheElk801
*/ */
public class FilterAnyTarget extends FilterPermanentOrPlayer { public class FilterAnyTarget extends FilterPermanentOrPlayer {

View file

@ -28,6 +28,7 @@ public interface Target extends Copyable<Target>, Serializable {
*/ */
boolean isChosen(Game game); boolean isChosen(Game game);
@Deprecated // TODO: replace usage in cards by full version from choose methods
boolean isChoiceCompleted(Game game); boolean isChoiceCompleted(Game game);
boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game); boolean isChoiceCompleted(UUID abilityControllerId, Ability source, Game game);
@ -84,7 +85,7 @@ public interface Target extends Copyable<Target>, Serializable {
/** /**
* @param id * @param id
* @param source WARNING, it can be null for AI or other calls from events (TODO: introduce normal source in AI ComputerPlayer) * @param source WARNING, it can be null for AI or other calls from events
* @param game * @param game
* @return * @return
*/ */

View file

@ -261,7 +261,6 @@ public abstract class TargetImpl implements Target {
} }
@Override @Override
@Deprecated // TODO: replace usage in cards by full version from choose methods
public boolean isChoiceCompleted(Game game) { public boolean isChoiceCompleted(Game game) {
return isChoiceCompleted(null, null, game); return isChoiceCompleted(null, null, game);
} }

View file

@ -3,6 +3,8 @@ package mage.target.common;
import mage.filter.common.FilterAnyTarget; import mage.filter.common.FilterAnyTarget;
/** /**
* Warning, it's a target for damage effects only (ignore lands, artifacts and other non-damageable objects)
*
* @author JRHerlehy Created on 4/8/18. * @author JRHerlehy Created on 4/8/18.
*/ */
public class TargetAnyTarget extends TargetPermanentOrPlayer { public class TargetAnyTarget extends TargetPermanentOrPlayer {

View file

@ -18,6 +18,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard { public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
@ -97,7 +98,7 @@ public class TargetCardInGraveyardBattlefieldOrStack extends TargetCard {
@Override @Override
public boolean canTarget(UUID id, Game game) { public boolean canTarget(UUID id, Game game) {
return this.canTarget(null, id, null, game); return this.canTarget(null, id, null, game); // wtf
} }
@Override @Override