Costs Tag Tracking part 2: Tag system and X values, reworked deep copy code (#11406)

* Implement Costs Tag Map system

* Use Costs Tag Map system to store X value for spells, abilities, and resolving permanents

* Store Bestow without target's tags
Change functions for getting tags and storing the tags of a new permanent

* Create and use deep copy function in CardUtil, add Copyable<T> to many classes

* Fix Hall Of the Bandit Lord infinite loop

* Add additional comments

* Don't store null/empty costs tags maps (saves memory)

* Fix two more Watchers with Ability variable

* Add check for exact collection types during deep copy

* Use generics instead of pure type erasure during deep copy

* convert more code to using deep copy helper, everything use Object copier, add EnumMap

* fix documentation

* Don't need the separate null checks anymore (handled in deepCopyObject)

* Minor cleanup
This commit is contained in:
ssk97 2023-11-16 11:12:32 -08:00 committed by GitHub
parent 72e30f1574
commit bea33c7493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 458 additions and 338 deletions

View file

@ -10,6 +10,8 @@ import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Ignore;
@ -571,10 +573,10 @@ public class CopySpellTest extends CardTestPlayerBase {
}
@Test
public void test_CopiedSpellsHasntETB() {
public void test_CopiedSpellsETBCounters() {
// testing:
// - x in copied creature spell (copy x)
// - copied spells enters as tokens and it hasn't ETB, see rules below
// - copied spells enters as tokens and correctly ETB, see rules below
// 0/0
// Capricopian enters the battlefield with X +1/+1 counters on it.
@ -616,36 +618,34 @@ public class CopySpellTest extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden");
// ETB triggers will not trigger here due not normal cast. From rules:
// - The token that a resolving copy of a spell becomes isnt said to have been created. (2021-04-16)
// - A nontoken permanent enters the battlefield when its moved onto the battlefield from another zone.
// A token enters the battlefield when its created. See rules 403.3, 603.6a, 603.6d, and 614.12.
//
// So both copies enters without counters:
// - Capricopian copy must die
// - Grenzo, Dungeon Warden must have default PT
// 608.3f If the object thats resolving is a copy of a permanent spell, it will become a token permanent
// as it is put onto the battlefield in any of the steps above.
// 111.12. A copy of a permanent spell becomes a token as it resolves. The token has the characteristics of
// the spell that became that token. The token is not created for the purposes of any replacement effects
// or triggered abilities that refer to creating a token.
// The tokens must enter with counters
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 1); // copy dies
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 2);
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// counters checks
// counters checks, have to check if it's a card or a token since token copies have isCopy()=false
int originalCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> !p.isCopy())
.filter(p -> p instanceof PermanentCard)
.mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum();
int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> p.isCopy())
.filter(p -> p instanceof PermanentToken)
.mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum();
Assert.assertEquals("original grenzo must have 2x counters", 2, originalCounters);
Assert.assertEquals("copied grenzo must have 0x counters", 0, copyCounters);
Assert.assertEquals("copied grenzo must have 2x counters", 2, copyCounters);
}
@Test
@ -748,7 +748,6 @@ public class CopySpellTest extends CardTestPlayerBase {
* Thieving Skydiver is kicked and then copied, but the copied version does not let you gain control of anything.
*/
@Test
@Ignore
public void copySpellWithKicker() {
// When Thieving Skydiver enters the battlefield, if it was kicked, gain control of target artifact with mana value X or less.
// If that artifact is an Equipment, attach it to Thieving Skydiver.
@ -758,7 +757,8 @@ public class CopySpellTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3); // Original price, + 1 kicker, + 1 for Double Major
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 2);
addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 1);
addCard(Zone.BATTLEFIELD, playerB, "Expedition Map", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thieving Skydiver");
@ -766,14 +766,16 @@ public class CopySpellTest extends CardTestPlayerBase {
setChoice(playerA, "X=1");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Thieving Skydiver", "Thieving Skydiver");
addTarget(playerA, "Sol Ring"); // Choice for copy
addTarget(playerA, "Sol Ring"); // Choice for original
addTarget(playerA, "Expedition Map"); // Choice for original
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Sol Ring", 2); // 1 taken by original, one by copy
assertPermanentCount(playerA, "Sol Ring", 1);
assertPermanentCount(playerA, "Expedition Map", 1);
assertPermanentCount(playerB, "Sol Ring", 0);
assertPermanentCount(playerB, "Expedition Map", 0);
}
private void abilitySourceMustBeSame(Card card, String infoPrefix) {

View file

@ -65,7 +65,7 @@ public class CardIconsTest extends CardTestPlayerBase {
}
@Test
public void test_CostX_Copies() {
public void test_CostX_StackCopy() {
// Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it.
addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
@ -144,6 +144,67 @@ public class CardIconsTest extends CardTestPlayerBase {
.orElse(null);
Assert.assertNotNull("copied card must be in battlefield", copiedCardView);
Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size());
Assert.assertEquals("copied x cost text", "x=2", copiedCardView.getCardIcons().get(0).getText());
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_CostX_TokenCopy() {
//Legend Rule doesn't apply
addCard(Zone.BATTLEFIELD, playerA, "Mirror Gallery", 1);
// Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it.
addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
// Create a token that's a copy of target creature you control.
// should not copy the X value of the Grenzo
addCard(Zone.HAND, playerA, "Quasiduplicate", 1); // {1}{U}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
// cast Grenzo
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 2);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden");
setChoice(playerA, "X=2");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// cast Quasiduplicate
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Quasiduplicate", "Grenzo, Dungeon Warden");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2);
// battlefield (card and copied card as token)
runCode("card icons in battlefield (cloned)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
GameView gameView = getGameView(player);
PlayerView playerView = gameView.getPlayers().get(0);
Assert.assertEquals("player", player.getName(), playerView.getName());
// original
CardView originalCardView = playerView.getBattlefield().values()
.stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> !p.isToken())
.findFirst()
.orElse(null);
Assert.assertNotNull("original card must be in battlefield", originalCardView);
Assert.assertEquals("original must have x cost card icons", 1, originalCardView.getCardIcons().size());
Assert.assertEquals("original x cost text", "x=2", originalCardView.getCardIcons().get(0).getText());
//
CardView copiedCardView = playerView.getBattlefield().values()
.stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> p.isToken())
.findFirst()
.orElse(null);
Assert.assertNotNull("copied card must be in battlefield", copiedCardView);
Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size());
Assert.assertEquals("copied x cost text", "x=0", copiedCardView.getCardIcons().get(0).getText());
});