From 7df86e91a021f5f02b89fa668466ab841362bc8d Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:51:13 +0100 Subject: [PATCH] [CLU] Implement Sludge Titan Introduced TargetCardAndOrCardInGraveyard, derived from the Library one. Did not achieved everything I wanted in the tests, as the choice seems to be accepted. Tested it live, no particular issue, you can't select more than one per category. --- Mage.Sets/src/mage/cards/s/SludgeTitan.java | 87 +++++++ .../src/mage/sets/RavnicaClueEdition.java | 1 + .../cards/single/clu/SludgeTitanTest.java | 220 ++++++++++++++++++ .../TargetCardAndOrCardInGraveyard.java | 114 +++++++++ 4 files changed, 422 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SludgeTitan.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java create mode 100644 Mage/src/main/java/mage/target/common/TargetCardAndOrCardInGraveyard.java diff --git a/Mage.Sets/src/mage/cards/s/SludgeTitan.java b/Mage.Sets/src/mage/cards/s/SludgeTitan.java new file mode 100644 index 00000000000..6ad9e7d196e --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SludgeTitan.java @@ -0,0 +1,87 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardAndOrCardInGraveyard; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class SludgeTitan extends CardImpl { + + public SludgeTitan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B/G}{B/G}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.GIANT); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever Sludge Titan enters the battlefield or attacks, mill five cards. You may put a creature card and/or a land card from among them into your hand. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new SludgeTitanEffect())); + } + + private SludgeTitan(final SludgeTitan card) { + super(card); + } + + @Override + public SludgeTitan copy() { + return new SludgeTitan(this); + } +} + +class SludgeTitanEffect extends OneShotEffect { + + SludgeTitanEffect() { + super(Outcome.Benefit); + this.staticText = "mill five cards. " + + "You may put a creature card and/or a land card from among them into your hand"; + } + + private SludgeTitanEffect(final SludgeTitanEffect effect) { + super(effect); + } + + @Override + public SludgeTitanEffect copy() { + return new SludgeTitanEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Cards cards = controller.millCards(5, source, game); + game.getState().processAction(game); + if (!cards.isEmpty()) { + TargetCard target = new TargetCardAndOrCardInGraveyard(CardType.CREATURE, CardType.LAND); + controller.choose(Outcome.DrawCard, cards, target, source, game); + Cards toHand = new CardsImpl(); + toHand.addAll(target.getTargets()); + controller.moveCards(toHand, Zone.HAND, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/RavnicaClueEdition.java b/Mage.Sets/src/mage/sets/RavnicaClueEdition.java index b77075ec975..b04d362bae8 100644 --- a/Mage.Sets/src/mage/sets/RavnicaClueEdition.java +++ b/Mage.Sets/src/mage/sets/RavnicaClueEdition.java @@ -44,6 +44,7 @@ public final class RavnicaClueEdition extends ExpansionSet { cards.add(new SetCardInfo("Sacred Foundry", 279, Rarity.RARE, mage.cards.s.SacredFoundry.class)); cards.add(new SetCardInfo("Secret Passage", 20, Rarity.UNCOMMON, mage.cards.s.SecretPassage.class)); cards.add(new SetCardInfo("Senator Peacock", 2, Rarity.RARE, mage.cards.s.SenatorPeacock.class)); + cards.add(new SetCardInfo("Sludge Titan", 43, Rarity.RARE, mage.cards.s.SludgeTitan.class)); cards.add(new SetCardInfo("Steam Vents", 280, Rarity.RARE, mage.cards.s.SteamVents.class)); cards.add(new SetCardInfo("Stomping Ground", 281, Rarity.RARE, mage.cards.s.StompingGround.class)); cards.add(new SetCardInfo("Study", 21, Rarity.UNCOMMON, mage.cards.s.Study.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java new file mode 100644 index 00000000000..9b22b8e5954 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/clu/SludgeTitanTest.java @@ -0,0 +1,220 @@ + +package org.mage.test.cards.single.clu; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class SludgeTitanTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.SludgeTitan Sludge Titan} {4}{B/G}{B/G} + * Creature — Zombie Giant + * Trample + * Whenever Sludge Titan enters the battlefield or attacks, mill five cards. You may put a creature card and/or a land card from among them into your hand. + * 6/6 + */ + private static final String titan = "Sludge Titan"; + + private static final String piker = "Goblin Piker"; // creature 1 + private static final String vanguard = "Elite Vanguard"; // creature 2 + private static final String divination = "Divination"; // sorcery + private static final String dryad = "Dryad Arbor"; // land creature + private static final String plateau = "Plateau"; // land 1 + private static final String savannah = "Savannah"; // land 2 + + // When Golgari Brownscale is put into your hand from your graveyard, you gain 2 life. + private static final String brownscale = "Golgari Brownscale"; + + @Test + public void testNoValidChoice() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, divination, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, playerA.CHOICE_SKIP); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, divination, 5); + } + + @Test + public void testNoValidChoiceInvalid() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, divination, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, divination); // invalid, titan doesn't allow for choosing sorcery + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + + try { + execute(); + Assert.fail("must throw exception on execute"); + } catch (Throwable e) { + if (!e.getMessage().startsWith("Missing CHOICE def")) { + Assert.fail("Unexpected exception " + e.getMessage()); + } + } + } + + @Test + public void testCreatureOnly_ChooseNone() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, piker, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, playerA.CHOICE_SKIP); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, piker, 5); + } + + @Test + public void testCreatureOnly_ChooseOne() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, piker, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, piker); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, piker, 4); + assertHandCount(playerA, piker, 1); + } + + @Ignore + // The test suite does not fully validate the choice, and accept it. Work properly live preventing such choice. + @Test + public void testCreatureOnly_ChooseTwoInvalid() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, piker, 3); + addCard(Zone.LIBRARY, playerA, vanguard, 2); + + attack(1, playerA, titan, playerB); + setChoice(playerA, piker + "^" + vanguard); // invalid, titan doesn't allow for choosing 2 creatures + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + + try { + execute(); + Assert.fail("must throw exception on execute"); + } catch (Throwable e) { + if (!e.getMessage().startsWith("Missing CHOICE def")) { + Assert.fail("Unexpected exception " + e.getMessage()); + } + } + } + + @Test + public void testBoth_ChooseTwo() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, piker); + addCard(Zone.LIBRARY, playerA, vanguard); + addCard(Zone.LIBRARY, playerA, divination); + addCard(Zone.LIBRARY, playerA, savannah); + addCard(Zone.LIBRARY, playerA, plateau); + + attack(1, playerA, titan, playerB); + setChoice(playerA, piker + "^" + savannah); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, 3); + assertHandCount(playerA, savannah, 1); + assertHandCount(playerA, piker, 1); + } + + @Test + public void testBoth_ChooseTwo_DryadArbor() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, dryad, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, dryad + "^" + dryad); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, dryad, 3); + assertHandCount(playerA, dryad, 2); + } + + @Ignore + // The test suite does not fully validate the choice, and accept it. Work properly live preventing selecting 3 dryads. + @Test + public void testBoth_ChooseThree_DryadArbor_Invalid() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, dryad, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, dryad + "^" + dryad + "^" + dryad); // invalid, titan doesn't allow for choosing a creature and/or a land + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + + try { + execute(); + Assert.fail("must throw exception on execute"); + } catch (Throwable e) { + if (!e.getMessage().startsWith("Missing CHOICE def")) { + Assert.fail("Unexpected exception " + e.getMessage()); + } + } + } + + @Test + public void test_Brownscale_triggers() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, titan); + addCard(Zone.LIBRARY, playerA, brownscale, 5); + + attack(1, playerA, titan, playerB); + setChoice(playerA, brownscale); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertGraveyardCount(playerA, brownscale, 4); + assertHandCount(playerA, brownscale, 1); + assertLife(playerA, 20 + 2); + } +} diff --git a/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInGraveyard.java b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInGraveyard.java new file mode 100644 index 00000000000..fee2478e044 --- /dev/null +++ b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInGraveyard.java @@ -0,0 +1,114 @@ +package mage.target.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.PredicateCardAssignment; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.util.CardUtil; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author Susucr + *

+ * almost identical to {@link TargetCardAndOrCardInLibrary} + */ +public class TargetCardAndOrCardInGraveyard extends TargetCardInGraveyard { + + private static FilterCard makeFilter(Predicate firstPredicate, + Predicate secondPredicate, + String filterText) { + FilterCard filter = new FilterCard(filterText); + filter.add(Predicates.or( + firstPredicate, secondPredicate + )); + return filter; + } + + private static String makeFilterText(String first, String second) { + return CardUtil.addArticle(first) + " card and/or " + CardUtil.addArticle(second) + " card"; + } + + private final PredicateCardAssignment assignment; + + /** + * a [firstType] card and/or a [secondType] card + */ + protected TargetCardAndOrCardInGraveyard(Predicate firstPredicate, Predicate secondPredicate, String filterText) { + super(0, 2, makeFilter(firstPredicate, secondPredicate, filterText)); + this.assignment = new PredicateCardAssignment(firstPredicate, secondPredicate); + } + + public TargetCardAndOrCardInGraveyard(CardType firstType, CardType secondType) { + this(firstType.getPredicate(), secondType.getPredicate(), makeFilterText( + CardUtil.getTextWithFirstCharLowerCase(firstType.toString()), + CardUtil.getTextWithFirstCharLowerCase(secondType.toString()))); + } + + protected TargetCardAndOrCardInGraveyard(final TargetCardAndOrCardInGraveyard target) { + super(target); + this.assignment = target.assignment; + } + + @Override + public TargetCardAndOrCardInGraveyard copy() { + return new TargetCardAndOrCardInGraveyard(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { + return false; + } + Card card = game.getCard(id); + if (card == null) { + return false; + } + if (this.getTargets().isEmpty()) { + return true; + } + Cards cards = new CardsImpl(this.getTargets()); + cards.add(card); + return assignment.getRoleCount(cards, game) >= cards.size(); + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = super.possibleTargets(sourceControllerId, source, game); + // assuming max targets = 2, need to expand this code if not + Card card = game.getCard(this.getFirstTarget()); + if (card == null) { + return possibleTargets; // no further restriction if no target yet chosen + } + Cards cards = new CardsImpl(card); + if (assignment.getRoleCount(cards, game) == 2) { + // if the first chosen target is both types, no further restriction + return possibleTargets; + } + Set leftPossibleTargets = new HashSet<>(); + for (UUID possibleId : possibleTargets) { + Card possibleCard = game.getCard(possibleId); + Cards checkCards = cards.copy(); + checkCards.add(possibleCard); + if (assignment.getRoleCount(checkCards, game) == 2) { + // if the possible target and the existing target have both types, it's legal + // but this prevents the case of both targets with the same type + leftPossibleTargets.add(possibleId); + } + } + return leftPossibleTargets; + } + + @Override + public String getDescription() { + return filter.getMessage(); + } +}