Implementing Daybound/Nightbound mechanic (#8200)

* adding initial day/night support in game state

* remove card exclusion for testing

* added functional implementation to abilities from main branch

* functionally implemented NightCondition

* updated DayNightHint

* added support for nightbound entering transformed at night

* [MID] Implemented Unnatural Moonrise

* [MID] Implemented The Celestus

* added some docs

* changed access for state day/night methods

* added transformation to day/night switch

* re-added unfinished filter, removed day/night cards

* fixed some errors with transforming

* added hints to all day/night cards

* added transformation prevention plus a test

* added Immerwolf test

* [MID] Implemented Tovolar, Dire Overlord / Tovolar, The Midnight Scourge

* refactored some cards to not use isTransformable

* removed transformable parameter

* simplified some transform code

* fixed null pointer exception

* removed unnecessary canTransform method

* fixed a small error

* reworked implementation of rule 701.28f

* small change in transform logic

* fixed failiing test

* fixed verify failure

* small merge change

* added support for day/night switching based on spells cast

* [MID] Implemented Curse of Leeches / Leeching Lurkers

* moved day/night handling to untap step

* added tests for cards which set day and trigger from a change

* [MID] Implemented Ludevic, Necrogenius / Olag, Ludevic's Hubris

* added support for creatures transforming to match day/night when necessary

* fixed verify failures

* fixed another verify failure

* remove temporary verify skip

* added transform message

* removed unnecessary transform message

* [MID] Implemented Angelic Enforcer / Enduring Angel

* updated DayNightHint with more information

* fixed verify failure

* merge fix

* fixed Startled Awake / Persistent Nightmare / Moonmist interaction

* added another test for Moonmist

* merge fix

* merge fix

* [MID] Implemented Baneblade Scoundrel / Baneclaw Marauder

* merge fix

* [MID] various text fixes

* [MID] a few more text fixes

* Merge fix

* Improved transform game logs (hints, source), fixed day/night logs, fixed miss game param (due code style);

* fixed a test failure

* Merge fix

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Evan Kranzler 2021-11-05 15:11:23 -04:00 committed by GitHub
parent 6d4e5672c3
commit 30afb11cd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
305 changed files with 2174 additions and 1064 deletions

View file

@ -0,0 +1,349 @@
package org.mage.test.cards.abilities.keywords;
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;
/**
* @author TheElk801
*/
public class DayNightTest extends CardTestPlayerBase {
private static final String ruffian = "Tavern Ruffian";
private static final String smasher = "Tavern Smasher";
private static final String moonmist = "Moonmist";
private static final String outcasts = "Grizzled Outcasts";
private static final String wantons = "Krallenhorde Wantons";
private static final String immerwolf = "Immerwolf";
private static final String bolt = "Lightning Bolt";
private static final String curse = "Curse of Leeches";
private static final String lurker = "Leeching Lurker";
private static final String vandal = "Brimstone Vandal";
private void assertDayNight(boolean daytime) {
Assert.assertTrue("It should not be neither day nor night", currentGame.hasDayNight());
Assert.assertTrue("It should be " + (daytime ? "day" : "night"), currentGame.checkDayNight(daytime));
Assert.assertFalse("It should not be " + (daytime ? "night" : "day"), currentGame.checkDayNight(!daytime));
}
private void assertRuffianSmasher(boolean daytime) {
assertDayNight(daytime);
if (daytime) {
assertPowerToughness(playerA, ruffian, 2, 5);
assertPermanentCount(playerA, smasher, 0);
} else {
assertPermanentCount(playerA, ruffian, 0);
assertPowerToughness(playerA, smasher, 6, 5);
}
}
private void setDayNight(int turn, PhaseStep phaseStep, boolean daytime) {
runCode("set game to " + (daytime ? "day" : "night"), turn, phaseStep, playerA, (i, p, game) -> game.setDaytime(daytime));
}
@Test
public void testRegularDay() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(true);
}
@Test
public void testNightbound() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(false);
}
@Test
public void testDayToNightTransform() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setDayNight(1, PhaseStep.POSTCOMBAT_MAIN, false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(false);
}
@Test
public void testNightToDayTransform() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setDayNight(1, PhaseStep.POSTCOMBAT_MAIN, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(true);
}
@Test
public void testMoonmistFails() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerA, ruffian);
addCard(Zone.BATTLEFIELD, playerA, outcasts);
addCard(Zone.HAND, playerA, moonmist);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, moonmist);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(true);
assertPermanentCount(playerA, outcasts, 0);
assertPowerToughness(playerA, wantons, 7, 7);
}
@Test
public void testImmerwolfPreventsTransformation() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, immerwolf);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setDayNight(1, PhaseStep.POSTCOMBAT_MAIN, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(true);
assertPowerToughness(playerA, smasher, 6 + 1, 5 + 1);
assertPermanentCount(playerA, ruffian, 0);
}
@Test
public void testImmerwolfRemoved() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
addCard(Zone.BATTLEFIELD, playerA, immerwolf);
addCard(Zone.HAND, playerA, bolt);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setDayNight(1, PhaseStep.BEGIN_COMBAT, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_COMBAT);
execute();
assertDayNight(true);
assertPowerToughness(playerA, smasher, 6 + 1, 5 + 1);
assertPermanentCount(playerA, ruffian, 0);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, immerwolf);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(true);
}
@Test
public void testNoSpellsBecomesNight() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertRuffianSmasher(true);
setStopAt(2, PhaseStep.END_TURN);
execute();
assertRuffianSmasher(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(false);
}
@Test
public void testTwoSpellsBecomesDay() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
addCard(Zone.HAND, playerA, ruffian);
addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ruffian);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 3);
assertGraveyardCount(playerA, bolt, 1);
assertRuffianSmasher(false);
setStopAt(2, PhaseStep.END_TURN);
execute();
assertRuffianSmasher(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertRuffianSmasher(false);
}
@Test
public void testCurseOfLeechesRegular() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.HAND, playerA, curse);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, curse, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(true);
Permanent permanent = getPermanent(curse);
Assert.assertTrue("Curse is attached to playerB", permanent.isAttachedTo(playerB.getId()));
assertPermanentCount(playerA, lurker, 0);
}
@Test
public void testCurseOfLeechesNightbound() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.HAND, playerA, curse);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, curse, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(false);
assertPermanentCount(playerA, curse, 0);
assertPermanentCount(playerA, lurker, 1);
}
@Test
public void testCurseOfLeechesDayToNight() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.HAND, playerA, curse);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, curse, playerB);
setDayNight(1, PhaseStep.POSTCOMBAT_MAIN, false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(false);
assertPermanentCount(playerA, curse, 0);
assertPermanentCount(playerA, lurker, 1);
}
@Test
public void testCurseOfLeechesNightToDay() {
currentGame.setDaytime(false);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.HAND, playerA, curse);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, curse, playerB);
setChoice(playerA, playerB.getName());
setDayNight(1, PhaseStep.POSTCOMBAT_MAIN, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(true);
Permanent permanent = getPermanent(curse);
Assert.assertTrue("Curse is attached to playerB", permanent.isAttachedTo(playerB.getId()));
assertPermanentCount(playerA, lurker, 0);
}
@Test
public void testBrimstoneVandalBecomeDay() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, vandal);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vandal);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertDayNight(true);
assertLife(playerB, 20);
}
@Test
public void testBrimstoneVandalTrigger() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, bolt, 2);
addCard(Zone.HAND, playerA, vandal);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vandal);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.UPKEEP);
execute();
assertDayNight(false);
assertLife(playerB, 20 - 1);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB);
setStopAt(4, PhaseStep.UPKEEP);
execute();
assertAllCommandsUsed();
assertDayNight(true);
assertLife(playerB, 20 - 1 - 3 - 3 - 1);
}
}

View file

@ -1,15 +1,13 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.Permanent;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class TransformTest extends CardTestPlayerBase {
@ -133,7 +131,7 @@ public class TransformTest extends CardTestPlayerBase {
* 4G Creature - Human Shaman Whenever a permanent you control transforms
* into a non-Human creature, put a 2/2 green Wolf creature token onto the
* battlefield.
*
* <p>
* Reported bug: "It appears to trigger either when a non-human creature
* transforms OR when a creature transforms from a non-human into a human
* (as in when a werewolf flips back to the sun side), rather than when a
@ -171,24 +169,51 @@ public class TransformTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Startled Awake"); // SORCERY {2}{U}{U}"
addCard(Zone.BATTLEFIELD, playerA, "Island", 9);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Startled Awake");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Startled Awake", playerB);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}{U}{U}");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, 13);
assertGraveyardCount(playerA, "Startled Awake", 0);
assertPermanentCount(playerA, "Persistent Nightmare", 1); // Night-side card of Startled Awake
Permanent nightmare = getPermanent("Persistent Nightmare", playerA);
Assert.assertTrue("Has to have creature card type", nightmare.isCreature(currentGame));
Assert.assertFalse("Has not to have sorcery card type", nightmare.isSorcery(currentGame));
assertType("Persistent Nightmare", CardType.CREATURE, true);
assertType("Persistent Nightmare", CardType.SORCERY, false);
}
@Test
public void testStartledAwakeMoonmist() {
addCard(Zone.HAND, playerA, "Startled Awake");
addCard(Zone.HAND, playerA, "Moonmist");
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 11);
addCard(Zone.BATTLEFIELD, playerA, "Maskwood Nexus");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Startled Awake", playerB);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}{U}{U}");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Moonmist");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, 13);
assertGraveyardCount(playerA, "Startled Awake", 0);
assertPermanentCount(playerA, "Persistent Nightmare", 1); // Night-side card of Startled Awake
assertType("Persistent Nightmare", CardType.CREATURE, true);
assertType("Persistent Nightmare", CardType.SORCERY, false);
}
/**
* When copy token of Lambholt Pacifist transforms with "its transform
* ability", I see below error. Then rollback.
*
* <p>
* 701.25a Only permanents represented by double-faced cards can transform.
* (See rule 711, Double-Faced Cards.) If a spell or ability instructs a
* player to transform any permanent that isnt represented by a
@ -221,7 +246,7 @@ public class TransformTest extends CardTestPlayerBase {
/**
* Mirror Mockery copies the front face of a Transformed card rather than
* the current face.
*
* <p>
* It's worth pointing out that my opponent cast Mirror Mockery the previous
* turn - after it had transformed. I should have included the part of the
* log that showed that Mirror Mockery was applied to the Unimpeded
@ -280,11 +305,13 @@ public class TransformTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Wastes", 3);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Silvercoat Lion");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{C}", "Archangel Avacyn", "Whenever a non-Angel creature you control dies");
activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{C}", "Archangel Avacyn");
setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Lightning Bolt", 1);
assertGraveyardCount(playerA, "Silvercoat Lion", 1);
@ -317,9 +344,9 @@ public class TransformTest extends CardTestPlayerBase {
* was on stack, my opponent used Displacer's ability targeting Huntmaster.
* That ability resolved and Huntmaster still transformed like it never left
* the battlefield.
*
* <p>
* http://www.slightlymagic.net/forum/viewtopic.php?f=70&t=20014&p=210533#p210513
*
* <p>
* The transform effect on the stack should fizzle. The card brought back
* from Exile should be a new object unless I am interpreting the rules
* incorrectly. The returned permanent uses the same GUID.
@ -356,12 +383,13 @@ public class TransformTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Ravager of the Fells", 0);
assertPermanentCount(playerA, "Huntmaster of the Fells", 1);
assertPowerToughness(playerA, "Huntmaster of the Fells", 2, 2);
assertPowerToughness(playerA, "Huntmaster of the Fells", 2, 2);
assertTappedCount("Plains", true, 2);
assertTappedCount("Wastes", true, 1);
}
@Test
@Test
public void testHuntmasterTransformed() {
// Whenever this creature enters the battlefield or transforms into Huntmaster of the Fells, create a 2/2 green Wolf creature token and you gain 2 life.
// At the beginning of each upkeep, if no spells were cast last turn, transform Huntmaster of the Fells.
@ -387,15 +415,16 @@ public class TransformTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Ravager of the Fells", 0);
assertPermanentCount(playerA, "Huntmaster of the Fells", 1);
assertPowerToughness(playerA, "Huntmaster of the Fells", 2, 2);
}
/**
* Having cast Phantasmal Image copying my opponent's flipped Thing in the
* Ice, I was left with a 0/4 Awoken Horror.
*
* <p>
* https://github.com/magefree/mage/issues/5893
*
* <p>
* The transform effect on the stack should fizzle. The card brought back
* from Exile should be a new object unless I am interpreting the rules
* incorrectly. The returned permanent uses the same GUID.
@ -440,4 +469,32 @@ public class TransformTest extends CardTestPlayerBase {
}
@Test
public void testMoonmistDelver() {
addCard(Zone.BATTLEFIELD, playerA, "Island");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.HAND, playerA, "Delver of Secrets");
addCard(Zone.HAND, playerA, "Moonmist", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delver of Secrets");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Moonmist");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Delver of Secrets", 0);
assertPermanentCount(playerA, "Insectile Aberration", 1);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Moonmist");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Delver of Secrets", 1);
assertPermanentCount(playerA, "Insectile Aberration", 0);
}
}