other: reworked target selection: (#13638)

- WIP: AI and multi targets, human and X=0 use cases, human and impossible targets use cases;
- improved stability and shared logic (related to #13606, #11134, #11666, continue from a53eb66b58, close #13617, close #13613);
- improved test logs and debug info to show more target info on errors;
- improved test framework to support multiple addTarget calls;
- improved test framework to find bad commands order for targets (related to #11666);
- fixed game freezes on auto-choice usages with disconnected or under control players (related to #11285);
- gui, game: fixed that player doesn't mark avatar as selected/green in "up to" targeting;
- gui, game: fixed small font in some popup messages on big screens (related to #969);
- gui, game: added min targets info for target selection dialog;
- for devs: added new cheat option to call and test any game dialog (define own dialogs, targets, etc in HumanDialogsTester);
- for devs: now tests require complete an any or up to target selection by addTarget + TestPlayer.TARGET_SKIP or setChoice + TestPlayer.CHOICE_SKIP (if not all max/possible targets used);
- for devs: added detail targets info for activate/trigger/cast, can be useful to debug unit tests, auto-choose or AI (see DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS)
This commit is contained in:
Oleg Agafonov 2025-05-16 13:55:54 +04:00 committed by GitHub
parent 80d62727e1
commit 133e4fe425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 2737 additions and 743 deletions

View file

@ -33,9 +33,10 @@ public class LightningStormTest extends CardTestPlayerBase {
// B discard and re-target
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Discard");
setChoice(playerB, "Mountain"); // discard cost
setChoice(playerB, true); // change target
addTarget(playerB, playerA); // new target
setChoice(playerB, "Mountain"); // discard cost
// A discard and re-target
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard");

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.enters;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -61,18 +62,19 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase {
@Test
public void sixEnterWithScapeshiftDamageToPlayerB() {
addCard(Zone.LIBRARY, playerA, "Mountain", 6);
addCard(Zone.LIBRARY, playerA, "Mountain", 10);
addCard(Zone.BATTLEFIELD, playerA, "Valakut, the Molten Pinnacle");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 10);
addCard(Zone.HAND, playerA, "Scapeshift");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift");
setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest");
addTarget(playerA, "Mountain^Mountain^Mountain^Mountain^Mountain^Mountain");
setChoice(playerA, "Whenever", 5); // order triggers
setChoice(playerA, true, 6); // yes to deal damage
setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest"); // to sac
addTarget(playerA, "Mountain^Mountain^Mountain^Mountain^Mountain^Mountain"); // to search
setChoice(playerA, "Whenever a Mountain", 6 - 1); // x6 triggers from valakut
addTarget(playerA, playerB, 6); // to deal damage
setChoice(playerA, true, 6); // yes to deal damage
setStrictChooseMode(true);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
@ -81,7 +83,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Mountain", 6);
assertLife(playerA, 20);
assertLife(playerB, 2); // 6 * 3 damage = 18
assertLife(playerB, 20 - 18); // 6 * 3 damage = 18
}

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/**
@ -26,9 +27,10 @@ public class ImproviseTest extends CardTestPlayerBaseWithAIHelps {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bastion Inventor");
setChoice(playerA, "Blue", 4); // pay 1-4
// improvise pay by one card
// improvise pay by one card (after 2025 addTarget improve - Improvise can be pay by multiple addTarget)
setChoice(playerA, "Improvise");
addTarget(playerA, "Alpha Myr"); // pay 5 as improvise
addTarget(playerA, TestPlayer.TARGET_SKIP); // choose only 1 of 2 possible permanent
setChoice(playerA, "Improvise");
addTarget(playerA, "Alpha Myr"); // pay 6 as improvise

View file

@ -6,6 +6,7 @@ import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -33,6 +34,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive");
addTarget(playerA, "Balduvian Bears");
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// after
@ -73,6 +75,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive");
setChoice(playerB, true); // continue
addTarget(playerB, "Balduvian Bears"); // player B must take control for searching
addTarget(playerB, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// after
@ -116,6 +119,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
setChoice(playerA, true); // yes, try to cast a creature card from lib
setChoice(playerA, "Panglacial Wurm"); // try to cast
addTarget(playerA, "Balduvian Bears"); // choice for searching
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// after
@ -170,6 +174,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
setChoice(playerB, true); // yes, try to cast a creature card from lib
setChoice(playerB, "Panglacial Wurm"); // try to cast
addTarget(playerB, "Balduvian Bears"); // choice for searching
addTarget(playerB, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// after
@ -230,6 +235,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive");
setChoice(playerB, true); // continue after new control
addTarget(playerB, "Balduvian Bears");
addTarget(playerB, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkGraveyardCount("after grave a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0);
checkGraveyardCount("after grave b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0);

View file

@ -15,6 +15,7 @@ import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import java.util.Arrays;
@ -509,6 +510,7 @@ public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fireball");
setChoice(playerA, "X=2");
addTarget(playerA, "Arbor Elf^Arbor Elf");
addTarget(playerA, TestPlayer.TARGET_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -10,6 +10,7 @@ import mage.counters.CounterType;
import mage.game.stack.StackObject;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -280,6 +281,7 @@ public class CollectEvidenceTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, monitor);
setChoice(playerA, true);
setChoice(playerA, giant);
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.cost.additional;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -22,6 +23,7 @@ public class RemoveCounterCostTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, Remove two +1/+1 counters");
setChoice(playerA, "Novijen Sages"); // counters to remove
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setChoice(playerA, "X=2"); // counters to remove
setStrictChooseMode(true);

View file

@ -29,6 +29,7 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vedalken Mastermind");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -52,6 +53,7 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase {
activateManaAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
@ -71,19 +73,26 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase {
// If Gemstone Caverns is in your opening hand and you're not playing first, you may begin the game with Gemstone Caverns on the battlefield with a luck counter on it. If you do, exile a card from your hand.
// {T}: Add {C}. If Gemstone Caverns has a luck counter on it, instead add one mana of any color.
addCard(Zone.HAND, playerB, "Gemstone Caverns", 1);
addCard(Zone.HAND, playerB, "Swamp", 1);
addCard(Zone.HAND, playerB, "Silvercoat Lion", 2);
// pay and put Gemstone to battlefield on starting
setChoice(playerB, true);
setChoice(playerB, "Swamp");
activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add");
setChoice(playerB, "White");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Silvercoat Lion");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerB, "Gemstone Caverns", 1);
assertCounterCount("Gemstone Caverns", CounterType.LUCK, 1);
assertPermanentCount(playerB, "Silvercoat Lion", 1);
assertExileCount("Silvercoat Lion", 1);
assertExileCount("Swamp", 1);
assertTapped("Gemstone Caverns", true);
}
@ -99,12 +108,19 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase {
// If Gemstone Caverns is in your opening hand and you're not playing first, you may begin the game with Gemstone Caverns on the battlefield with a luck counter on it. If you do, exile a card from your hand.
// {T}: Add {C}. If Gemstone Caverns has a luck counter on it, instead add one mana of any color.
addCard(Zone.HAND, playerB, "Gemstone Caverns", 1);
addCard(Zone.HAND, playerB, "Swamp", 1);
addCard(Zone.HAND, playerB, "Silvercoat Lion", 2);
activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add");
// pay and put Gemstone to battlefield on starting
setChoice(playerB, true);
setChoice(playerB, "Swamp");
activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{T}: Add {C}");
setChoice(playerB, "White");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Vorinclex, Voice of Hunger");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
@ -112,7 +128,6 @@ public class AddManaOfAnyTypeProducedTest extends CardTestPlayerBase {
assertCounterCount("Gemstone Caverns", CounterType.LUCK, 1);
assertPermanentCount(playerB, "Vorinclex, Voice of Hunger", 1);
assertTapped("Gemstone Caverns", true);
}
private static final String kinnan = "Kinnan, Bonder Prodigy";

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.afc;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestCommander4Players;
@ -313,6 +314,7 @@ public class ShareTheSpoilsTest extends CardTestCommander4Players {
// Cast an adventure card from hand
castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Dizzying Swoop");
addTarget(playerA, "Prosper, Tome-Bound");
addTarget(playerA, TestPlayer.TARGET_SKIP); // tap 1 of 2 targets
waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN);
// Make sure the creature card can't be played from exile since there isn't the {W}{W} for it

View file

@ -169,15 +169,25 @@ public class ApproachOfTheSecondSunTest extends CardTestPlayerBase {
@Test
public void testCastFromGraveyard() {
removeAllCardsFromLibrary(playerA);
addCard(Zone.LIBRARY, playerA, "Plains", 6);
addCard(Zone.HAND, playerA, "Approach of the Second Sun", 1);
addCard(Zone.GRAVEYARD, playerA, "Approach of the Second Sun", 2);
addCard(Zone.HAND, playerA, "Finale of Promise", 2);
addCard(Zone.BATTLEFIELD, playerA, "Mystic Monastery", 25);
//
// If this spell was cast from your hand and you've cast another spell named Approach of the Second Sun this game,
// you win the game. Otherwise, put Approach of the Second Sun into its owner's library seventh from the top
// and you gain 7 life.
addCard(Zone.HAND, playerA, "Approach of the Second Sun", 1); // {6}{W}
addCard(Zone.GRAVEYARD, playerA, "Approach of the Second Sun", 2); // {6}{W}
//
// You may cast up to one target instant card and/or up to one target sorcery card from your graveyard each
// with mana value X or less without paying their mana costs. If a spell cast this way would be put into
// your graveyard, exile it instead. If X is 10 or more, copy each of those spells twice. You may choose
// new targets for the copies.
addCard(Zone.HAND, playerA, "Finale of Promise", 2); // {X}{R}{R}
//
addCard(Zone.BATTLEFIELD, playerA, "Mystic Monastery", 25); // for mana
// first may have been cast from anywhere.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Finale of Promise", true);
// You may cast up to one target instant card and/or up to one target sorcery card from your graveyard each with mana value X or less without paying their mana costs. If a spell cast this way would be put into your graveyard, exile it instead. If X is 10 or more, copy each of those spells twice. You may choose new targets for the copies.
setChoice(playerA, "X=7"); // each with mana value X or less
setChoice(playerA, "Yes"); // You may cast
addTarget(playerA, TARGET_SKIP); // up to one target instant card

View file

@ -24,6 +24,7 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase {
addCard(Zone.GRAVEYARD, playerA, maaka, 2);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard");
setChoice(playerA, mountain); // discard cost
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, maaka);
@ -33,8 +34,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase {
attack(1, playerA, maaka);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, maaka, 1);
@ -52,7 +53,10 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase {
addCard(Zone.GRAVEYARD, playerA, khenra);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard");
setChoice(playerA, true);
setChoice(playerA, mountain); // discard cost
setChoice(playerA, true); // use copy
setChoice(playerA, "0"); // for casting main spell - x2 permitting object
setChoice(playerA, "0"); // for casting copied spell - x2 permitting object
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, maaka, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, khenra);
@ -60,8 +64,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase {
attack(1, playerA, maaka);
attack(1, playerA, khenra);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, maaka, 1);
@ -88,8 +92,8 @@ public class ChainerNightmareAdeptTest extends CardTestPlayerBase {
attack(1, playerA, maaka);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, maaka, 1);

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.clb;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -29,6 +30,7 @@ public class BabaLysagaNightWitchTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.UPKEEP, playerA, "{1}: {this} becomes a 2/2 Assembly-Worker artifact creature until end of turn. It's still a land.");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three permanents: If there ");
setChoice(playerA, "Mishra's Factory");
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
@ -48,6 +50,7 @@ public class BabaLysagaNightWitchTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three permanents: If there ");
setChoice(playerA, "Mishra's Factory");
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();

View file

@ -5,6 +5,7 @@ import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -45,6 +46,7 @@ public class MuYanlingWindRiderTest extends CardTestPlayerBase {
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Crew 1");
setChoice(playerA, "Memnite");
setChoice(playerA, TestPlayer.CHOICE_SKIP);
attack(3, playerA, "Vehicle Token", playerB);
attack(3, playerA, muyanling, playerB);

View file

@ -4,6 +4,7 @@ package org.mage.test.cards.single.emn;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -77,6 +78,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two");
addTarget(playerA, "Bronze Sable");
addTarget(playerA, TestPlayer.TARGET_SKIP);
attack(1, playerA, "Bronze Sable");
@ -135,6 +137,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two");
addTarget(playerA, "Sylvan Advocate");
addTarget(playerA, TestPlayer.TARGET_SKIP);
attack(1, playerA, "Sylvan Advocate");
attack(2, playerB, "Memnite");
@ -167,6 +170,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo, Field Researcher", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two");
addTarget(playerA, "Sylvan Advocate");
addTarget(playerA, TestPlayer.TARGET_SKIP);
attack(1, playerA, "Sylvan Advocate");
@ -236,6 +240,7 @@ public class TamiyoFieldResearcherTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo, Field Researcher", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Choose up to two");
addTarget(playerA, "Bronze Sable");
addTarget(playerA, TestPlayer.TARGET_SKIP);
attack(2, playerB, "Bronze Sable");

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.grn;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -31,6 +32,7 @@ public class WandOfVertebraeTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}");
addTarget(playerA, lavaCoil);
addTarget(playerA, TestPlayer.TARGET_SKIP); // must choose 1 of 5
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.j22;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -80,13 +81,17 @@ public class AgrusKosEternalSoldierTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Smoldering Werewolf");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Smoldering Werewolf");
setChoice(playerA, "When {this} enters, it deals");
addTarget(playerA, agrus);
addTarget(playerA, agrus);
setChoice(playerB, true); // gain life
setChoice(playerB, "Whenever {this} becomes");
setChoice(playerB, true); // pay to copy
setChoice(playerB, true); // pay to copy
setChoice(playerB, true); // gain life on cast
//
setChoice(playerA, "When {this} enters, it deals"); // x2 triggers from Panharmonicon
addTarget(playerA, agrus); // x1 trigger
addTarget(playerA, TestPlayer.TARGET_SKIP); // x1 trigger
addTarget(playerA, agrus); // x2 trigger
addTarget(playerA, TestPlayer.TARGET_SKIP); // x2 trigger
//
setChoice(playerB, "Whenever {this} becomes"); // x2 triggers from Panharmonicon
setChoice(playerB, true); // x1 pay to copy
setChoice(playerB, true); // x2 pay to copy
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.m21;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class AlpineHoundmasterTest extends CardTestPlayerBase {
@ -22,6 +23,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster");
setChoice(playerA, true);
addTarget(playerA, "Alpine Watchdog");
addTarget(playerA, TestPlayer.TARGET_SKIP); // only single card
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -44,6 +46,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster");
setChoice(playerA, true);
addTarget(playerA, "Igneous Cur");
addTarget(playerA, TestPlayer.TARGET_SKIP); // only single card
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -54,7 +57,7 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase {
}
@Test
public void searchBoth() {
public void searchBoth_TestFramework_AddTargetsAsSingle() {
// When Alpine Houndmaster enters the battlefield, you may search your library for a card named
// Alpine Watchdog and/or a card named Igneous Cur, reveal them, put them into your hand, then shuffle your library.
addCard(Zone.HAND, playerA, "Alpine Houndmaster", 1);
@ -72,7 +75,30 @@ public class AlpineHoundmasterTest extends CardTestPlayerBase {
execute();
assertHandCount(playerA, 2);
}
@Test
public void searchBoth_TestFramework_AddTargetsAsMultiple() {
// test framework must support both
// When Alpine Houndmaster enters the battlefield, you may search your library for a card named
// Alpine Watchdog and/or a card named Igneous Cur, reveal them, put them into your hand, then shuffle your library.
addCard(Zone.HAND, playerA, "Alpine Houndmaster", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerA, "Plains");
addCard(Zone.LIBRARY, playerA, "Alpine Watchdog");
addCard(Zone.LIBRARY, playerA, "Igneous Cur");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Alpine Houndmaster");
setChoice(playerA, true);
addTarget(playerA, "Igneous Cur");
addTarget(playerA, "Alpine Watchdog");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertHandCount(playerA, 2);
}

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.single.mh3;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/**
@ -72,6 +73,7 @@ public class NethergoyfTest extends CardTestPlayerBaseWithAIHelps {
checkPlayableAbility("can escape", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + nethergoyf + " with Escape", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nethergoyf + " with Escape");
setChoice(playerA, "Memnite^Memnite^Memnite^Memnite^Bitterblossom"); // cards exiled for escape cost: Exile all the Memnite but one.
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -4,6 +4,7 @@ import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -39,6 +40,7 @@ public class SuppressionRayTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suppression Ray", playerB);
setChoiceAmount(playerA, 3); // decide to pay 3 energy
setChoice(playerA, "Zodiac Pig^Zodiac Rabbit"); // put stun on those 2 creatures
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -2,8 +2,10 @@ package org.mage.test.cards.single.mkm;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.target.TargetPlayer;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class CovetedFalconTest extends CardTestPlayerBase {
@ -31,6 +33,7 @@ public class CovetedFalconTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up.");
addTarget(playerA, playerB);
addTarget(playerA, "Grizzly Bears");
addTarget(playerA, TestPlayer.TARGET_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -57,6 +60,7 @@ public class CovetedFalconTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up.");
addTarget(playerA, playerB);
addTarget(playerA, "Grizzly Bears");
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Turn Against", "Grizzly Bears");
@ -82,6 +86,7 @@ public class CovetedFalconTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up.");
addTarget(playerA, playerB);
addTarget(playerA, "Putrid Goblin");
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Putrid Goblin");
@ -111,6 +116,7 @@ public class CovetedFalconTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up.");
addTarget(playerA, playerB);
addTarget(playerA, "Treacherous Pit-Dweller");
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Treacherous Pit-Dweller");
@ -151,6 +157,7 @@ public class CovetedFalconTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up.");
addTarget(playerA, playerB);
addTarget(playerA, "Darksteel Relic^Grizzly Bears");
addTarget(playerA, TestPlayer.TARGET_SKIP);
castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Murder", "Guardian Beast");

View file

@ -5,6 +5,7 @@ import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -65,6 +66,7 @@ public class TenthDistrictHeroTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}");
setChoice(playerA, giant);
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
@ -92,6 +94,7 @@ public class TenthDistrictHeroTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}");
setChoice(playerA, giant);
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);

View file

@ -4,6 +4,7 @@ import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -37,6 +38,7 @@ public class TheWiseMothmanTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}");
addTarget(playerA, mothman + "^Grizzly Bears"); // up to three targets => choosing 2
addTarget(playerA, TestPlayer.TARGET_SKIP);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -1,5 +1,4 @@
package org.mage.test.cards.asthough;
package org.mage.test.cards.targets;
import mage.constants.PhaseStep;
import mage.constants.Zone;
@ -7,13 +6,11 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
* @author LevelX2, JayDi85
*/
public class CastAsInstantTest extends CardTestPlayerBase {
public class AutoChooseTargetsAndCastAsInstantTest extends CardTestPlayerBase {
@Test
public void testEffectOnlyForOneTurn() {
private void run_WithAutoSelection(int selectedTargets, int possibleTargets) {
addCard(Zone.BATTLEFIELD, playerB, "Island");
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4);
// The next sorcery card you cast this turn can be cast as though it had flash.
@ -23,22 +20,45 @@ public class CastAsInstantTest extends CardTestPlayerBase {
// Target opponent exiles two cards from their hand and loses 2 life.
addCard(Zone.HAND, playerB, "Witness the End"); // {3}{B}
addCard(Zone.HAND, playerA, "Silvercoat Lion", 2);
addCard(Zone.HAND, playerA, "Silvercoat Lion", possibleTargets);
castSpell(1, PhaseStep.UPKEEP, playerB, "Quicken", true);
castSpell(1, PhaseStep.UPKEEP, playerB, "Witness the End", playerA);
// it uses auto-choose logic inside, so disable strict mode
// logic for possible targets with min/max = 2:
// 0, 1, 2 - auto-choose all possible targets
// 3+ - AI choose best targets
setStrictChooseMode(false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertGraveyardCount(playerB, "Quicken", 1);
assertGraveyardCount(playerB, "Witness the End", 1);
assertExileCount("Silvercoat Lion", 2);
assertExileCount("Silvercoat Lion", selectedTargets);
assertLife(playerA, 18);
assertLife(playerB, 20);
}
@Test
public void test_AutoChoose_0_of_0() {
run_WithAutoSelection(0, 0);
}
@Test
public void test_AutoChoose_1_of_1() {
run_WithAutoSelection(1, 1);
}
@Test
public void test_AutoChoose_2_of_2() {
run_WithAutoSelection(2, 2);
}
@Test
public void test_AutoChoose_2_of_5() {
run_WithAutoSelection(2, 5);
}
}

View file

@ -0,0 +1,258 @@
package org.mage.test.cards.targets;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapAttachedCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.common.GainLifeEffect;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInHand;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
/**
* Helper class for target selection tests in diff use cases like selection on targets declare or on resolve
*
* @author JayDi85
*/
public class TargetsSelectionBaseTest extends CardTestPlayerBaseWithAIHelps {
static final boolean DEBUG_ENABLE_DETAIL_LOGS = true;
protected void run_PlayerChooseTarget_OnActivate(int chooseCardsCount, int availableCardsCount) {
// custom effect - exile and gain life due selected targets
int minTarget = 0;
int maxTarget = 3;
String startingText = "exile and gain";
Ability ability = new SimpleActivatedAbility(
new ExileTargetEffect().setText("exile"),
new ManaCostsImpl<>("")
);
ability.addEffect(new GainLifeEffect(TotalTargetsValue.instance).concatBy("and").setText("gain life"));
ability.addTarget(new TargetCardInHand(minTarget, maxTarget, StaticFilters.FILTER_CARD).withNotTarget(false));
addCustomCardWithAbility("test choice", playerA, ability);
addCard(Zone.HAND, playerA, "Swamp", availableCardsCount);
checkHandCardCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", availableCardsCount);
checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", 0);
if (availableCardsCount > 0) {
checkPlayableAbility("can activate on non-zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText);
} else {
checkPlayableAbility("can't activate on zero targets", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, false);
}
if (chooseCardsCount > 0) {
// need selection
List<String> targetCards = new ArrayList<>();
IntStream.rangeClosed(1, chooseCardsCount).forEach(x -> {
targetCards.add("Swamp");
});
addTarget(playerA, String.join("^", targetCards));
// end selection:
// - x of 0 - no
// - 1 of 3 - yes
// - 2 of 3 - yes
// - 3 of 3 - no, it's auto-finish on last select
// - 3 of 5 - no, it's auto-finish on last select
if (chooseCardsCount < maxTarget) {
addTarget(playerA, TestPlayer.TARGET_SKIP);
}
} else {
// need skip
// on 0 cards there are not valid targets, so no any dialogs
if (availableCardsCount > 0) {
addTarget(playerA, TestPlayer.TARGET_SKIP);
}
}
if (DEBUG_ENABLE_DETAIL_LOGS) {
System.out.println("planning actions:");
playerA.getActions().forEach(System.out::println);
System.out.println("planning targets:");
playerA.getTargets().forEach(System.out::println);
System.out.println("planning choices:");
playerA.getChoices().forEach(System.out::println);
}
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertExileCount(playerA, "Swamp", chooseCardsCount);
assertLife(playerA, 20 + chooseCardsCount);
}
protected void run_PlayerChoose_OnResolve(int chooseCardsCount, int availableCardsCount) {
// custom effect - select, exile and gain life
int minTarget = 0;
int maxTarget = 3;
String startingText = "select, exile and gain life";
Ability ability = new SimpleActivatedAbility(
new SelectExileAndGainLifeCustomEffect(minTarget, maxTarget, Outcome.Benefit),
new ManaCostsImpl<>("")
);
addCustomCardWithAbility("test choice", playerA, ability);
addCard(Zone.HAND, playerA, "Swamp", availableCardsCount);
checkHandCardCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", availableCardsCount);
checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", 0);
checkPlayableAbility("can activate any time (even with zero cards)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText);
if (chooseCardsCount > 0) {
// need selection
List<String> targetCards = new ArrayList<>();
IntStream.rangeClosed(1, chooseCardsCount).forEach(x -> {
targetCards.add("Swamp");
});
setChoice(playerA, String.join("^", targetCards));
} else {
// need skip
// on 0 cards there are must be dialog with done button anyway
// end selection:
// - x of 0 - yes
// - 1 of 3 - yes
// - 2 of 3 - yes
// - 3 of 3 - no, it's auto-finish on last select
// - 3 of 5 - no, it's auto-finish on last select
if (chooseCardsCount < maxTarget) {
setChoice(playerA, TestPlayer.CHOICE_SKIP);
}
}
if (DEBUG_ENABLE_DETAIL_LOGS) {
System.out.println("planning actions:");
playerA.getActions().forEach(System.out::println);
System.out.println("planning targets:");
playerA.getTargets().forEach(System.out::println);
System.out.println("planning choices:");
playerA.getChoices().forEach(System.out::println);
}
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertExileCount(playerA, "Swamp", chooseCardsCount);
assertLife(playerA, 20 + chooseCardsCount);
}
protected void run_PlayerChoose_OnResolve_AI(Outcome outcome, int minTargets, int maxTargets, int aiMustChooseCardsCount, int availableCardsCount) {
// custom effect - select, exile and gain life
String startingText = "{T}: Select, exile and gain life";
Ability ability = new SimpleActivatedAbility(
new SelectExileAndGainLifeCustomEffect(minTargets, maxTargets, outcome),
new ManaCostsImpl<>("")
);
ability.addCost(new TapSourceCost());
addCustomCardWithAbility("test choice", playerA, ability);
addCard(Zone.HAND, playerA, "Swamp", availableCardsCount);
addCard(Zone.HAND, playerA, "Forest", 1);
// Forest play is workaround to disable lands play in ai priority
// {T} cost is workaround to disable multiple calls of the ability
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest");
checkPlayableAbility("can activate any time (even with zero cards)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, startingText, true);
// AI must see bad effect and select only halves of the max targets, e.g. 1 of 3
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertExileCount(playerA, "Swamp", aiMustChooseCardsCount);
assertLife(playerA, 20 + aiMustChooseCardsCount);
}
}
enum TotalTargetsValue implements DynamicValue {
instance;
@Override
public TotalTargetsValue copy() {
return instance;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return effect.getTargetPointer().getTargets(game, sourceAbility).size();
}
@Override
public String getMessage() {
return "total targets";
}
@Override
public String toString() {
return "X";
}
}
class SelectExileAndGainLifeCustomEffect extends OneShotEffect {
private final int minTargets;
private final int maxTargets;
SelectExileAndGainLifeCustomEffect(int minTargets, int maxTargets, Outcome outcome) {
super(outcome);
this.minTargets = minTargets;
this.maxTargets = maxTargets;
staticText = "select, exile and gain life";
}
private SelectExileAndGainLifeCustomEffect(final SelectExileAndGainLifeCustomEffect effect) {
super(effect);
this.minTargets = effect.minTargets;
this.maxTargets = effect.maxTargets;
}
@Override
public SelectExileAndGainLifeCustomEffect copy() {
return new SelectExileAndGainLifeCustomEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
Target target = new TargetCardInHand(this.minTargets, this.maxTargets, StaticFilters.FILTER_CARD).withNotTarget(true);
if (!player.choose(outcome, target, source, game)) {
return false;
}
player.moveCardsToExile(new CardsImpl(target.getTargets()).getCards(game), source, game, false, source.getSourceId(), player.getLogName());
player.gainLife(target.getTargets().size(), game, source);
return true;
}
}

View file

@ -0,0 +1,89 @@
package org.mage.test.cards.targets;
import org.junit.Test;
/**
* Testing targets selection on activate/cast (player.chooseTarget)
*
* @author JayDi85
*/
public class TargetsSelectionOnActivateTest extends TargetsSelectionBaseTest {
// no selects
@Test
public void test_OnActivate_0_of_0() {
run_PlayerChooseTarget_OnActivate(0, 0);
}
@Test
public void test_OnActivate_0_of_1() {
run_PlayerChooseTarget_OnActivate(0, 1);
}
@Test
public void test_OnActivate_0_of_2() {
run_PlayerChooseTarget_OnActivate(0, 2);
}
@Test
public void test_OnActivate_0_of_3() {
run_PlayerChooseTarget_OnActivate(0, 3);
}
@Test
public void test_OnActivate_0_of_10() {
run_PlayerChooseTarget_OnActivate(0, 10);
}
// 1 select
@Test
public void test_OnActivate_1_of_1() {
run_PlayerChooseTarget_OnActivate(1, 1);
}
@Test
public void test_OnActivate_1_of_2() {
run_PlayerChooseTarget_OnActivate(1, 2);
}
@Test
public void test_OnActivate_1_of_3() {
run_PlayerChooseTarget_OnActivate(1, 3);
}
@Test
public void test_OnActivate_1_of_10() {
run_PlayerChooseTarget_OnActivate(1, 10);
}
// 2 selects
@Test
public void test_OnActivate_2_of_2() {
run_PlayerChooseTarget_OnActivate(2, 2);
}
@Test
public void test_OnActivate_2_of_3() {
run_PlayerChooseTarget_OnActivate(2, 3);
}
@Test
public void test_OnActivate_2_of_10() {
run_PlayerChooseTarget_OnActivate(2, 10);
}
// 3 selects
@Test
public void test_OnActivate_3_of_3() {
run_PlayerChooseTarget_OnActivate(3, 3);
}
@Test
public void test_OnActivate_3_of_10() {
run_PlayerChooseTarget_OnActivate(3, 10);
}
}

View file

@ -0,0 +1,66 @@
package org.mage.test.cards.targets;
import mage.constants.Outcome;
import org.junit.Test;
/**
* Testing targets selection on resolve (player.choose) for AI
* <p>
* AI must use logic like:
* - for good effects - choose as much as possible targets
* - for bad effects - choose as much as lower targets
*
* @author JayDi85
*/
public class TargetsSelectionOnResolveAITest extends TargetsSelectionBaseTest {
@Test
public void test_OnResolve_Good_0_of_0() {
run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 0, 0);
}
@Test
public void test_OnResolve_Good_1_of_1() {
run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 1, 1);
}
@Test
public void test_OnResolve_Good_2_of_2() {
run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 2, 2);
}
@Test
public void test_OnResolve_Good_3_of_3() {
run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 3, 3);
}
@Test
public void test_OnResolve_Good_3_of_10() {
run_PlayerChoose_OnResolve_AI(Outcome.Benefit, 0, 3, 3, 10);
}
@Test
public void test_OnResolve_Bad_0_of_0() {
run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 0);
}
@Test
public void test_OnResolve_Bad_0_of_1() {
run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 1);
}
@Test
public void test_OnResolve_Bad_0_of_2() {
run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 2);
}
@Test
public void test_OnResolve_Bad_0_of_3() {
run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 3);
}
@Test
public void test_OnResolve_Bad_0_of_10() {
run_PlayerChoose_OnResolve_AI(Outcome.Detriment, 0, 3, 0, 10);
}
}

View file

@ -0,0 +1,91 @@
package org.mage.test.cards.targets;
import org.junit.Test;
/**
* Testing targets selection on resolve (player.choose)
* <p>
* Player can use any logic and choose any number of targets
*
* @author JayDi85
*/
public class TargetsSelectionOnResolveTest extends TargetsSelectionBaseTest {
// no selects
@Test
public void test_OnActivate_0_of_0() {
run_PlayerChoose_OnResolve(0, 0);
}
@Test
public void test_OnActivate_0_of_1() {
run_PlayerChoose_OnResolve(0, 1);
}
@Test
public void test_OnActivate_0_of_2() {
run_PlayerChoose_OnResolve(0, 2);
}
@Test
public void test_OnActivate_0_of_3() {
run_PlayerChoose_OnResolve(0, 3);
}
@Test
public void test_OnActivate_0_of_10() {
run_PlayerChoose_OnResolve(0, 10);
}
// 1 select
@Test
public void test_OnActivate_1_of_1() {
run_PlayerChoose_OnResolve(1, 1);
}
@Test
public void test_OnActivate_1_of_2() {
run_PlayerChoose_OnResolve(1, 2);
}
@Test
public void test_OnActivate_1_of_3() {
run_PlayerChoose_OnResolve(1, 3);
}
@Test
public void test_OnActivate_1_of_10() {
run_PlayerChoose_OnResolve(1, 10);
}
// 2 selects
@Test
public void test_OnActivate_2_of_2() {
run_PlayerChoose_OnResolve(2, 2);
}
@Test
public void test_OnActivate_2_of_3() {
run_PlayerChoose_OnResolve(2, 3);
}
@Test
public void test_OnActivate_2_of_10() {
run_PlayerChoose_OnResolve(2, 10);
}
// 3 selects
@Test
public void test_OnActivate_3_of_3() {
run_PlayerChoose_OnResolve(3, 3);
}
@Test
public void test_OnActivate_3_of_10() {
run_PlayerChoose_OnResolve(3, 10);
}
}

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.triggers;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -23,6 +24,7 @@ public class EnterLeaveBattlefieldExileTargetTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angel of Serenity");
addTarget(playerA, "Silvercoat Lion^Pillarfield Ox");
addTarget(playerA, TestPlayer.TARGET_SKIP);
setChoice(playerA, true);
setStrictChooseMode(true);

View file

@ -7,7 +7,6 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class WorldgorgerDragonTest extends CardTestPlayerBase {
@ -83,62 +82,48 @@ public class WorldgorgerDragonTest extends CardTestPlayerBase {
// When Staunch Defenders enters the battlefield, you gain 4 life.
addCard(Zone.BATTLEFIELD, playerA, "Staunch Defenders", 1);
// 1 cast and resolve
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Animate Dead", "Worldgorger Dragon");
setChoice(playerA, "Worldgorger Dragon"); // attach
setChoice(playerA, "When {this} enters, if it's"); // x2 triggers (gain life from Staunch Defenders and etb from Animate Dead)
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
// 2 etb and attach
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
// 3 etb and attach
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
// 4 etb and attach
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
// 5 etb and attach
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, false); // no draws on infinite loop
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
// 6 etb and attach
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
setChoice(playerA, false);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3);
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 3 - 1);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
setChoice(playerA, "Worldgorger Dragon");
setChoice(playerA, "When {this} enters, if it's");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volcanic Geyser", playerB, 22);
// cast spell and stop infinite loop after 20+ mana in pool
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Volcanic Geyser", playerB, 20);
setChoice(playerA, "X=20");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 44);
assertLife(playerA, 20 + 5 * 4);
assertLife(playerB, 0);
assertGraveyardCount(playerA, "Volcanic Geyser", 1);
@ -154,12 +139,11 @@ public class WorldgorgerDragonTest extends CardTestPlayerBase {
* you choose to skip or pick a different creature, it always returns the
* first creature you picked. Kind of hard to explain, but here's how to
* reproduce:
*
* <p>
* 1) Cast Animate Dead, targeting Worldgorger Dragon 2) Worldgorger Dragon
* will exile Animate Dead, killing the dragon and returning the permanents
* 3) Select Worldgorger again 4) Step 2 repeats 5) Attempt to select a
* different creature. Worldgorger Dragon is returned instead.
*
*/
@Test
public void testWithAnimateDeadDifferentTargets() {

View file

@ -3,6 +3,7 @@ package org.mage.test.cards.triggers.dies;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -159,6 +160,7 @@ public class VengefulTownsfolkTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three");
setChoice(playerA, "Angel of the God-Pharaoh"); // sac cost
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
@ -183,6 +185,7 @@ public class VengefulTownsfolkTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice up to three");
setChoice(playerA, "Grizzly Bears^Angel of the God-Pharaoh"); // sac cost
setChoice(playerA, TestPlayer.CHOICE_SKIP);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -70,7 +70,7 @@ public class TestPlayer implements Player {
private static final Logger LOGGER = Logger.getLogger(TestPlayer.class);
private static final int takeMaxTargetsPerChoose = Integer.MAX_VALUE; // TODO: set 1, fix broken tests and replace all "for (String targetDefinition" by targets.get(0)
private static final int takeMaxTargetsPerChoose = Integer.MAX_VALUE; // TODO: set 1 here, fix broken tests and replace all "for (String targetDefinition" by targets.get(0)
public static final String TARGET_SKIP = "[target_skip]"; // stop/skip targeting
public static final String CHOICE_SKIP = "[choice_skip]"; // stop/skip choice
@ -113,7 +113,7 @@ public class TestPlayer implements Player {
// (example: card call TestPlayer's choice, but it uses another choices, see docs in TestComputerPlayer)
private boolean strictChooseMode = false;
private String[] groupsForTargetHandling = null;
private String[] groupsForTargetHandling = null; // predefined targets list from cast/activate command
// Tracks the initial turns (turn 0s) both players are given at the start of the game.
// Before actual turns start. Needed for checking attacker/blocker legality in the tests
@ -516,7 +516,7 @@ public class TestPlayer implements Player {
}
}
for (UUID id : currentTarget.possibleTargets(ability.getControllerId(), ability, game)) {
if (!currentTarget.getTargets().contains(id)) {
if (!currentTarget.contains(id)) {
MageObject object = game.getObject(id);
if (object == null) {
@ -594,7 +594,7 @@ public class TestPlayer implements Player {
}
// fake test ability for triggers and events
Ability source = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
Ability source = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("fake ability"));
source.setControllerId(this.getId());
int numberOfActions = actions.size();
@ -2099,8 +2099,18 @@ public class TestPlayer implements Player {
return "Ability: null";
}
private String getInfo(Target o, Game game) {
return "Target: " + (o != null ? o.getClass().getSimpleName() + ": " + o.getMessage(game) : "null");
private String getInfo(Target target, Ability source, Game game) {
if (target == null) {
return "Target: null";
}
UUID abilityControllerId = getId();
if (target.getTargetController() != null && target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
}
Set<UUID> possibleTargets = target.possibleTargets(abilityControllerId, source, game);
return "Target: selected " + target.getSize() + ", possible " + possibleTargets.size()
+ ", " + target.getClass().getSimpleName() + ": " + target.getMessage(game);
}
private void assertAliasSupportInChoices(boolean methodSupportAliases) {
@ -2211,7 +2221,7 @@ public class TestPlayer implements Player {
// skip choices
if (possibleChoice.equals(CHOICE_SKIP)) {
choices.remove(0);
return true;
return false; // false - stop to choose
}
if (choice.setChoiceByAnswers(choices, true)) {
@ -2264,27 +2274,28 @@ public class TestPlayer implements Player {
abilityControllerId = target.getAbilityController();
}
// TODO: warning, some cards call player.choose methods instead target.choose, see #8254
// most use cases - discard and other cost with choice like that method
// must migrate all choices.remove(xxx) to choices.remove(0), takeMaxTargetsPerChoose can help to find it
// ignore player select
if (target.getMessage(game).equals("Select a starting player")) {
return computerPlayer.choose(outcome, target, source, game, options);
}
boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
assertAliasSupportInChoices(true);
if (!choices.isEmpty()) {
// skip choices
if (choices.get(0).equals(CHOICE_SKIP)) {
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
Assert.assertTrue("found skip choice, but it require more choices, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
choices.remove(0);
return true;
return false; // false - stop to choose
}
List<Integer> usedChoices = new ArrayList<>();
List<UUID> usedTargets = new ArrayList<>();
// TODO: Allow to choose a player with TargetPermanentOrPlayer
if ((target.getOriginalTarget() instanceof TargetPermanent)
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet
@ -2294,10 +2305,13 @@ public class TestPlayer implements Player {
} else {
filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter();
}
while (!choices.isEmpty()) {
while (!choices.isEmpty()) { // TODO: remove cycle after main commits
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
return false; // stop dialog
}
String choiceRecord = choices.get(0);
String[] targetList = choiceRecord.split("\\^");
boolean targetFound = false;
isAddedSomething = false;
for (String targetName : targetList) {
boolean originOnly = false;
boolean copyOnly = false;
@ -2312,14 +2326,14 @@ public class TestPlayer implements Player {
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filterPermanent, abilityControllerId, source, game)) {
if (target.getTargets().contains(permanent.getId())) {
if (target.contains(permanent.getId())) {
continue;
}
if (hasObjectTargetNameOrAlias(permanent, targetName)) {
if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) {
target.add(permanent.getId(), game);
targetFound = true;
isAddedSomething = true;
break;
}
}
@ -2327,7 +2341,7 @@ public class TestPlayer implements Player {
if (target.isNotTarget() || target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) {
target.add(permanent.getId(), game);
targetFound = true;
isAddedSomething = true;
break;
}
}
@ -2335,51 +2349,50 @@ public class TestPlayer implements Player {
}
}
if (!targetFound) {
//failOnLastBadChoice(game, source, target, choiceRecord, "unknown or can't target");
}
try {
if (target.isChosen(game)) {
return true;
} else {
// TODO: move check above and fix all fail tests (not after target.isChosen)
if (!targetFound) {
failOnLastBadChoice(game, source, target, choiceRecord, "selected, but not all required targets");
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return true;
}
} else {
failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command");
}
} finally {
choices.remove(0);
}
}
return isAddedSomething;
} // choices
}
if (target instanceof TargetPlayer) {
while (!choices.isEmpty()) {
while (!choices.isEmpty()) { // TODO: remove cycle after main commits
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
return false; // stop dialog
}
String choiceRecord = choices.get(0);
boolean targetFound = false;
isAddedSomething = false;
for (Player player : game.getPlayers().values()) {
if (player.getName().equals(choiceRecord)) {
if (target.canTarget(abilityControllerId, player.getId(), null, game) && !target.getTargets().contains(player.getId())) {
if (target.canTarget(abilityControllerId, player.getId(), null, game) && !target.contains(player.getId())) {
target.add(player.getId(), game);
targetFound = true;
} else {
failOnLastBadChoice(game, source, target, choiceRecord, "can't target");
isAddedSomething = true;
}
}
}
try {
if (target.isChosen(game)) {
return true;
}
if (!targetFound) {
failOnLastBadChoice(game, source, target, choiceRecord, "unknown target");
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return true;
}
} else {
failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command");
}
} finally {
choices.remove(0);
}
}
return isAddedSomething;
} // while choices
}
// TODO: add same choices fixes for other target types (one choice must uses only one time for one target)
@ -2388,102 +2401,78 @@ public class TestPlayer implements Player {
// only unique targets
//TargetCard targetFull = ((TargetCard) target);
usedChoices.clear();
usedTargets.clear();
boolean targetCompleted = false;
CheckAllChoices:
for (int choiceIndex = 0; choiceIndex < choices.size(); choiceIndex++) {
String choiceRecord = choices.get(choiceIndex);
if (targetCompleted) {
break CheckAllChoices;
for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
return false; // stop dialog
}
boolean targetFound = false;
isAddedSomething = false;
String[] possibleChoices = choiceRecord.split("\\^");
CheckOneChoice:
for (String possibleChoice : possibleChoices) {
Set<UUID> possibleCards = target.possibleTargets(abilityControllerId, source, game);
CheckTargetsList:
for (UUID targetId : possibleCards) {
MageObject targetObject = game.getCard(targetId);
if (hasObjectTargetNameOrAlias(targetObject, possibleChoice)) {
if (target.canTarget(targetObject.getId(), game)) {
// only unique targets
if (usedTargets.contains(targetObject.getId())) {
continue;
}
// OK, can use it
if (target.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) {
target.add(targetObject.getId(), game);
targetFound = true;
usedTargets.add(targetObject.getId());
// break on full targets list
if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
targetCompleted = true;
break CheckOneChoice;
}
// restart search
break CheckTargetsList;
isAddedSomething = true;
break;
}
}
}
}
if (targetFound) {
usedChoices.add(choiceIndex);
}
}
// apply only on ALL targets or revert
if (usedChoices.size() > 0) {
if (target.isChosen(game)) {
// remove all used choices
for (int i = choices.size(); i >= 0; i--) {
if (usedChoices.contains(i)) {
choices.remove(i);
try {
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return true;
}
} else {
failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command");
}
return true;
} else {
Assert.fail("Not full targets list.");
target.clearChosen();
} finally {
choices.remove(0);
}
}
return isAddedSomething;
} // for choices
}
if (target.getOriginalTarget() instanceof TargetSource) {
Set<UUID> possibleTargets;
TargetSource t = ((TargetSource) target.getOriginalTarget());
possibleTargets = t.possibleTargets(abilityControllerId, source, game);
for (String choiceRecord : choices) {
Set<UUID> possibleTargets = t.possibleTargets(abilityControllerId, source, game);
// TODO: enable choices.get first instead all
for (String choiceRecord : new ArrayList<>(choices)) { // TODO: remove cycle after main commits
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
return false; // stop dialog
}
String[] targetList = choiceRecord.split("\\^");
boolean targetFound = false;
isAddedSomething = false;
for (String targetName : targetList) {
for (UUID targetId : possibleTargets) {
MageObject targetObject = game.getObject(targetId);
if (targetObject != null) {
if (hasObjectTargetNameOrAlias(targetObject, targetName)) {
List<UUID> alreadyTargetted = target.getTargets();
if (t.canTarget(targetObject.getId(), game)) {
if (alreadyTargetted != null && !alreadyTargetted.contains(targetObject.getId())) {
target.add(targetObject.getId(), game);
choices.remove(choiceRecord);
targetFound = true;
}
if (t.canTarget(targetObject.getId(), game) && !target.contains(targetObject.getId())) {
target.add(targetObject.getId(), game);
isAddedSomething = true;
break;
}
}
}
if (targetFound) {
choices.remove(choiceRecord);
return true;
}
}
}
}
try {
if (isAddedSomething) {
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
return true;
}
} else {
failOnLastBadChoice(game, source, target, choiceRecord, "invalid target or miss skip command");
}
} finally {
choices.remove(choiceRecord);
}
return isAddedSomething;
} // for choices
}
// TODO: enable fail checks and fix tests
@ -2492,7 +2481,7 @@ public class TestPlayer implements Player {
}
}
this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, game));
this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game));
return computerPlayer.choose(outcome, target, source, game, options);
}
@ -2535,7 +2524,7 @@ public class TestPlayer implements Player {
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0);
return true;
return false; // false - stop to choose
}
Set<Zone> targetCardZonesChecked = new HashSet<>(); // control miss implementation
@ -2594,7 +2583,7 @@ public class TestPlayer implements Player {
}
for (Permanent permanent : game.getBattlefield().getActivePermanents((FilterPermanent) filter, abilityControllerId, source, game)) {
if (hasObjectTargetNameOrAlias(permanent, targetName) || (permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove exp code search?
if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) {
if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
if ((permanent.isCopy() && !originOnly) || (!permanent.isCopy() && !copyOnly)) {
target.addTarget(permanent.getId(), source, game);
targetFound = true;
@ -2624,7 +2613,7 @@ public class TestPlayer implements Player {
for (String targetName : targetList) {
for (Card card : computerPlayer.getHand().getCards(((TargetCard) target.getOriginalTarget()).getFilter(), game)) {
if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search?
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game);
targetFound = true;
break; // return to next targetName
@ -2662,7 +2651,7 @@ public class TestPlayer implements Player {
for (String targetName : targetList) {
for (Card card : game.getExile().getCards(filter, game)) {
if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search?
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game);
targetFound = true;
break; // return to next targetName
@ -2687,7 +2676,7 @@ public class TestPlayer implements Player {
for (String targetName : targetList) {
for (Card card : game.getBattlefield().getAllActivePermanents()) {
if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search?
if (targetFull.canTarget(abilityControllerId, card.getId(), source, game) && !targetFull.getTargets().contains(card.getId())) {
if (targetFull.canTarget(abilityControllerId, card.getId(), source, game) && !targetFull.contains(card.getId())) {
targetFull.add(card.getId(), game);
targetFound = true;
break; // return to next targetName
@ -2739,7 +2728,7 @@ public class TestPlayer implements Player {
Player player = game.getPlayer(playerId);
for (Card card : player.getGraveyard().getCards(targetFull.getFilter(), game)) {
if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search?
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
target.addTarget(card.getId(), source, game);
targetFound = true;
break IterateGraveyards; // return to next targetName
@ -2754,7 +2743,6 @@ public class TestPlayer implements Player {
return true;
}
}
}
// stack
@ -2769,7 +2757,7 @@ public class TestPlayer implements Player {
for (String targetName : targetList) {
for (StackObject stackObject : game.getStack()) {
if (hasObjectTargetNameOrAlias(stackObject, targetName)) {
if (target.canTarget(abilityControllerId, stackObject.getId(), source, game) && !target.getTargets().contains(stackObject.getId())) {
if (target.canTarget(abilityControllerId, stackObject.getId(), source, game) && !target.contains(stackObject.getId())) {
target.addTarget(stackObject.getId(), source, game);
targetFound = true;
break; // return to next targetName
@ -2804,24 +2792,25 @@ public class TestPlayer implements Player {
// how to fix: implement target class processing above (if it a permanent target then check "filter instanceof" code too)
if (!targets.isEmpty()) {
String message;
Set<UUID> possibleTargets = target.possibleTargets(abilityControllerId, source, game);
if (source != null) {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "\nCard: " + source.getSourceObject(game)
+ "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")"
+ "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")"
+ "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")"
+ "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets";
} else {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "\nCard: unknown source"
+ "\nAbility: unknown source"
+ "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")"
+ "\nTarget: selected " + target.getSize() + ", possible " + possibleTargets.size() + ", " + target.getClass().getSimpleName() + " (" + target.getMessage(game) + ")"
+ "\nYou must implement target class support in TestPlayer, \"filter instanceof\", or setup good targets";
}
Assert.fail(message);
}
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game));
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game));
return computerPlayer.chooseTarget(outcome, target, source, game);
}
@ -2840,7 +2829,7 @@ public class TestPlayer implements Player {
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0);
return true;
return false; // false - stop to choose
}
for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) {
String[] targetList = targetDefinition.split("\\^");
@ -2848,7 +2837,7 @@ public class TestPlayer implements Player {
for (String targetName : targetList) {
for (Card card : cards.getCards(game)) {
if (hasObjectTargetNameOrAlias(card, targetName)
&& !target.getTargets().contains(card.getId())
&& !target.contains(card.getId())
&& target.canTarget(abilityControllerId, card.getId(), source, cards, game)) {
target.addTarget(card.getId(), source, game);
targetFound = true;
@ -2867,7 +2856,7 @@ public class TestPlayer implements Player {
LOGGER.warn("Wrong target");
}
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game));
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game));
return computerPlayer.chooseTarget(outcome, cards, target, source, game);
}
@ -2930,7 +2919,7 @@ public class TestPlayer implements Player {
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
assertAliasSupportInChoices(false);
if (!choices.isEmpty()) {
for (String choice : choices) {
for (String choice : new ArrayList<>(choices)) {
if (choice.startsWith("X=")) {
int xValue = Integer.parseInt(choice.substring(2));
assertXMinMaxValue(game, ability, xValue, min, max);
@ -2941,7 +2930,7 @@ public class TestPlayer implements Player {
}
this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
return computerPlayer.announceXMana(min, max, message, game, ability);
}
@ -4276,32 +4265,40 @@ public class TestPlayer implements Player {
return choose(outcome, target, source, game, null);
}
private boolean tryToSkipSelection(List<String> selections, String selectionMark) {
if (!selections.isEmpty() && selections.get(0).equals(selectionMark)) {
selections.remove(0);
return true;
}
return false;
}
@Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
assertAliasSupportInChoices(false);
if (!choices.isEmpty()) {
// skip choices
if (choices.get(0).equals(CHOICE_SKIP)) {
choices.remove(0);
if (tryToSkipSelection(choices, CHOICE_SKIP)) {
if (cards.isEmpty()) {
// cancel button forced in GUI on no possible choices
// TODO: need research
return false;
} else {
Assert.assertTrue("found skip choice, but it require more choices, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
return true;
return false; // stop dialog
}
}
for (String choose2 : choices) {
for (String choose2 : new ArrayList<>(choices)) {
// TODO: More targetting to fix
String[] targetList = choose2.split("\\^");
boolean targetFound = false;
for (String targetName : targetList) {
for (Card card : cards.getCards(game)) {
if (target.getTargets().contains(card.getId())) {
if (target.contains(card.getId())) {
continue;
}
if (hasObjectTargetNameOrAlias(card, targetName)) {
@ -4322,7 +4319,7 @@ public class TestPlayer implements Player {
assertWrongChoiceUsage(choices.size() > 0 ? choices.get(0) : "empty list");
}
this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, game));
this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\n" + getInfo(target, source, game));
return computerPlayer.choose(outcome, cards, target, source, game);
}
@ -4344,7 +4341,7 @@ public class TestPlayer implements Player {
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0);
return false; // false in chooseTargetAmount = stop to choose
return false; // false - stop to choose
}
// only target amount needs
@ -4384,7 +4381,7 @@ public class TestPlayer implements Player {
}
if (foundTarget) {
if (!target.getTargets().contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) {
if (!target.contains(possibleTarget) && target.canTarget(possibleTarget, source, game)) {
// can select
target.addTarget(possibleTarget, targetAmount, source, game);
targets.remove(0);
@ -4395,7 +4392,7 @@ public class TestPlayer implements Player {
}
}
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, game));
this.chooseStrictModeFailed("target", game, getInfo(source, game) + "\n" + getInfo(target, source, game));
return computerPlayer.chooseTargetAmount(outcome, target, source, game);
}
@ -4744,7 +4741,7 @@ public class TestPlayer implements Player {
Assert.fail(String.format("Found wrong choice command (%s):\n%s\n%s\n%s",
reason,
lastChoice,
getInfo(target, game),
getInfo(target, source, game),
getInfo(source, game)
));
}

View file

@ -6,7 +6,6 @@ import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousEffectsList;
import mage.abilities.effects.Effect;
import mage.cards.Card;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardLists;
@ -32,7 +31,6 @@ import mage.players.Player;
import mage.server.game.GameSessionPlayer;
import mage.util.CardUtil;
import mage.util.ThreadUtils;
import mage.utils.StreamUtils;
import mage.utils.SystemUtil;
import mage.view.GameView;
import org.junit.Assert;
@ -1677,7 +1675,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void assertChoicesCount(TestPlayer player, int count) throws AssertionError {
String mes = String.format(
"(Choices of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.",
"(Choices of %s) Count are not equal (found %s). Make sure you use target.chooseXXX instead player.choose. Also some inner choose dialogs can be set up only in strict mode.",
player.getName(),
player.getChoices()
);
@ -1686,7 +1684,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void assertTargetsCount(TestPlayer player, int count) throws AssertionError {
String mes = String.format(
"(Targets of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.",
"(Targets of %s) Count are not equal (found %s). Make sure you use target.chooseXXX instead player.choose. Also some inner choose dialogs can be set up only in strict mode.",
player.getName(),
player.getTargets()
);
@ -2271,11 +2269,18 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
setChoice(player, choice ? "Yes" : "No", timesToChoose);
}
/**
* Declare non target choice. You can use multiple choices in one line like setChoice(name1^name2)
* Also support "up to" choices, e.g. choose 2 of 3 cards by setChoice(card1^card2) + setChoice(TestPlayer.CHOICE_SKIP)
*/
public void setChoice(TestPlayer player, String choice) {
setChoice(player, choice, 1);
}
public void setChoice(TestPlayer player, String choice, int timesToChoose) {
if (choice.equals(TestPlayer.TARGET_SKIP)) {
Assert.fail("setChoice allow only TestPlayer.CHOICE_SKIP, but found " + choice);
}
for (int i = 0; i < timesToChoose; i++) {
player.addChoice(choice);
}
@ -2340,13 +2345,18 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*
* @param player
* @param target you can add multiple targets by separating them by the "^"
* character e.g. "creatureName1^creatureName2" you can
* qualify the target additional by setcode e.g.
* character e.g. "creatureName1^creatureName2"
* -
* you can qualify the target additional by setcode e.g.
* "creatureName-M15" you can add [no copy] to the end of the
* target name to prohibit targets that are copied you can add
* [only copy] to the end of the target name to allow only
* targets that are copies. For modal spells use a prefix with
* the mode number: mode=1Lightning Bolt^mode=2Silvercoat Lion
* -
* it's also support multiple addTarget commands instead single line,
* so you can declare not full "up to" targets list by addTarget(name)
* and addTarget(TestPlayer.TARGET_SKIP)
*/
// TODO: mode options doesn't work here (see BrutalExpulsionTest)
public void addTarget(TestPlayer player, String target) {
@ -2354,6 +2364,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
public void addTarget(TestPlayer player, String target, int timesToChoose) {
if (target.equals(TestPlayer.CHOICE_SKIP)) {
Assert.fail("addTarget allow only TestPlayer.TARGET_SKIP, but found " + target);
}
for (int i = 0; i < timesToChoose; i++) {
assertAliaseSupportInActivateCommand(target, true);
player.addTarget(target);

View file

@ -55,7 +55,7 @@ public class LoadCheatsTest extends CardTestPlayerBase {
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
setChoice(playerA, "7"); // choose [group 3]: 7 = 4 default menus + 3 group
setChoice(playerA, "8"); // choose [group 3]: 8 = 5 default menus + 3 group
SystemUtil.executeCheatCommands(currentGame, commandsFile, playerA);
assertHandCount(playerA, "Razorclaw Bear", 1);