mirror of
https://github.com/magefree/mage.git
synced 2025-12-28 22:42:03 -08:00
Reworked cost adjuster logic for better support of X and cost modification effects:
Improves: * refactor: split CostAdjuster logic in multiple parts - prepare X, prepare cost, increase cost, reduce cost; * refactor: improved VariableManaCost to support min/max values, playable and AI calculations, test framework; * refactor: improved EarlyTargetCost to support mana costs too (related to #13023); * refactor: migrated some cards with CostAdjuster and X to EarlyTargetCost (Knollspine Invocation, etc - related to #13023); * refactor: added shared code for "As an additional cost to cast this spell, discard X creature cards"; * refactor: added shared code for "X is the converted mana cost of the exiled card"; * tests: added dozens tests with cost adjusters; Bug fixes: * game: fixed that some cards with CostAdjuster ignore min/max limits for X (allow to choose any X, example: Scorched Earth, Open The Way); * game: fixed that some cards ask to announce already defined X values (example: Bargaining Table); * game: fixed that some cards with CostAdjuster do not support combo with other cost modification effects; * game, gui: fixed missing game logs about predefined X values; * game, gui: fixed wrong X icon for predefined X values; Test framework: * test framework: added X min/max check for wrong values; * test framework: added X min/max info in miss X value announce; * test framework: added check to find duplicated effect bugs (see assertNoDuplicatedEffects); Cards: * Open The Way - fixed that it allow to choose any X without limits (close #12810); * Unbound Flourishing - improved combo support for activated abilities with predefined X mana costs like Bargaining Table;
This commit is contained in:
parent
13a832ae00
commit
bae3089abb
100 changed files with 1519 additions and 449 deletions
|
|
@ -0,0 +1,587 @@
|
|||
package org.mage.test.cards.cost.additional;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.common.SimpleActivatedAbility;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.common.CreateTokenEffect;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.token.custom.CreatureToken;
|
||||
import mage.util.CardUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps {
|
||||
|
||||
private void prepareCustomCardInHand(String cardName, String spellManaCost, CostAdjuster costAdjuster) {
|
||||
SpellAbility spellAbility = new SpellAbility(new ManaCostsImpl<>(spellManaCost), cardName);
|
||||
if (costAdjuster != null) {
|
||||
spellAbility.setCostAdjuster(costAdjuster);
|
||||
}
|
||||
addCustomCardWithAbility(
|
||||
cardName,
|
||||
playerA,
|
||||
null,
|
||||
spellAbility,
|
||||
CardType.ENCHANTMENT,
|
||||
spellManaCost,
|
||||
Zone.HAND
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareCustomPermanent(String cardName, String abilityName, String abilityManaCost, CostAdjuster costAdjuster) {
|
||||
Ability ability = new SimpleActivatedAbility(
|
||||
new CreateTokenEffect(new CreatureToken(1, 1).withName("test token")).setText(abilityName),
|
||||
new ManaCostsImpl<>(abilityManaCost)
|
||||
);
|
||||
if (costAdjuster != null) {
|
||||
ability.setCostAdjuster(costAdjuster);
|
||||
}
|
||||
addCustomCardWithAbility(cardName, playerA, ability);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_DistributeValues() {
|
||||
// make sure it can distribute values between min and max and skip useless values (example: mana optimization)
|
||||
|
||||
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, 0, 0));
|
||||
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, -10, 10));
|
||||
Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, Integer.MIN_VALUE, Integer.MAX_VALUE));
|
||||
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 0));
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 1));
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 2));
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 3));
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 9));
|
||||
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(2, 0, 0));
|
||||
Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(2, 0, 1));
|
||||
Assert.assertEquals(Arrays.asList(0, 2), CardUtil.distributeValues(2, 0, 2));
|
||||
Assert.assertEquals(Arrays.asList(0, 3), CardUtil.distributeValues(2, 0, 3));
|
||||
Assert.assertEquals(Arrays.asList(0, 9), CardUtil.distributeValues(2, 0, 9));
|
||||
|
||||
Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(3, 0, 0));
|
||||
Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(3, 0, 1));
|
||||
Assert.assertEquals(Arrays.asList(0, 1, 2), CardUtil.distributeValues(3, 0, 2));
|
||||
Assert.assertEquals(Arrays.asList(0, 2, 3), CardUtil.distributeValues(3, 0, 3));
|
||||
Assert.assertEquals(Arrays.asList(0, 5, 9), CardUtil.distributeValues(3, 0, 9));
|
||||
|
||||
Assert.assertEquals(Arrays.asList(10, 15, 20), CardUtil.distributeValues(3, 10, 20));
|
||||
Assert.assertEquals(Arrays.asList(10, 16, 21), CardUtil.distributeValues(3, 10, 21));
|
||||
Assert.assertEquals(Arrays.asList(10, 16, 22), CardUtil.distributeValues(3, 10, 22));
|
||||
Assert.assertEquals(Arrays.asList(10, 17, 23), CardUtil.distributeValues(3, 10, 23));
|
||||
Assert.assertEquals(Arrays.asList(10, 20, 29), CardUtil.distributeValues(3, 10, 29));
|
||||
|
||||
Assert.assertEquals(Arrays.asList(10), CardUtil.distributeValues(5, 10, 10));
|
||||
Assert.assertEquals(Arrays.asList(10, 11), CardUtil.distributeValues(5, 10, 11));
|
||||
Assert.assertEquals(Arrays.asList(10, 11, 12), CardUtil.distributeValues(5, 10, 12));
|
||||
Assert.assertEquals(Arrays.asList(10, 11, 12, 13), CardUtil.distributeValues(5, 10, 13));
|
||||
Assert.assertEquals(Arrays.asList(10, 11, 13, 14, 15), CardUtil.distributeValues(5, 10, 15));
|
||||
Assert.assertEquals(Arrays.asList(10, 13, 15, 18, 20), CardUtil.distributeValues(5, 10, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X_SpellAbility() {
|
||||
prepareCustomCardInHand("test card", "{X}{1}", null);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card");
|
||||
setChoice(playerA, "X=2");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "test card", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X_ActivatedAbility() {
|
||||
prepareCustomPermanent("test card", "test ability", "{X}{1}", null);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:");
|
||||
setChoice(playerA, "X=2");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "test token", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_SpellAbility_TestFrameworkMustCatchLimits() {
|
||||
prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() {
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
ability.setVariableCostsMinMax(0, 1);
|
||||
}
|
||||
});
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card");
|
||||
setChoice(playerA, "X=2");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
try {
|
||||
execute();
|
||||
} catch (AssertionError e) {
|
||||
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2"));
|
||||
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1"));
|
||||
return;
|
||||
}
|
||||
Assert.fail("test must fail");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: AI must support game simulations for X choice, see announceXMana
|
||||
public void test_prepareX_SpellAbility_AI() {
|
||||
prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() {
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
ability.setVariableCostsMinMax(0, 10);
|
||||
}
|
||||
});
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
// AI must play card and use min good value
|
||||
// it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1
|
||||
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "test card", 1);
|
||||
assertTappedCount("Forest", true, 1); // must choose X=0
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_ActivatedAbility_TestFrameworkMustCatchLimits() {
|
||||
prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() {
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
ability.setVariableCostsMinMax(0, 1);
|
||||
}
|
||||
});
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:");
|
||||
setChoice(playerA, "X=2");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
try {
|
||||
execute();
|
||||
} catch (AssertionError e) {
|
||||
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2"));
|
||||
Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1"));
|
||||
return;
|
||||
}
|
||||
Assert.fail("test must fail");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: AI must support game simulations for X choice, see announceXMana
|
||||
// TODO: implement AI and add tests for non-mana X values (announceXCost)
|
||||
public void test_prepareX_ActivatedAbility_AI() {
|
||||
prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() {
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
ability.setVariableCostsMinMax(0, 10);
|
||||
}
|
||||
});
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
// AI must activate ability with min good value for X
|
||||
// it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1
|
||||
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "test token", 1);
|
||||
assertTappedCount("Forest", true, 1); // must choose X=0
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_SpellAbility_ScorchedEarth_PayZero() {
|
||||
// with X announce
|
||||
|
||||
// As an additional cost to cast this spell, discard X land cards.
|
||||
// Destroy X target lands.
|
||||
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 0 + 1);
|
||||
addCard(Zone.HAND, playerA, "Island", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
|
||||
setChoice(playerA, "X=0");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_SpellAbility_ScorchedEarth_PaySome() {
|
||||
// with X announce
|
||||
|
||||
// As an additional cost to cast this spell, discard X land cards.
|
||||
// Destroy X target lands.
|
||||
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 1);
|
||||
addCard(Zone.HAND, playerA, "Island", 10);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
|
||||
setChoice(playerA, "X=2");
|
||||
addTarget(playerA, "Forest", 2); // to destroy
|
||||
setChoice(playerA, "Island", 2); // discard cost
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_SpellAbility_ScorchedEarth_PayAll() {
|
||||
// with X announce
|
||||
|
||||
// As an additional cost to cast this spell, discard X land cards.
|
||||
// Destroy X target lands.
|
||||
addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10 + 1);
|
||||
addCard(Zone.HAND, playerA, "Island", 10);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Forest", 10);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth");
|
||||
setChoice(playerA, "X=10");
|
||||
addTarget(playerA, "Forest", 10); // to destroy
|
||||
setChoice(playerA, "Island", 10); // discard cost
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_ActivatedAbility_BargainingTable_PayZero() {
|
||||
// with direct X (without announce)
|
||||
|
||||
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
|
||||
//
|
||||
// no cards in opponent's hand
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
|
||||
setChoice(playerA, playerB.getName());
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_ActivatedAbility_BargainingTable_PaySome() {
|
||||
// with direct X (without announce)
|
||||
|
||||
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
|
||||
//
|
||||
addCard(Zone.HAND, playerB, "Forest", 3);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
|
||||
setChoice(playerA, playerB.getName());
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_ActivatedAbility_BargainingTable_CantPay() {
|
||||
// with direct X (without announce)
|
||||
|
||||
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
|
||||
//
|
||||
addCard(Zone.HAND, playerB, "Forest", 3);
|
||||
|
||||
// must not request opponent choice because it must see min hand size as 3
|
||||
checkPlayableAbility("must not able to activate due lack of mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:", false);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_ActivatedAbility_BargainingTable_UnboundFlourishingMustCopy() {
|
||||
// with direct X (without announce)
|
||||
|
||||
// {X}, {T}: Draw a card. X is the number of cards in an opponent's hand.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
|
||||
//
|
||||
addCard(Zone.HAND, playerB, "Forest", 3);
|
||||
//
|
||||
// Whenever you cast a permanent spell with a mana cost that contains {X}, double the value of X.
|
||||
// Whenever you cast an instant or sorcery spell or activate an ability, if that spell’s mana cost or that
|
||||
// ability’s activation cost contains {X}, copy that spell or ability. You may choose new targets for the copy.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1);
|
||||
|
||||
// Unbound Flourishing must see {X} mana cost and duplicate ability on stack
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:");
|
||||
setChoice(playerA, playerB.getName());
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 2); // from original and copied abilities
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_NecropolisFiend() {
|
||||
// {X}, {T}, Exile X cards from your graveyard: Target creature gets -X/-X until end of turn.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Necropolis Fiend");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
|
||||
addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 3);
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Ancient Bronze Dragon"); // 7/7
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}, Exile");
|
||||
setChoice(playerA, "X=2");
|
||||
addTarget(playerA, "Ancient Bronze Dragon"); // to -2/-2
|
||||
setChoice(playerA, "Grizzly Bears", 2);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPowerToughness(playerB, "Ancient Bronze Dragon", 7 - 2, 7 - 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_OpenTheWay() {
|
||||
skipInitShuffling();
|
||||
|
||||
// X can't be greater than the number of players in the game.
|
||||
// Reveal cards from the top of your library until you reveal X land cards.
|
||||
// Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order.
|
||||
addCard(Zone.HAND, playerA, "Open the Way"); // {X}{G}{G}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 2);
|
||||
addCard(Zone.LIBRARY, playerA, "Island", 5);
|
||||
|
||||
// min/max test require multiple tests (see above), so just disable setChoice and look at logs for good limits
|
||||
// example: Message: Announce the value for {X} (any value)
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Open the Way");
|
||||
setChoice(playerA, "X=2");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Island", 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_KnollspineInvocation() {
|
||||
skipInitShuffling();
|
||||
|
||||
// {X}, Discard a card with mana value X: This enchantment deals X damage to any target.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Knollspine Invocation");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
//
|
||||
addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1); // {1}{G}
|
||||
|
||||
// turn 1 - can't play due empty hand
|
||||
checkPlayableAbility("no cards to discard", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false);
|
||||
|
||||
// turn 3 - can't play due no mana
|
||||
activateManaAbility(3, PhaseStep.UPKEEP, playerA, "{T}: Add {G}", 2);
|
||||
checkPlayableAbility("no mana to activate", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false);
|
||||
|
||||
// turn 5 - can play
|
||||
checkPlayableAbility("must able to activate", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", true);
|
||||
activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard");
|
||||
setChoice(playerA, "Grizzly Bears"); // discard
|
||||
addTarget(playerA, playerB); // damage
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(5, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertLife(playerB, 20 - 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prepareX_EliteArcanist() {
|
||||
// When Elite Arcanist enters the battlefield, you may exile an instant card from your hand.
|
||||
// {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card.
|
||||
addCard(Zone.HAND, playerA, "Elite Arcanist"); // {3}{U}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Island", 4 + 1);
|
||||
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
|
||||
|
||||
// turn 1
|
||||
// prepare arcanist
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elite Arcanist");
|
||||
setChoice(playerA, true); // use exile
|
||||
setChoice(playerA, "Lightning Bolt"); // to exile and copy later
|
||||
|
||||
// turn 3
|
||||
// cast copy
|
||||
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}: Copy");
|
||||
setChoice(playerA, true); // cast copy
|
||||
addTarget(playerA, playerB); // damage
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(3, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertTappedCount("Island", true, 1); // used to cast copy for {1}
|
||||
assertLife(playerB, 20 - 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_modifyCost_Fireball() {
|
||||
// This spell costs {1} more to cast for each target beyond the first.
|
||||
// Fireball deals X damage divided evenly, rounded down, among any number of targets.
|
||||
addCard(Zone.HAND, playerA, "Fireball"); // {X}{R}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1); // 3 for x=2 cast, 1 for x2 targets
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 3); // 1/1
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fireball");
|
||||
setChoice(playerA, "X=2");
|
||||
addTarget(playerA, "Arbor Elf^Arbor Elf");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertLife(playerA, 20);
|
||||
assertLife(playerB, 20);
|
||||
assertTappedCount("Mountain", true, 3 + 1); // 3 for x=2 cast, 1 for x2 targets
|
||||
assertGraveyardCount(playerA, "Arbor Elf", 2);
|
||||
assertPermanentCount(playerA, "Arbor Elf", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_modifyCost_DeepwoodDenizen() {
|
||||
// {5}{G}, {T}: Draw a card. This ability costs {1} less to activate for each +1/+1 counter on creatures you control.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Deepwood Denizen");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 - 3);
|
||||
//
|
||||
// +1: Distribute three +1/+1 counters among one, two, or three target creatures you control
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Mentor of Heroes", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1);
|
||||
|
||||
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", false);
|
||||
|
||||
// add +3 counters and get -3 cost decrease
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Distribute");
|
||||
addTargetAmount(playerA, "Arbor Elf", 2);
|
||||
addTargetAmount(playerA, "Deepwood Denizen", 1);
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", true);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertHandCount(playerA, 1); // +1 from draw ability
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_modifyCost_BaruWurmspeaker() {
|
||||
// Wurms you control get +2/+2 and have trample.
|
||||
// {7}{G}, {T}: Create a 4/4 green Wurm creature token. This ability costs {X} less to activate, where X is the greatest power among Wurms you control.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Baru, Wurmspeaker");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
|
||||
//
|
||||
// When this creature dies, create a 3/3 colorless Phyrexian Wurm artifact creature token with deathtouch
|
||||
// and a 3/3 colorless Phyrexian Wurm artifact creature token with lifelink.
|
||||
addCard(Zone.HAND, playerA, "Wurmcoil Engine", 1); // {6}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
|
||||
|
||||
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", false);
|
||||
|
||||
// turn 1
|
||||
// prepare wurm and get -8 cost decrease (6 wurm + 2 boost)
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wurmcoil Engine");
|
||||
|
||||
// turn 3
|
||||
waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", true);
|
||||
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(3, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertTokenCount(playerA, "Wurm Token", 1);
|
||||
assertTappedCount("Forest", true, 1);
|
||||
assertTappedCount("Mountain", true, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Other_AbandonHope() {
|
||||
// used both modify and cost reduction in one cost adjuster
|
||||
|
||||
// As an additional cost to cast this spell, discard X cards.
|
||||
// Look at target opponent's hand and choose X cards from it. That player discards those cards.
|
||||
addCard(Zone.HAND, playerA, "Abandon Hope"); // {X}{1}{B}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 + 2);
|
||||
addCard(Zone.HAND, playerA, "Forest", 2);
|
||||
addCard(Zone.HAND, playerB, "Grizzly Bears", 3);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abandon Hope");
|
||||
setChoice(playerA, "X=2");
|
||||
addTarget(playerA, playerB);
|
||||
setChoice(playerA, "Forest^Forest"); // discard cost
|
||||
setChoice(playerA, "Grizzly Bears^Grizzly Bears"); // discard from hand
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, "Forest", 2);
|
||||
assertGraveyardCount(playerB, "Grizzly Bears", 2);
|
||||
}
|
||||
|
||||
// additional tasks to improve code base:
|
||||
// TODO: OsgirTheReconstructorCostAdjuster - migrate to EarlyTargetCost
|
||||
// TODO: SkeletalScryingAdjuster - migrate to EarlyTargetCost
|
||||
// TODO: NecropolisFiend - migrate to EarlyTargetCost
|
||||
// TODO: KnollspineInvocation - migrate to EarlyTargetCost
|
||||
// TODO: ExileCardsFromHandAdjuster - need rework to remove dialog from inside, e.g. migrate to EarlyTargetCost?
|
||||
// TODO: CallerOfTheHuntAdjuster - research and add test
|
||||
// TODO: VoodooDoll - research and add test
|
||||
}
|
||||
|
|
@ -13,8 +13,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
|
|||
*/
|
||||
public class BeltOfGiantStrengthTest extends CardTestPlayerBase {
|
||||
|
||||
/**
|
||||
* Equipped creature has base power and toughness 10/10.
|
||||
* Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets.
|
||||
*/
|
||||
private static final String belt = "Belt of Giant Strength";
|
||||
private static final String gigantosauras = "Gigantosaurus";
|
||||
private static final String gigantosauras = "Gigantosaurus"; // 10/10
|
||||
|
||||
@Test
|
||||
public void testWithManaAvailable() {
|
||||
|
|
|
|||
|
|
@ -2927,6 +2927,7 @@ public class TestPlayer implements Player {
|
|||
for (String choice : choices) {
|
||||
if (choice.startsWith("X=")) {
|
||||
int xValue = Integer.parseInt(choice.substring(2));
|
||||
assertXMinMaxValue(game, ability, xValue, min, max);
|
||||
choices.remove(choice);
|
||||
return xValue;
|
||||
}
|
||||
|
|
@ -2934,7 +2935,7 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
|
||||
this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
|
||||
+ "\nMessage: " + message);
|
||||
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
|
||||
return computerPlayer.announceXMana(min, max, message, game, ability);
|
||||
}
|
||||
|
||||
|
|
@ -2944,16 +2945,34 @@ public class TestPlayer implements Player {
|
|||
if (!choices.isEmpty()) {
|
||||
if (choices.get(0).startsWith("X=")) {
|
||||
int xValue = Integer.parseInt(choices.get(0).substring(2));
|
||||
assertXMinMaxValue(game, ability, xValue, min, max);
|
||||
choices.remove(0);
|
||||
return xValue;
|
||||
}
|
||||
}
|
||||
|
||||
this.chooseStrictModeFailed("choice", game, getInfo(ability, game)
|
||||
+ "\nMessage: " + message);
|
||||
+ "\nMessage: " + message + prepareXMaxInfo(min, max));
|
||||
return computerPlayer.announceXCost(min, max, message, game, ability, null);
|
||||
}
|
||||
|
||||
private String prepareXMaxInfo(int min, int max) {
|
||||
if (min == 0 && max == Integer.MAX_VALUE) {
|
||||
return " (any value)";
|
||||
} else {
|
||||
return String.format(" (from %s to %s)",
|
||||
min,
|
||||
(max == Integer.MAX_VALUE) ? "any" : String.valueOf(max)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertXMinMaxValue(Game game, Ability source, int xValue, int min, int max) {
|
||||
if (xValue < min || xValue > max) {
|
||||
Assert.fail("Found wrong X value = " + xValue + ", for " + CardUtil.getSourceName(game, source) + ", must" + prepareXMaxInfo(min, max));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAmount(int min, int max, String message, Game game) {
|
||||
assertAliasSupportInChoices(false);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import mage.MageObject;
|
|||
import mage.Mana;
|
||||
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;
|
||||
|
|
@ -29,6 +32,7 @@ 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;
|
||||
|
|
@ -320,6 +324,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
|||
}
|
||||
|
||||
assertAllCommandsUsed();
|
||||
|
||||
//assertNoDuplicatedEffects();
|
||||
}
|
||||
|
||||
protected TestPlayer createNewPlayer(String playerName, RangeOfInfluence rangeOfInfluence) {
|
||||
|
|
@ -1702,6 +1708,57 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure game state do not contain any duplicated effects, e.g. all effect's durations are fine
|
||||
*/
|
||||
private void assertNoDuplicatedEffects() {
|
||||
// to find bugs like https://github.com/magefree/mage/issues/12932
|
||||
|
||||
// TODO: simulate full end turn to end all effects and make it default?
|
||||
|
||||
// some effects can generate duplicated effects by design
|
||||
// example: Tamiyo, Inquisitive Student + x2 attack in TamiyoInquisitiveStudentTest.test_PlusTwo
|
||||
// +2: Until your next turn, whenever a creature attacks you or a planeswalker you control, it gets -1/-0 until end of turn.
|
||||
// TODO: add targets and affected objects check to unique key?
|
||||
|
||||
// one effect can be used multiple times by different sources
|
||||
// so use group key like: effect + ability + source
|
||||
Map<String, List<ContinuousEffect>> groups = new HashMap<>();
|
||||
for (ContinuousEffectsList layer : currentGame.getState().getContinuousEffects().allEffectsLists) {
|
||||
for (Object effectObj : layer) {
|
||||
ContinuousEffect effect = (ContinuousEffect) effectObj;
|
||||
for (Object abilityObj : layer.getAbility(effect.getId())) {
|
||||
Ability ability = (Ability) abilityObj;
|
||||
MageObject sourceObject = currentGame.getObject(ability.getSourceId());
|
||||
String groupKey = "effectClass_" + effect.getClass().getCanonicalName()
|
||||
+ "_abilityClass_" + ability.getClass().getCanonicalName()
|
||||
+ "_sourceName_" + (sourceObject == null ? "null" : sourceObject.getIdName());
|
||||
List<ContinuousEffect> groupList = groups.getOrDefault(groupKey, null);
|
||||
if (groupList == null) {
|
||||
groupList = new ArrayList<>();
|
||||
groups.put(groupKey, groupList);
|
||||
}
|
||||
groupList.add(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// analyse
|
||||
List<String> duplicatedGroups = groups.keySet().stream()
|
||||
.filter(groupKey -> groups.get(groupKey).size() > 1)
|
||||
.collect(Collectors.toList());
|
||||
if (duplicatedGroups.size() > 0) {
|
||||
System.out.println("Duplicated effect groups: " + duplicatedGroups.size());
|
||||
duplicatedGroups.forEach(groupKey -> {
|
||||
System.out.println("group " + groupKey + ": ");
|
||||
groups.get(groupKey).forEach(e -> {
|
||||
System.out.println(" - " + e.getId() + " - " + e.getDuration() + " - " + e);
|
||||
});
|
||||
});
|
||||
Assert.fail("Found duplicated effects: " + duplicatedGroups.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void assertActivePlayer(TestPlayer player) {
|
||||
Assert.assertEquals("message", currentGame.getState().getActivePlayerId(), player.getId());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue