* Improved rollback handling of the test framework. Test metadata will no longer rollbacked by a rollback and one can now define different actions after the executed rollback.

This commit is contained in:
LevelX2 2020-07-17 17:44:52 +02:00
parent 621d8c188d
commit 8c4c2728d6
11 changed files with 306 additions and 193 deletions

View file

@ -964,9 +964,8 @@ public class MorphTest extends CardTestPlayerBase {
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// 1 action must be here ("no" option is restores on failed morph call in playLand)
//assertAllCommandsUsed();
assertChoicesCount(playerA, 1);
assertAllCommandsUsed();
assertPermanentCount(playerA, "Zoetic Cavern", 1);
}

View file

@ -1,54 +0,0 @@
package org.mage.test.cards.abilities.oneshot.damage;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class DeflectingPalmTest extends CardTestPlayerBase {
/**
* Test that prevented damage will be created with the correct source and
* will trigger the ability of Satyr Firedance
* https://github.com/magefree/mage/issues/804
*/
@Test
public void testDamageInPlayer() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerA, "Plains");
// The next time a source of your choice would deal damage to you this turn, prevent that damage.
// If damage is prevented this way, Deflecting Palm deals that much damage to that source's controller.
addCard(Zone.HAND, playerA, "Deflecting Palm");
// Whenever an instant or sorcery spell you control deals damage to an opponent, Satyr Firedancer deals
// that much damage to target creature that player controls.
addCard(Zone.BATTLEFIELD, playerA, "Satyr Firedancer");
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion");
addCard(Zone.BATTLEFIELD, playerB, "Mountain");
addCard(Zone.HAND, playerB, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB ,"Lightning Bolt", playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA ,"Deflecting Palm", null, "Lightning Bolt");
setChoice(playerA, "Lightning Bolt");
addTarget(playerA, "Silvercoat Lion"); // target for Satyr Firedancer
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Deflecting Palm", 1);
assertGraveyardCount(playerB, "Lightning Bolt", 1);
assertGraveyardCount(playerB, "Silvercoat Lion", 1);
assertLife(playerA, 20);
assertLife(playerB, 17);
}
}

View file

@ -95,20 +95,20 @@ public class JaceTest extends CardTestPlayerBase {
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Draw a card");
setChoice(playerA, "Pillarfield Ox");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
rollbackTurns(3, PhaseStep.BEGIN_COMBAT, playerA, 0); // Start of turn 3
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
currentGame.rollbackTurns(0); // Start of turn 3
assertGraveyardCount(playerA, "Pillarfield Ox", 0); // Goes back to hand
assertHandCount(playerA, "Pillarfield Ox", 1);
assertExileCount("Jace, Vryn's Prodigy", 0);
assertPermanentCount(playerA, "Jace, Telepath Unbound", 0);
assertPermanentCount(playerA, "Jace, Vryn's Prodigy", 1);
Assert.assertFalse("Jace, Vryn's Prodigy may not be flipped", getPermanent("Jace, Vryn's Prodigy").isFlipped());
}
@ -119,7 +119,7 @@ public class JaceTest extends CardTestPlayerBase {
// exile Jace, Vryn's Prodigy, then return him to the battefield transformed under his owner's control.
String jVryn = "Jace, Vryn's Prodigy"; // {U}{1} 0/2
//3: You may cast target instant or sorcery card from your graveyard this turn. If that card would be put into your graveyard this turn, exile it instead.
//3: You may cast target instant or sorcery card from your graveyard this turn. If that card would be put into your graveyard this turn, exile it instead.
String jTelepath = "Jace, Telepath Unbound"; // 5 loyalty
// Sorcery, Suspend 4 {U}. Target player draws three cards.

View file

@ -1,4 +1,3 @@
package org.mage.test.cards.replacement.prevent;
import mage.constants.PhaseStep;
@ -49,7 +48,7 @@ public class DeflectingPalmTest extends CardTestPlayerBase {
*/
@Test
public void testPreventDamageWithDromokasCommand() {
setStrictChooseMode(true);
// Choose two -
// - Prevent all damage target instant or sorcery spell would deal this turn;
// - or Target player sacrifices an enchantment;
@ -68,15 +67,21 @@ public class DeflectingPalmTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Deflecting Palm");
setChoice(playerB, "Silvercoat Lion");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dromoka's Command", "Deflecting Palm");
addTarget(playerA, "Silvercoat Lion");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dromoka's Command", null, "Deflecting Palm");
setModeChoice(playerA, "1");
addTarget(playerA, "Deflecting Palm");
setModeChoice(playerA, "3");
addTarget(playerA, "Silvercoat Lion");
attack(1, playerA, "Silvercoat Lion");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Deflecting Palm", 1);
assertGraveyardCount(playerA, "Dromoka's Command", 1);
@ -87,4 +92,40 @@ public class DeflectingPalmTest extends CardTestPlayerBase {
}
/**
* Test that prevented damage will be created with the correct source and
* will trigger the ability of Satyr Firedance
* https://github.com/magefree/mage/issues/804
*/
@Test
public void testDamageInPlayer() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerA, "Plains");
// The next time a source of your choice would deal damage to you this turn, prevent that damage.
// If damage is prevented this way, Deflecting Palm deals that much damage to that source's controller.
addCard(Zone.HAND, playerA, "Deflecting Palm");
// Whenever an instant or sorcery spell you control deals damage to an opponent, Satyr Firedancer deals
// that much damage to target creature that player controls.
addCard(Zone.BATTLEFIELD, playerA, "Satyr Firedancer");
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion");
addCard(Zone.BATTLEFIELD, playerB, "Mountain");
addCard(Zone.HAND, playerB, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deflecting Palm", null, "Lightning Bolt");
setChoice(playerA, "Lightning Bolt");
addTarget(playerA, "Silvercoat Lion"); // target for Satyr Firedancer
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Deflecting Palm", 1);
assertGraveyardCount(playerB, "Lightning Bolt", 1);
assertGraveyardCount(playerB, "Silvercoat Lion", 1);
assertLife(playerA, 20);
assertLife(playerB, 17);
}
}

View file

@ -37,7 +37,11 @@ public class PlayerAction {
}
/**
* Calls after action removed from commands queue later (for multi steps action, e.g. AI related)
* Calls after action removed from commands queue later (for multi steps
* action, e.g.AI related)
*
* @param game
* @param player
*/
public void onActionRemovedLater(Game game, TestPlayer player) {
//

View file

@ -1,8 +1,14 @@
package org.mage.test.player;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.MageItem;
import mage.MageObject;
import mage.MageObjectReference;
import mage.Mana;
import mage.ObjectColor;
import mage.abilities.*;
import mage.abilities.costs.AlternativeSourceCosts;
@ -57,14 +63,6 @@ import mage.util.CardUtil;
import org.apache.log4j.Logger;
import org.junit.Assert;
import org.junit.Ignore;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.Mana;
import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*;
/**
@ -87,7 +85,8 @@ public class TestPlayer implements Player {
private boolean AIPlayer; // full playable AI
private boolean AICanChooseInStrictMode = false; // AI can choose in custom aiXXX commands (e.g. on one priority or step)
private final List<PlayerAction> actions = new ArrayList<>();
private final Map<PlayerAction, PhaseStep> actionsToRemovesLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands)
private final Map<PlayerAction, PhaseStep> actionsToRemoveLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands)
private final Map<Integer, HashMap<UUID, ArrayList<PlayerAction>>> rollbackActions = new HashMap<>(); // actions to add after a executed rollback
private final List<String> choices = new ArrayList<>(); // choices stack for choice
private final List<String> targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command)
private final Map<String, UUID> aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use)
@ -552,16 +551,16 @@ public class TestPlayer implements Player {
@Override
public boolean priority(Game game) {
// later remove actions (ai commands related)
if (actionsToRemovesLater.size() > 0) {
if (actionsToRemoveLater.size() > 0) {
List<PlayerAction> removed = new ArrayList<>();
actionsToRemovesLater.forEach((action, step) -> {
actionsToRemoveLater.forEach((action, step) -> {
if (game.getStep().getType() != step) {
action.onActionRemovedLater(game, this);
actions.remove(action);
removed.add(action);
}
});
removed.forEach(actionsToRemovesLater::remove);
removed.forEach(actionsToRemoveLater::remove);
}
int numberOfActions = actions.size();
@ -681,11 +680,15 @@ public class TestPlayer implements Player {
String[] groups = command.split("\\$");
if (groups.length > 0) {
if (groups[0].equals("Rollback")) {
if (groups.length > 1 && groups[1].startsWith("turns=")) {
if (groups.length > 2 && groups[1].startsWith("turns=") && groups[2].startsWith("rollbackBlock=")) {
int turns = Integer.parseInt(groups[1].substring(6));
int rollbackBlockNumber = Integer.parseInt(groups[2].substring(14));
game.rollbackTurns(turns);
actions.remove(action);
addActionsAfterRollback(game, rollbackBlockNumber);
return true;
} else {
Assert.fail("Rollback command misses parameter: " + command);
}
}
if (groups[0].equals("Concede")) {
@ -710,7 +713,7 @@ public class TestPlayer implements Player {
// play step
if (command.equals(AI_COMMAND_PLAY_STEP)) {
AICanChooseInStrictMode = true; // disable on action's remove
actionsToRemovesLater.put(action, game.getStep().getType());
actionsToRemoveLater.put(action, game.getStep().getType());
computerPlayer.priority(game);
return true;
}
@ -1008,6 +1011,32 @@ public class TestPlayer implements Player {
return false;
}
/**
* Adds actions to the player actions after an executed rollback Actions
* have to be added after the rollback becauuse otherwise the actions are
* not valid because otehr ot the same actions are already taken before the
* rollback.
*
* @param game
* @param rollbackBlock rollback block to add the actions for
*/
private void addActionsAfterRollback(Game game, int rollbackBlockNumber) {
Map<UUID, ArrayList<PlayerAction>> rollbackBlock = rollbackActions.get(rollbackBlockNumber);
if (rollbackBlock != null && !rollbackBlock.isEmpty()) {
for (Map.Entry<UUID, ArrayList<PlayerAction>> entry : rollbackBlock.entrySet()) {
TestPlayer testPlayer = (TestPlayer) game.getPlayer(entry.getKey());
if (testPlayer != null) {
// Add the actions at the start of the action list
int pos = 0;
for (PlayerAction playerAction : entry.getValue()) {
testPlayer.getActions().add(pos, playerAction);
pos++;
}
}
}
}
}
private void tryToPlayPriority(Game game) {
if (AIPlayer) {
computerPlayer.priority(game);
@ -1081,13 +1110,13 @@ public class TestPlayer implements Player {
List<String> data = cards.stream()
.map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ")
+ c.getIdName()
+ (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "")
+ " - " + c.getPower().getValue() + "/" + c.getToughness().getValue()
+ (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "")
+ ", " + (c.isTapped() ? "Tapped" : "Untapped")
+ getPrintableAliases(", [", c.getId(), "]")
+ (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName())))
+ c.getIdName()
+ (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "")
+ " - " + c.getPower().getValue() + "/" + c.getToughness().getValue()
+ (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "")
+ ", " + (c.isTapped() ? "Tapped" : "Untapped")
+ getPrintableAliases(", [", c.getId(), "]")
+ (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName())))
.sorted()
.collect(Collectors.toList());
@ -1111,12 +1140,12 @@ public class TestPlayer implements Player {
List<String> data = abilities.stream()
.map(a -> (a.getZone() + " -> "
+ a.getSourceObject(game).getIdName() + " -> "
+ (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified
+ (a.toString().length() > 0
? a.toString().substring(0, Math.min(20, a.toString().length()))
: a.getClass().getSimpleName())
+ "..."))
+ a.getSourceObject(game).getIdName() + " -> "
+ (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified
+ (a.toString().length() > 0
? a.toString().substring(0, Math.min(20, a.toString().length()))
: a.getClass().getSimpleName())
+ "..."))
.sorted()
.collect(Collectors.toList());
@ -1531,7 +1560,7 @@ public class TestPlayer implements Player {
UUID defenderId = null;
boolean mustAttackByAction = false;
boolean madeAttackByAction = false;
for (Iterator<org.mage.test.player.PlayerAction> it = actions.iterator(); it.hasNext(); ) {
for (Iterator<org.mage.test.player.PlayerAction> it = actions.iterator(); it.hasNext();) {
PlayerAction action = it.next();
// aiXXX commands
@ -2112,7 +2141,7 @@ public class TestPlayer implements Player {
// skip targets
if (targets.get(0).equals(TARGET_SKIP)) {
Assert.assertTrue("found skip target, but it require more targets, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0);
return true;
@ -2423,7 +2452,7 @@ public class TestPlayer implements Player {
this.chooseStrictModeFailed("choice", game,
"Triggered list (total " + abilities.size() + "):\n"
+ abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n")));
+ abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n")));
return computerPlayer.chooseTriggeredAbility(abilities, game);
}
@ -2538,16 +2567,7 @@ public class TestPlayer implements Player {
@Override
public void restore(Player player) {
this.modesSet.clear();
this.modesSet.addAll(((TestPlayer) player).modesSet);
this.actions.clear();
this.actions.addAll(((TestPlayer) player).actions);
this.choices.clear();
this.choices.addAll(((TestPlayer) player).choices);
this.targets.clear();
this.targets.addAll(((TestPlayer) player).targets);
this.aliases.clear();
this.aliases.putAll(((TestPlayer) player).aliases);
// no rollback for test player meta data (modesSet, actions, choices, targets, aliases)
computerPlayer.restore(player);
}
@ -3275,17 +3295,17 @@ public class TestPlayer implements Player {
public ManaOptions getManaAvailable(Game game) {
return computerPlayer.getManaAvailable(game);
}
@Override
public void addAvailableTriggeredMana(List<Mana> availableTriggeredMana) {
computerPlayer.addAvailableTriggeredMana(availableTriggeredMana);
}
}
@Override
public List<List<Mana>> getAvailableTriggeredMana() {
return computerPlayer.getAvailableTriggeredMana();
}
@Override
public List<ActivatedAbility> getPlayable(Game game, boolean hidden) {
return computerPlayer.getPlayable(game, hidden);
@ -3681,7 +3701,7 @@ public class TestPlayer implements Player {
// skip targets
if (targets.get(0).equals(TARGET_SKIP)) {
Assert.assertTrue("found skip target, but it require more targets, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0);
return false; // false in chooseTargetAmount = stop to choose
@ -4031,4 +4051,9 @@ public class TestPlayer implements Player {
public void setAICanChooseInStrictMode(boolean AICanChooseInStrictMode) {
this.AICanChooseInStrictMode = AICanChooseInStrictMode;
}
public Map<Integer, HashMap<UUID, ArrayList<org.mage.test.player.PlayerAction>>> getRollbackActions() {
return rollbackActions;
}
}

View file

@ -87,13 +87,13 @@ public class DemonicPactTest extends CardTestPlayerBase {
* the game. The log says I'm the winner and the opponent lost and that is
* immediately after rollback request.
*/
@Test
public void testPactOfNegationRollback() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Silvercoat Lion", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Island", 5);
// Counter target spell.
// At the beginning of your next upkeep, pay {3}{U}{U}. If you don't, you lose the game.
@ -106,22 +106,21 @@ public class DemonicPactTest extends CardTestPlayerBase {
rollbackTurns(2, PhaseStep.PRECOMBAT_MAIN, playerB, 0);
setStrictChooseMode(true);
setChoice(playerB, "Yes");
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Silvercoat Lion", 1);
assertGraveyardCount(playerB, "Pact of Negation", 1);
Assert.assertTrue("Player A is still in game", playerA.isInGame());
Assert.assertTrue("Player B is still in game", playerB.isInGame());
assertTappedCount("Island", true, 5);
assertTappedCount("Island", true, 5);
}
}

View file

@ -1,4 +1,3 @@
package org.mage.test.rollback;
import mage.constants.PhaseStep;
@ -70,28 +69,38 @@ public class NewCreaturesAreRemovedTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Port Town"); // Land
addCard(Zone.HAND, playerA, "Island"); // Land
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 3);
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); // TODO: Check why the test fails (related to rollback?) if the number is set to 3
addCard(Zone.BATTLEFIELD, playerB, "Pillarfield Ox", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tamiyo's Journal");
attack(2, playerB, "Pillarfield Ox");
attack(2, playerB, "Pillarfield Ox"); // A = 18
attack(3, playerA, "Silvercoat Lion"); // B = 18
attack(3, playerA, "Silvercoat Lion");
rollbackTurns(3, PhaseStep.END_TURN, playerA, 0);
rollbackAfterActionsStart();
attack(3, playerA, "Silvercoat Lion"); // B = 18
rollbackAfterActionsEnd();
attack(4, playerB, "Pillarfield Ox");
attack(5, playerA, "Silvercoat Lion");
attack(4, playerB, "Pillarfield Ox"); // A =16
attack(5, playerA, "Silvercoat Lion"); // B = 16
rollbackTurns(5, PhaseStep.END_TURN, playerA, 0);
rollbackAfterActionsStart();
attack(5, playerA, "Silvercoat Lion"); // B = 16
rollbackAfterActionsEnd();
attack(6, playerB, "Pillarfield Ox");
attack(6, playerB, "Pillarfield Ox"); // A = 14
playLand(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Port Town");
attack(7, playerA, "Silvercoat Lion");
attack(7, playerA, "Silvercoat Lion"); // B = 14
rollbackTurns(7, PhaseStep.POSTCOMBAT_MAIN, playerA, 0);
rollbackAfterActionsStart();
playLand(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Port Town");
attack(7, playerA, "Silvercoat Lion"); // B = 14
rollbackAfterActionsEnd();
setStopAt(7, PhaseStep.END_TURN);
execute();
@ -100,8 +109,8 @@ public class NewCreaturesAreRemovedTest extends CardTestPlayerBase {
assertTapped("Port Town", false);
assertPermanentCount(playerA, "Clue", 3);
assertLife(playerA, 14);
assertLife(playerB, 14);
assertLife(playerA, 14);
}

View file

@ -30,20 +30,29 @@ public class StateValuesTest extends CardTestPlayerBase {
attack(3, playerA, "Dragon Whelp");
rollbackTurns(3, PhaseStep.BEGIN_COMBAT, playerA, 0);
rollbackAfterActionsStart();
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: ");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}: ");
attack(3, playerA, "Dragon Whelp");
rollbackAfterActionsEnd();
setStopAt(4, PhaseStep.UPKEEP);
execute();
assertLife(playerA, 20);
assertLife(playerB, 12);
assertPermanentCount(playerA, "Dragon Whelp", 1);
assertGraveyardCount(playerA, "Dragon Whelp", 0);
assertLife(playerA, 20);
assertLife(playerB, 12);
}
@Test
public void testBriarbridgePatrol() {
setStrictChooseMode(true);
// Whenever Briarbridge Patrol deals damage to one or more creatures, investigate (Create a colorless Clue artifact token onto the battlefield with "{2}, Sacrifice this artifact: Draw a card.").
// At the beginning of each end step, if you sacrificed three or more Clues this turn, you may put a creature card from your hand onto the battlefield.
addCard(Zone.BATTLEFIELD, playerA, "Briarbridge Patrol", 1); // 3/3
@ -55,11 +64,19 @@ public class StateValuesTest extends CardTestPlayerBase {
attack(3, playerA, "Briarbridge Patrol");
block(3, playerB, "Pillarfield Ox", "Briarbridge Patrol");
rollbackTurns(3, PhaseStep.POSTCOMBAT_MAIN, playerA, 0);
rollbackAfterActionsStart();
attack(3, playerA, "Briarbridge Patrol");
block(3, playerB, "Pillarfield Ox", "Briarbridge Patrol");
rollbackAfterActionsEnd();
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20);
assertLife(playerB, 20);

View file

@ -1,4 +1,3 @@
package org.mage.test.rollback;
import mage.constants.PhaseStep;
@ -50,6 +49,7 @@ public class TransformTest extends CardTestPlayerBase {
// BACK: It That Rides as One
// Creature 4/4 First strike, lifelink
addCard(Zone.HAND, playerA, "Lone Rider"); // Creature {1}{W} 1/1
// When Venerable Monk enters the battlefield, you gain 2 life.
addCard(Zone.HAND, playerA, "Venerable Monk"); // Creature {2}{W} 2/2
@ -59,6 +59,11 @@ public class TransformTest extends CardTestPlayerBase {
attack(3, playerA, "Lone Rider");
rollbackTurns(3, PhaseStep.END_TURN, playerA, 0);
castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Venerable Monk");
attack(3, playerA, "Lone Rider");
setStopAt(4, PhaseStep.PRECOMBAT_MAIN);
execute();

View file

@ -1,5 +1,14 @@
package org.mage.test.serverside.base.impl;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.MageObject;
import mage.Mana;
import mage.ObjectColor;
@ -35,15 +44,6 @@ import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestAPI;
import org.mage.test.serverside.base.MageTestPlayerBase;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* API for test initialization and asserting the test results.
*
@ -130,6 +130,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
protected String deckNameC;
protected String deckNameD;
private int rollbackBlock = 0; // used to handle actions that have to be added aufter a rollback
private boolean rollbackBlockActive = false;
private TestPlayer rollbackPlayer = null;
protected enum ExpectedType {
TURN_NUMBER,
RESULT,
@ -197,6 +201,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
addCard(Zone.LIBRARY, playerB, "Plains", 10);
}
/**
*
* @throws GameException
* @throws FileNotFoundException
*/
@Before
public void reset() throws GameException, FileNotFoundException {
if (currentGame != null) {
@ -223,6 +232,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
gameOptions = new GameOptions();
rollbackBlock = 0;
rollbackBlockActive = false;
}
abstract protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException;
@ -279,7 +291,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
}
Assert.assertFalse("Wrong stop command on " + this.stopOnTurn + " / " + this.stopAtStep + " (" + this.stopAtStep.getIndex() + ")"
+ " (found actions after stop on " + maxTurn + " / " + maxPhase + ")",
+ " (found actions after stop on " + maxTurn + " / " + maxPhase + ")",
(maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex()));
for (Player player : currentGame.getPlayers().values()) {
@ -296,6 +308,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
gameOptions.stopAtStep = stopAtStep;
currentGame.setGameOptions(gameOptions);
currentGame.start(activePlayer.getId());
currentGame.setGameStopped(true); // used for rollback handling
long t2 = System.nanoTime();
logger.debug("Winner: " + currentGame.getWinner());
logger.info(Thread.currentThread().getStackTrace()[2].getMethodName() + " has been executed. Execution time: " + (t2 - t1) / 1000000 + " ms");
@ -329,13 +342,34 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
return player;
}
private void addPlayerAction(TestPlayer player, int turnNum, PhaseStep step, String action) {
PlayerAction playerAction = new PlayerAction("", turnNum, step, action);
addPlayerAction(player, playerAction);
}
private void addPlayerAction(TestPlayer player, String actionName, int turnNum, PhaseStep step, String action) {
PlayerAction playerAction = new PlayerAction(actionName, turnNum, step, action);
addPlayerAction(player, playerAction);
}
private void addPlayerAction(TestPlayer player, PlayerAction playerAction) {
if (rollbackBlockActive) {
rollbackPlayer.getRollbackActions()
.computeIfAbsent(rollbackBlock, block -> new HashMap<>())
.computeIfAbsent(player.getId(), playerId -> new ArrayList<>())
.add(playerAction);
} else {
player.addAction(playerAction);
}
}
// check commands
private void check(String checkName, int turnNum, PhaseStep step, TestPlayer player, String command, String... params) {
String res = CHECK_PREFIX + command;
for (String param : params) {
res += CHECK_PARAM_DELIMETER + param;
}
player.addAction(checkName, turnNum, step, res);
addPlayerAction(player, checkName, turnNum, step, res);
}
public void checkPT(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer power, Integer toughness) {
@ -452,7 +486,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
for (String param : params) {
res += CHECK_PARAM_DELIMETER + param;
}
player.addAction(showName, turnNum, step, res);
addPlayerAction(player, showName, turnNum, step, res);
}
public void showLibrary(String showName, int turnNum, PhaseStep step, TestPlayer player) {
@ -564,8 +598,9 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param cardName Card name in string format.
* @param count Amount of cards to be added.
* @param tapped In case gameZone is Battlefield, determines whether
* permanent should be tapped. In case gameZone is other than Battlefield,
* {@link IllegalArgumentException} is thrown
* permanent should be tapped. In case gameZone is other
* than Battlefield, {@link IllegalArgumentException} is
* thrown
*/
@Override
public void addCard(Zone gameZone, TestPlayer player, String cardName, int count, boolean tapped) {
@ -744,10 +779,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param cardName Card name to compare with.
* @param power Expected power to compare with.
* @param toughness Expected toughness to compare with.
* @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if you
* want "at least one creature with given name should have specified p\t"
* Use ALL, if you want "all creature with gived name should have specified
* p\t"
* @param scope {@link mage.filter.Filter.ComparisonScope} Use ANY, if
* you want "at least one creature with given name should
* have specified p\t" Use ALL, if you want "all creature
* with gived name should have specified p\t"
*/
@Override
public void assertPowerToughness(Player player, String cardName, int power, int toughness, Filter.ComparisonScope scope)
@ -1432,36 +1467,40 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void playLand(int turnNum, PhaseStep step, TestPlayer player, String cardName) {
//Assert.assertNotEquals("", cardName);
assertAliaseSupportInActivateCommand(cardName, false);
player.addAction(turnNum, step, ACTIVATE_PLAY + cardName);
addPlayerAction(player, turnNum, step, ACTIVATE_PLAY + cardName);
}
public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName) {
//Assert.assertNotEquals("", cardName);
assertAliaseSupportInActivateCommand(cardName, false);
player.addAction(turnNum, step, ACTIVATE_CAST + cardName);
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName);
}
public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, Player target) {
//Assert.assertNotEquals("", cardName);
// warning, target in spell cast command setups without choose target call
assertAliaseSupportInActivateCommand(cardName, false);
player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName());
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName());
}
public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, Player target, int manaInPool) {
//Assert.assertNotEquals("", cardName);
assertAliaseSupportInActivateCommand(cardName, false);
player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName() + "$manaInPool=" + manaInPool);
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$targetPlayer=" + target.getName() + "$manaInPool=" + manaInPool);
}
/**
* AI play one PRIORITY with multi game simulations (calcs and play ONE best
* action, can be called with stack) All choices must be made by AI (e.g.
* strict mode possible)
* action, can be called with stack) All choices must be made by AI
* (e.g.strict mode possible)
*
* @param turnNum
* @param step
* @param player
*/
public void aiPlayPriority(int turnNum, PhaseStep step, TestPlayer player) {
assertAiPlayAndGameCompatible(player);
player.addAction(createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY));
addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY));
}
/**
@ -1471,7 +1510,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*/
public void aiPlayStep(int turnNum, PhaseStep step, TestPlayer player) {
assertAiPlayAndGameCompatible(player);
player.addAction(createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_STEP));
addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_STEP));
}
public PlayerAction createAIPlayerAction(int turnNum, PhaseStep step, String aiCommand) {
@ -1497,12 +1536,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void waitStackResolved(int turnNum, PhaseStep step, TestPlayer player, boolean skipOneStackObjectOnly) {
String command = "waitStackResolved" + (skipOneStackObjectOnly ? ":1" : "");
player.addAction(turnNum, step, command);
addPlayerAction(player, turnNum, step, command);
}
/**
* Rollback the number of given turns: 0 = rollback to the start of the
* current turn
* current turn. Use the commands rollbackAfterActionsStart() and
* rollbackAfterActionsEnd() to define a block of actions, that will be
* added and executed after the rollback.
*
* @param turnNum
* @param step
@ -1510,7 +1551,33 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param turns
*/
public void rollbackTurns(int turnNum, PhaseStep step, TestPlayer player, int turns) {
player.addAction(turnNum, step, "playerAction:Rollback" + "$turns=" + turns);
rollbackBlock++;
addPlayerAction(player, turnNum, step, "playerAction:Rollback" + "$turns=" + turns + "$rollbackBlock=" + rollbackBlock);
rollbackPlayer = player;
}
/**
* Adds a number of actions that will be added to the to the start of the
* list of actions of the players but only after the rollback is executed
* because otherwis the actions are executed to early and would lead to
* invalid actions (e.g. casting the same spell twice).
*/
public void rollbackAfterActionsStart() throws IllegalStateException {
if (rollbackPlayer == null || rollbackBlock < 1) {
throw new IllegalStateException("There was no rollback action defined before. You can use this command only after a rollback action.");
}
rollbackBlockActive = true;
}
/**
* Ends a block of actions to be added after an rollback action
*/
public void rollbackAfterActionsEnd() throws IllegalStateException {
if (rollbackBlockActive = false || rollbackPlayer == null) {
throw new IllegalStateException("There was no rollback action defined before or no rollback block started. You can use this command only after a rollback action.");
}
rollbackBlockActive = false;
rollbackPlayer = null;
}
/**
@ -1521,7 +1588,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param player
*/
public void concede(int turnNum, PhaseStep step, TestPlayer player) {
player.addAction(turnNum, step, "playerAction:Concede");
addPlayerAction(player, turnNum, step, "playerAction:Concede");
}
/**
@ -1530,14 +1597,14 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
* @param player
* @param cardName
* @param targetName for modes you can add "mode=3" before target name,
* multiple targets can be seperated by ^, not target marks as
* TestPlayer.NO_TARGET
* multiple targets can be seperated by ^, not target
* marks as TestPlayer.NO_TARGET
*/
public void castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName) {
//Assert.assertNotEquals("", cardName);
assertAliaseSupportInActivateCommand(cardName, true);
assertAliaseSupportInActivateCommand(targetName, true);
player.addAction(turnNum, step, ACTIVATE_CAST + cardName + "$target=" + targetName);
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName + "$target=" + targetName);
}
public enum StackClause {
@ -1580,11 +1647,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
assertAliaseSupportInActivateCommand(targetName, true);
assertAliaseSupportInActivateCommand(spellOnStack, false);
if (StackClause.WHILE_ON_STACK == clause) {
player.addAction(turnNum, step, ACTIVATE_CAST + cardName
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName)
+ "$spellOnStack=" + spellOnStack);
} else {
player.addAction(turnNum, step, ACTIVATE_CAST + cardName
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName)
+ "$!spellOnStack=" + spellOnStack);
}
@ -1603,7 +1670,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
if (spellOnTopOfStack != null && !spellOnTopOfStack.isEmpty()) {
action += "$spellOnTopOfStack=" + spellOnTopOfStack;
}
player.addAction(turnNum, step, action);
addPlayerAction(player, turnNum, step, action);
}
public void activateManaAbility(int turnNum, PhaseStep step, TestPlayer player, String ability) {
@ -1612,20 +1679,20 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void activateManaAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, int timesToActivate) {
for (int i = 0; i < timesToActivate; i++) {
player.addAction(turnNum, step, ACTIVATE_MANA + ability);
addPlayerAction(player, turnNum, step, ACTIVATE_MANA + ability);
}
}
public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability) {
// TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't
assertAliaseSupportInActivateCommand(ability, false);
player.addAction(turnNum, step, ACTIVATE_ABILITY + ability);
addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability);
}
public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, Player target) {
// TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't
assertAliaseSupportInActivateCommand(ability, false);
player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$targetPlayer=" + target.getName());
addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability + "$targetPlayer=" + target.getName());
}
public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String... targetNames) {
@ -1634,7 +1701,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
Arrays.stream(targetNames).forEach(n -> {
assertAliaseSupportInActivateCommand(n, true);
});
player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames));
addPlayerAction(player, turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames));
}
/**
@ -1682,31 +1749,31 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
sb.append(spellOnStack);
}
player.addAction(turnNum, step, sb.toString());
addPlayerAction(player, turnNum, step, sb.toString());
}
public void addCounters(int turnNum, PhaseStep step, TestPlayer player, String cardName, CounterType type, int count) {
//Assert.assertNotEquals("", cardName);
player.addAction(turnNum, step, "addCounters:" + cardName + '$' + type.getName() + '$' + count);
addPlayerAction(player, turnNum, step, "addCounters:" + cardName + '$' + type.getName() + '$' + count);
}
public void attack(int turnNum, TestPlayer player, String attacker) {
//Assert.assertNotEquals("", attacker);
assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index
player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker);
addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker);
}
public void attack(int turnNum, TestPlayer player, String attacker, TestPlayer defendingPlayer) {
//Assert.assertNotEquals("", attacker);
assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index
player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker + "$defendingPlayer=" + defendingPlayer.getName());
addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, "attack:" + attacker + "$defendingPlayer=" + defendingPlayer.getName());
}
public void attack(int turnNum, TestPlayer player, String attacker, String planeswalker) {
//Assert.assertNotEquals("", attacker);
assertAliaseSupportInActivateCommand(attacker, false); // it uses old special notation like card_name:index
assertAliaseSupportInActivateCommand(planeswalker, false);
player.addAction(turnNum, PhaseStep.DECLARE_ATTACKERS, new StringBuilder("attack:").append(attacker).append("$planeswalker=").append(planeswalker).toString());
addPlayerAction(player, turnNum, PhaseStep.DECLARE_ATTACKERS, new StringBuilder("attack:").append(attacker).append("$planeswalker=").append(planeswalker).toString());
}
public void attackSkip(int turnNum, TestPlayer player) {
@ -1718,7 +1785,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
//Assert.assertNotEquals("", attacker);
assertAliaseSupportInActivateCommand(blocker, false); // it uses old special notation like card_name:index
assertAliaseSupportInActivateCommand(attacker, false);
player.addAction(turnNum, PhaseStep.DECLARE_BLOCKERS, "block:" + blocker + '$' + attacker);
addPlayerAction(player, turnNum, PhaseStep.DECLARE_BLOCKERS, "block:" + blocker + '$' + attacker);
}
public void blockSkip(int turnNum, TestPlayer player) {
@ -1751,10 +1818,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*
* @param player
* @param choice starting with "1" for mode 1, "2" for mode 2 and so on (to
* set multiple modes call the command multiple times). If a spell mode can
* be used only once like Demonic Pact, the value has to be set to the
* number of the remaining modes (e.g. if only 2 are left the number need to
* be 1 or 2).
* set multiple modes call the command multiple times). If a
* spell mode can be used only once like Demonic Pact, the
* value has to be set to the number of the remaining modes
* (e.g. if only 2 are left the number need to be 1 or 2).
*/
public void setModeChoice(TestPlayer player, String choice) {
player.addModeChoice(choice);
@ -1765,12 +1832,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
*
* @param player
* @param target you can add multiple targets by separating them by the "^"
* character e.g. "creatureName1^creatureName2" you can qualify the target
* additional by setcode e.g. "creatureName-M15" you can add [no copy] to
* the end of the target name to prohibit targets that are copied you can
* add [only copy] to the end of the target name to allow only targets that
* are copies. For modal spells use a prefix with the mode number:
* mode=1Lightning Bolt^mode=2Silvercoat Lion
* character e.g. "creatureName1^creatureName2" you can
* qualify the target additional by setcode e.g.
* "creatureName-M15" you can add [no copy] to the end of the
* target name to prohibit targets that are copied you can add
* [only copy] to the end of the target name to allow only
* targets that are copies. For modal spells use a prefix with
* the mode number: mode=1Lightning Bolt^mode=2Silvercoat Lion
*/
// TODO: mode options doesn't work here (see BrutalExpulsionTest)
public void addTarget(TestPlayer player, String target) {