Merge pull request #1125 from Simown/strive-cost-fix

Possible fix to Strive cost modification.
This commit is contained in:
LevelX2 2015-07-18 00:04:25 +02:00
commit fc483445fc
5 changed files with 395 additions and 12 deletions

View file

@ -0,0 +1,300 @@
package org.mage.test.cards.cost.modification;
import mage.abilities.keyword.HexproofAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.game.permanent.Permanent;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* Battlefield Thaumaturge:
* Creature - Human Wizard
* Each instant and sorcery spell you cast costs {1} less to cast for each creature it targets.
* Heroic - Whenever you cast a spell that targets Battlefield Thaumaturge, Battlefield Thaumaturge gains hexproof until end of turn.
*
* @author Simown
*/
public class BattlefieldThaumaturgeTest extends CardTestPlayerBase {
@Test
public void testSingleTargetReduction() {
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge");
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerA, "Island");
addCard(Zone.HAND, playerA, "Lightning Strike");
addCard(Zone.BATTLEFIELD, playerB, "Akroan Skyguard");
// Lightning Strike - {1}{R} - Lightning Strike deals 3 damage to target creature or player.
// Because Battlefield Thaumaturge is on the battlefield, and the creature is targeted by the
// Lightning Strike it will be payable with {R}.
castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Strike", "Akroan Skyguard");
setStopAt(2, PhaseStep.END_TURN);
execute();
// PlayerA still has the Battlefield Thaumaturge
assertPermanentCount(playerA, "Battlefield Thaumaturge", 1);
// The Akroan Skyguard has been killed by the Lightning Strike
assertPermanentCount(playerB, "Akroan Skyguard", 0);
assertGraveyardCount(playerB, "Akroan Skyguard", 1);
// Check {R} has been used to pay, and the other land remains untapped
assertTappedCount("Mountain", true, 1);
assertTappedCount("Island", false, 1);
}
@Test
public void testStriveTargetingReduction1() {
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge");
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.BATTLEFIELD, playerA, "Pharika's Chosen");
addCard(Zone.HAND, playerA, "Silence the Believers");
addCard(Zone.BATTLEFIELD, playerB, "Battlewise Hoplite");
/**
* Silence the Believers - {2}{B}{B}
* Exile any number of target creatures and all Auras attached to them
* Strive - Silence the Believers costs {2}{B} more to cast for each target beyond the first.
* Targetting a creature on both sides of the battlefield.
*/
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Silence the Believers", "Pharika's Chosen^Battlewise Hoplite");
setStopAt(2, PhaseStep.END_TURN);
execute();
// Both creatures were exiled
assertExileCount("Pharika's Chosen", 1);
assertExileCount("Battlewise Hoplite", 1);
// Not still on the battlefield or in the graveyard
assertPermanentCount(playerA, "Pharika's Chosen", 0);
assertPermanentCount(playerB, "Battlewise Hoplite", 0);
assertGraveyardCount(playerA, "Pharika's Chosen", 0);
assertGraveyardCount(playerB, "Battlewise Hoplite", 0);
/* Cost to exile 2 permanents will be:
* + {2}{B}{B} for the base spell
* + {2}{B} for an additional target
* - {2} for Battlefield Thaumaturge cost reducing effect
* = {2}{B}{B}{B} to pay.
*/
// Check 3 Swamps have been tapped to pay the reduced cost
assertTappedCount("Swamp", true, 3);
// And 2 Forests for the colorless mana
assertTappedCount("Forest", true, 2);
// And the rest of the Forests remain untapped
assertTappedCount("Forest", false, 2);
}
@Test
public void testStriveTargetingReduction2() {
String [] creatures = {"Battlefield Thaumaturge", "Agent of Horizons", "Blood-Toll Harpy", "Anvilwrought Raptor", "Fleshmad Steed" };
for(String creature : creatures) {
addCard(Zone.BATTLEFIELD, playerA, creature);
}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, "Launch the Fleet");
// Launch the Fleet - {W}
// Strive - Launch the Fleet costs {1} more to cast for each target beyond the first.
// Until end of turn, any number of target creatures each gain "Whenever this creature attacks, put a 1/1 white Soldier token onto the battlefield tapped and attacking."
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Launch the Fleet", createTargetingString(creatures));
for(String creature : creatures) {
attack(3, playerA, creature);
}
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
// 5 initial creatures + 5 soldier tokens + 6 lands
assertPermanentCount(playerA, 16);
// The initial creatures exist
for(String creature : creatures) {
assertPermanentCount(playerA, creature, 1);
}
// Each has a solider token generated while attacking
assertPermanentCount(playerA, "Soldier", 5);
// Battlefield Thaumaturge will have hexproof from heroic trigger
Permanent battlefieldThaumaturge = getPermanent("Battlefield Thaumaturge", playerA.getId());
Assert.assertTrue("Battlefield Thaumaturge must have hexproof", battlefieldThaumaturge.getAbilities().contains(HexproofAbility.getInstance()));
assertLife(playerA, 20);
// 5 initial creatures + 5 soldier tokens => 16 damage
assertLife(playerB, 4);
/* Cost to have 5 attackers generate soldier tokens
* + {W} for the base spell
* + {4} for an additional targets
* - {4} for Battlefield Thaumaturge cost reducing effect (reduce {1} per target)
* = {W} to pay.
*/
assertTappedCount("Plains", true, 1);
// No other mana has been tapped to pay costs
assertTappedCount("Island", false, 5);
}
@Test
public void testVariableCostReduction() {
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge");
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.HAND, playerA, "Curse of the Swine");
String [] opponentsCreatures = {"Fleecemane Lion", "Sedge Scorpion", "Boon Satyr", "Bronze Sable"};
for(String creature: opponentsCreatures) {
addCard(Zone.BATTLEFIELD, playerB, creature);
}
/* Curse of the Swine - {X}{U}{U}
* Exile X target creatures. For each creature exiled this way,
* its controller puts a 2/2 green Boar creature token onto the battlefield
*/
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curse of the Swine");
setChoice(playerA, "X=4");
addTarget(playerA, createTargetingString(opponentsCreatures));
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
// All the opponents creatures have been exiled from the battlefield
for(String creature: opponentsCreatures) {
assertPermanentCount(playerB, creature, 0);
assertGraveyardCount(playerB, creature, 0);
assertExileCount(creature, 1);
}
// All 4 creatures have been replaced by boars
assertPermanentCount(playerB, "Boar", 4);
/* Cost to target 4 permanents will be:
* + {4}{U}{U} for the base spell with X = 4
* - {4} for Battlefield Thaumaturge cost reducing effect as 4 creatures are targetted
* = {U}{U} to pay.
*/
// Check 2 islands have been tapped to pay the reduced cost
assertTappedCount("Island", true, 2);
// And the rest of the lands remain untapped
assertTappedCount("Plains", false, 4);
}
@Test
public void testMutipleTargetReduction() {
String [] playerACreatures = {"Battlefield Thaumaturge", "Sedge Scorpion", "Boon Satyr"};
String [] playerBCreatures = {"Agent of Horizons", "Blood-Toll Harpy", "Anvilwrought Raptor"};
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
// Creatures for player A
for(String creature: playerACreatures) {
addCard(Zone.BATTLEFIELD, playerA, creature);
}
addCard(Zone.HAND, playerA, "Descent of the Dragons");
// Creatures for player B
for(String creature: playerBCreatures) {
addCard(Zone.BATTLEFIELD, playerB, creature);
}
/* Descent of the Dragons - {4}{R}{R}
* Destroy any number of target creatures.
* For each creature destroyed this way, its controller puts a 4/4 red Dragon creature token with flying onto the battlefield.
* Battlefield Thaumaturge should reduce the cost of the spell when cast, before he is destroyed and replaced with a dragon.
*/
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Descent of the Dragons", createTargetingString(playerACreatures) + "^" + createTargetingString(playerBCreatures));
setStopAt(1, PhaseStep.END_TURN);
execute();
// All creatures have been put in the graveyard
for(String creature: playerACreatures) {
assertPermanentCount(playerA, creature, 0);
assertGraveyardCount(playerA, creature, 1);
}
for(String creature: playerBCreatures) {
assertPermanentCount(playerB, creature, 0);
assertGraveyardCount(playerB, creature, 1);
}
// And each player has 3 dragons
assertPermanentCount(playerA, "Dragon", 3);
assertPermanentCount(playerB, "Dragon", 3);
/* Cost to target 6 creatures will be
* + {4}{R}{R} for the fixed cost base spell
* - {4} for Battlefield Thaumaturge cost reducing effect
* each creature targeted will reduce the cost by {1} so the cost
* can only be reduced by {4} maximum using Battlefield Thaumaturge
* even though 6 creatures are targeted.
* = {R}{R} to pay.
*/
// Check 2 mountains have been tapped to pay the reduced cost
assertTappedCount("Mountain", true, 2);
// And the rest of the lands remain untapped
assertTappedCount("Swamp", false, 4);
}
@Test
public void testTargetNonCreature() {
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.HAND, playerA, "Fade into Antiquity");
addCard(Zone.BATTLEFIELD, playerB, "Heliod, God of the Sun");
// Fade into Antiquity - Sorcery - {2}{G} - Exile target artifact or enchantment.
// Battlefield Thaumaturge only reduces the cost instants and sorceries where the target is a creature
// No cost reduction for targeting an enchantment (devotion is too low for Heliod to be a creature)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fade into Antiquity", "Heliod, God of the Sun");
setStopAt(1, PhaseStep.END_TURN);
execute();
// PlayerA still has the Battlefield Thaumaturge
assertPermanentCount(playerA, "Battlefield Thaumaturge", 1);
// Heliod has been exiled
assertPermanentCount(playerB, "Heliod, God of the Sun", 0);
assertGraveyardCount(playerB, "Heliod, God of the Sun", 0);
assertExileCount("Heliod, God of the Sun", 1);
// Expect full amount paid, i.e. all lands are tapped. Cost was not reduced by Battlefield Thaumaturge, no creature was targeted.
assertTappedCount("Forest", true, 3);
}
@Test
public void testTargetWithAura() {
addCard(Zone.BATTLEFIELD, playerA, "Battlefield Thaumaturge");
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.HAND, playerA, "Spectra Ward");
// Spectra Ward - {3}{W}{W} - Aura
// Enchanted creature gets +2/+2 and has protection from all colors. This effect doesn't remove auras.
// Battlefield Thaumaturge only reduces the cost instants and sorceries targeting creatures.
// No cost reduction for targeting a creature with an Aura.
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Spectra Ward", "Battlefield Thaumaturge");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Battlefield Thaumaturge", 1);
assertPermanentCount(playerA, "Spectra Ward", 1);
// Battlefield Thaumaturge will have hexproof from heroic trigger
Permanent battlefieldThaumaturge = getPermanent("Battlefield Thaumaturge", playerA.getId());
Assert.assertTrue("Battlefield Thaumaturge must have hexproof", battlefieldThaumaturge.getAbilities().contains(HexproofAbility.getInstance()));
// No cost reduction from Battlefield Thaumaturge, full amount paid
assertTappedCount("Plains", true, 5);
}
private String createTargetingString(String [] targets) {
StringBuilder targetBuilder = new StringBuilder();
for (String target : targets) {
if (targetBuilder.length() > 0) {
targetBuilder.append('^');
}
targetBuilder.append(target);
}
System.out.println(targetBuilder.toString());
return targetBuilder.toString();
}
}

View file

@ -711,6 +711,28 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
Assert.assertEquals("(Battlefield) Tapped state is not equal (" + cardName + ")", tapped, found.isTapped());
}
/**
* Assert whether X permanents of the same name are tapped or not.
*
* @param cardName Name of the permanent that should be checked.
* @param tapped Whether the permanent is tapped or not
* @param count The amount of this permanents that should be tapped
*/
public void assertTappedCount(String cardName, boolean tapped, int count) throws AssertionError {
int tappedAmount = 0;
Permanent found = null;
for (Permanent permanent : currentGame.getBattlefield().getAllActivePermanents()) {
if (permanent.getName().equals(cardName)) {
if(permanent.isTapped() == tapped) {
tappedAmount++;
}
found = permanent;
}
}
Assert.assertNotNull("There is no such permanent on the battlefield, cardName=" + cardName, found);
Assert.assertEquals("(Battlefield) " + count + " permanents (" + cardName + ") are not " + ((tapped) ? "" : "un") + "tapped.", count, tappedAmount);
}
/**
* Assert whether a permanent is attacking or not
*

View file

@ -66,6 +66,22 @@ public class ManaUtilTest extends CardTestPlayerBase {
testManaToPayVsLand("{W/R}{R/G}", "Sacred Foundry", 2, 2); // can't auto choose to pay
}
@Test
public void testManaCondensing() {
Assert.assertEquals("{5}{W}", ManaUtil.condenseManaCostString(("{1}{1}{1}{2}{W}")));
Assert.assertEquals("{4}{B}{B}", ManaUtil.condenseManaCostString("{2}{B}{2}{B}"));
Assert.assertEquals("{6}{R}{R}{R}{U}", ManaUtil.condenseManaCostString("{R}{1}{R}{2}{R}{3}{U}"));
Assert.assertEquals("{5}{B}{U}{W}", ManaUtil.condenseManaCostString("{1}{B}{W}{4}{U}"));
Assert.assertEquals("{8}{B}{G}{G}{U}", ManaUtil.condenseManaCostString("{1}{G}{1}{2}{3}{G}{B}{U}{1}"));
Assert.assertEquals("{3}{R}{U}", ManaUtil.condenseManaCostString("{3}{R}{U}"));
Assert.assertEquals("{10}", ManaUtil.condenseManaCostString("{1}{2}{3}{4}"));
Assert.assertEquals("{B}{G}{R}{U}{W}", ManaUtil.condenseManaCostString("{B}{G}{R}{U}{W}"));
Assert.assertEquals("{R}{R}", ManaUtil.condenseManaCostString("{R}{R}"));
Assert.assertEquals("{U}", ManaUtil.condenseManaCostString("{U}"));
Assert.assertEquals("{2}", ManaUtil.condenseManaCostString("{2}"));
Assert.assertEquals("", ManaUtil.condenseManaCostString(""));
}
/**
* Common way to test ManaUtil.tryToAutoPay
*
@ -133,4 +149,5 @@ public class ManaUtilTest extends CardTestPlayerBase {
}
return useableAbilities;
}
}

View file

@ -39,6 +39,7 @@ import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.target.Target;
import mage.util.ManaUtil;
/**
*
@ -50,7 +51,7 @@ public class StriveAbility extends SimpleStaticAbility {
private final String striveCost;
public StriveAbility(String manaString) {
super(Zone.STACK, new StriveCostIncreasementEffect(new ManaCostsImpl(manaString)));
super(Zone.STACK, new StriveCostIncreasingEffect(new ManaCostsImpl(manaString)));
setRuleAtTheTop(true);
this.striveCost = manaString;
}
@ -71,29 +72,32 @@ public class StriveAbility extends SimpleStaticAbility {
}
}
class StriveCostIncreasementEffect extends CostModificationEffectImpl {
class StriveCostIncreasingEffect extends CostModificationEffectImpl {
private ManaCostsImpl striveCosts = null;
public StriveCostIncreasementEffect(ManaCostsImpl striveCosts) {
public StriveCostIncreasingEffect(ManaCostsImpl striveCosts) {
super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.INCREASE_COST);
this.striveCosts = striveCosts;
}
protected StriveCostIncreasementEffect(StriveCostIncreasementEffect effect) {
protected StriveCostIncreasingEffect(StriveCostIncreasingEffect effect) {
super(effect);
this.striveCosts = effect.striveCosts;
}
@Override
public boolean apply(Game game, Ability source, Ability abilityToModify) {
// Target target = abilityToModify.getTargets().get(0);
for (Target target : abilityToModify.getTargets()) {
if (target.getMaxNumberOfTargets() == Integer.MAX_VALUE) {
int additionalTargets = target.getTargets().size() - 1;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < additionalTargets; i++) {
abilityToModify.getManaCostsToPay().add(striveCosts.copy());
// Build up a string of strive costs for each target
sb.append(striveCosts.getText());
}
String finalCost = ManaUtil.condenseManaCostString(sb.toString());
abilityToModify.getManaCostsToPay().add(new ManaCostsImpl(finalCost));
return true;
}
}
@ -106,7 +110,7 @@ class StriveCostIncreasementEffect extends CostModificationEffectImpl {
}
@Override
public StriveCostIncreasementEffect copy() {
return new StriveCostIncreasementEffect(this);
public StriveCostIncreasingEffect copy() {
return new StriveCostIncreasingEffect(this);
}
}

View file

@ -1,9 +1,7 @@
package mage.util;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import mage.MageObject;
import mage.Mana;
import mage.ManaSymbol;
@ -379,4 +377,46 @@ public class ManaUtil {
return unpaid.getText();
}
}
/** Converts a collection of mana symbols into a single condensed string
* e.g.
* {1}{1}{1}{1}{1}{W} = {5}{W}
* {2}{B}{2}{B}{2}{B} = {6}{B}{B}{B}
* {1}{2}{R}{U}{1}{1} = {5}{R}{U}
* {B}{G}{R} = {B}{G}{R}
* */
public static String condenseManaCostString(String rawCost) {
int total = 0;
int index = 0;
// Split the string in to an array of numbers and colored mana symbols
String[] splitCost = rawCost.replace("{", "").replace("}", " ").split(" ");
// Sort alphabetically which will push1 the numbers to the front before the colored mana symbols
Arrays.sort(splitCost);
for (String c : splitCost) {
// If the string is a representation of a number
if(c.matches("\\d+")) {
total += Integer.parseInt(c);
} else {
// First non-number we see we can finish as they are sorted
break;
}
index++;
}
int splitCostLength = splitCost.length;
// No need to add {total} to the mana cost if total == 0
int shift = (total > 0) ? 1 : 0;
String [] finalCost = new String[shift + splitCostLength - index];
// Account for no colourless mana symbols seen
if(total > 0) {
finalCost[0] = String.valueOf(total);
}
System.arraycopy(splitCost, index, finalCost, shift, splitCostLength - index);
// Combine the cost back as a mana string
StringBuilder sb = new StringBuilder();
for(String s: finalCost) {
sb.append("{" + s + "}");
}
// Return the condensed string
return sb.toString();
}
}