Test framework: many improvements and fixes:

* added support to use custom cards with any abilities/effects (addCustomCardWithAbility);
 * added support of multiplayer games with all range (CardTestMultiPlayerBaseWithRangeAll);
 * added realtime checks for permanent counters (checkPermanentCounters);
 * added wrong attack commands check in strict mode;
 * fixed that added by addCard command cards don't init continues effects;
 * fixed that block commands don't removed from actions queue;
This commit is contained in:
Oleg Agafonov 2019-04-28 11:10:28 +04:00
parent 0bb735b482
commit dc04092fce
8 changed files with 163 additions and 22 deletions

View file

@ -19,6 +19,7 @@ import mage.cards.decks.Deck;
import mage.choices.Choice;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.counters.Counters;
import mage.designations.Designation;
import mage.designations.DesignationType;
@ -639,6 +640,13 @@ public class TestPlayer implements Player {
wasProccessed = true;
}
// check permanent counters: card name, counter type, count
if (params[0].equals(CHECK_COMMAND_PERMANENT_COUNTERS) && params.length == 4) {
assertPermanentCounters(action, game, computerPlayer, params[1], CounterType.findByName(params[2]), Integer.parseInt(params[3]));
actions.remove(action);
wasProccessed = true;
}
// check exile count: card name, count
if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) {
assertExileCount(action, game, computerPlayer, params[1], Integer.parseInt(params[2]));
@ -836,8 +844,8 @@ public class TestPlayer implements Player {
List<String> data = cards.stream()
.map(c -> (c.getIdName()
+ " - " + c.getPower().getValue()
+ "/" + c.getToughness().getValue()
+ " - " + c.getPower().getValue() + "/" + c.getToughness().getValue()
+ (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "")
+ ", " + (c.isTapped() ? "Tapped" : "Untapped")
+ (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName())
))
@ -949,6 +957,17 @@ public class TestPlayer implements Player {
Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must exists in " + count + " instances", count, foundedCount);
}
private void assertPermanentCounters(PlayerAction action, Game game, Player player, String permanentName, CounterType counterType, int count) {
int foundedCount = 0;
for (Permanent perm : game.getBattlefield().getAllPermanents()) {
if (perm.getName().equals(permanentName) && perm.getControllerId().equals(player.getId())) {
foundedCount = perm.getCounters(game).getCount(counterType);
}
}
Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundedCount);
}
private void assertExileCount(PlayerAction action, Game game, Player player, String permanentName, int count) {
int foundedCount = 0;
for (Card card : game.getExile().getAllCards(game)) {
@ -1151,10 +1170,12 @@ public class TestPlayer implements Player {
public void selectAttackers(Game game, UUID attackingPlayerId) {
// Loop through players and validate can attack/block this turn
UUID defenderId = null;
//List<PlayerAction>
boolean mustAttackByAction = false;
boolean madeAttackByAction = false;
for (Iterator<org.mage.test.player.PlayerAction> it = actions.iterator(); it.hasNext(); ) {
PlayerAction action = it.next();
if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("attack:")) {
mustAttackByAction = true;
String command = action.getAction();
command = command.substring(command.indexOf("attack:") + 7);
String[] groups = command.split("\\$");
@ -1198,9 +1219,13 @@ public class TestPlayer implements Player {
if (attacker != null && attacker.canAttack(defenderId, game)) {
computerPlayer.declareAttacker(attacker.getId(), defenderId, game, false);
it.remove();
madeAttackByAction = true;
}
}
}
if (mustAttackByAction && !madeAttackByAction) {
this.chooseStrictModeFailed(game, "select attackers must use attack command but don't");
}
}
@ -1212,10 +1237,12 @@ public class TestPlayer implements Player {
@Override
public void selectBlockers(Game game, UUID defendingPlayerId) {
List<PlayerAction> tempActions = new ArrayList<>(actions);
UUID opponentId = game.getOpponents(computerPlayer.getId()).iterator().next();
// Map of Blocker reference -> list of creatures blocked
Map<MageObjectReference, List<MageObjectReference>> blockedCreaturesByCreature = new HashMap<>();
for (PlayerAction action : actions) {
for (PlayerAction action : tempActions) {
if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("block:")) {
String command = action.getAction();
command = command.substring(command.indexOf("block:") + 6);
@ -1226,6 +1253,7 @@ public class TestPlayer implements Player {
Permanent blocker = findPermanent(new FilterControlledPermanent(), blockerName, computerPlayer.getId(), game);
if (canBlockAnother(game, blocker, attacker, blockedCreaturesByCreature)) {
computerPlayer.declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), game);
actions.remove(action);
} else {
throw new UnsupportedOperationException(blockerName + " cannot block " + attackerName + " it is already blocking the maximum amount of creatures.");
}

View file

@ -1,6 +1,5 @@
package org.mage.test.serverside.base;
import java.io.FileNotFoundException;
import mage.constants.MultiplayerAttackOption;
import mage.constants.RangeOfInfluence;
import mage.game.FreeForAll;
@ -9,13 +8,14 @@ import mage.game.GameException;
import mage.game.mulligan.VancouverMulligan;
import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl;
import java.io.FileNotFoundException;
/**
* Base class for testing single cards and effects in multiplayer game. For PvP
* games {
*
* @see CardTestPlayerBase}
*
* @author magenoxx_at_gmail.com
* @see CardTestPlayerBase}
*/
public abstract class CardTestMultiPlayerBase extends CardTestPlayerAPIImpl {
@ -29,5 +29,4 @@ public abstract class CardTestMultiPlayerBase extends CardTestPlayerAPIImpl {
playerD = createPlayer(game, playerD, "PlayerD");
return game;
}
}

View file

@ -0,0 +1,28 @@
package org.mage.test.serverside.base;
import mage.constants.MultiplayerAttackOption;
import mage.constants.RangeOfInfluence;
import mage.game.FreeForAll;
import mage.game.Game;
import mage.game.GameException;
import mage.game.mulligan.VancouverMulligan;
import org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl;
import java.io.FileNotFoundException;
/**
* @author JayDi82
*/
public abstract class CardTestMultiPlayerBaseWithRangeAll extends CardTestPlayerAPIImpl {
@Override
protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException {
Game game = new FreeForAll(MultiplayerAttackOption.LEFT, RangeOfInfluence.ALL, new VancouverMulligan(0), 20);
// Player order: A -> D -> C -> B
playerA = createPlayer(game, playerA, "PlayerA");
playerB = createPlayer(game, playerB, "PlayerB");
playerC = createPlayer(game, playerC, "PlayerC");
playerD = createPlayer(game, playerD, "PlayerD");
return game;
}
}

View file

@ -1,12 +1,15 @@
package org.mage.test.serverside.base;
import mage.abilities.Abilities;
import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep;
import mage.constants.RangeOfInfluence;
import mage.constants.Zone;
import mage.constants.*;
import mage.game.Game;
import mage.game.match.MatchType;
import mage.game.permanent.PermanentCard;
@ -33,7 +36,7 @@ import java.util.regex.Pattern;
/**
* Base class for all tests.
*
* @author ayratn
* @author ayratn, JayDi85
*/
public abstract class MageTestPlayerBase {
@ -333,10 +336,63 @@ public abstract class MageTestPlayerBase {
return new TestPlayer(new TestComputerPlayer(name, rangeOfInfluence));
}
public void setStrictChooseMode(boolean enable) {
protected void setStrictChooseMode(boolean enable) {
if (playerA != null) playerA.setChooseStrictMode(enable);
if (playerB != null) playerB.setChooseStrictMode(enable);
if (playerC != null) playerC.setChooseStrictMode(enable);
if (playerD != null) playerD.setChooseStrictMode(enable);
}
protected void addCustomCardWithAbility(String customName, TestPlayer controllerPlayer, Ability ability) {
// add custom card with selected ability to battlefield
CustomTestCard.clearCustomAbilities(customName);
CustomTestCard.addCustomAbility(customName, ability);
CardSetInfo testSet = new CardSetInfo(customName, "custom", "123", Rarity.COMMON);
PermanentCard card = new PermanentCard(new CustomTestCard(controllerPlayer.getId(), testSet), controllerPlayer.getId(), currentGame);
getBattlefieldCards(controllerPlayer).add(card);
}
}
// custom card with global abilities list to init (can contains abilities per card name)
class CustomTestCard extends CardImpl {
static private Map<String, Abilities<Ability>> abilitiesList = new HashMap<>(); // card name -> abilities
static protected void addCustomAbility(String cardName, Ability ability) {
if (!abilitiesList.containsKey(cardName)) {
abilitiesList.put(cardName, new AbilitiesImpl<>());
}
Abilities<Ability> oldAbilities = abilitiesList.get(cardName);
oldAbilities.add(ability);
}
static protected void clearCustomAbilities(String cardName) {
abilitiesList.remove(cardName);
}
static public void clearCustomAbilities() {
abilitiesList.clear();
}
public CustomTestCard(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "");
// load dynamic abilities by card name
Abilities<Ability> extraAbitilies = abilitiesList.get(setInfo.getName());
if (extraAbitilies != null) {
for (Ability ability : extraAbitilies) {
this.addAbility(ability.copy());
}
}
}
private CustomTestCard(final CustomTestCard card) {
super(card);
}
@Override
public CustomTestCard copy() {
return new CustomTestCard(this);
}
}

View file

@ -55,6 +55,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public static final String CHECK_COMMAND_LIFE = "LIFE";
public static final String CHECK_COMMAND_ABILITY = "ABILITY";
public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT";
public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS";
public static final String CHECK_COMMAND_EXILE_COUNT = "EXILE_COUNT";
public static final String CHECK_COMMAND_HAND_COUNT = "HAND_COUNT";
public static final String CHECK_COMMAND_HAND_CARD_COUNT = "HAND_CARD_COUNT";
@ -232,10 +233,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
+ " (found actions after stop on " + maxTurn + " / " + maxPhase + ")",
(maxTurn > this.stopOnTurn) || (maxTurn == this.stopOnTurn && maxPhase > this.stopAtStep.getIndex()));
for (Player player : currentGame.getPlayers().values()) {
TestPlayer testPlayer = (TestPlayer) player;
currentGame.cheat(player.getId(), getCommands(testPlayer));
currentGame.cheat(player.getId(), getLibraryCards(testPlayer), getHandCards(testPlayer),
currentGame.cheat(player.getId(), activePlayer.getId(), getLibraryCards(testPlayer), getHandCards(testPlayer),
getBattlefieldCards(testPlayer), getGraveCards(testPlayer));
}
@ -308,6 +310,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNT, permanentName, count.toString());
}
public void checkPermanentCounters(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, CounterType counterType, Integer count) {
check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNTERS, permanentName, counterType.toString(), count.toString());
}
public void checkExileCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) {
//Assert.assertNotEquals("", permanentName);
check(checkName, turnNum, step, player, CHECK_COMMAND_EXILE_COUNT, permanentName, count.toString());
@ -1231,6 +1237,16 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
+ "])", count, player.getActions().size());
}
public void assertActionsMustBeEmpty(TestPlayer player) throws AssertionError {
if (!player.getActions().isEmpty()) {
System.out.println("Remaining actions for " + player.getName() + " (" + player.getActions().size() + "):");
player.getActions().stream().forEach(a -> {
System.out.println("* turn " + a.getTurnNum() + " - " + a.getStep() + ": " + a.getActionName());
});
Assert.fail("Player " + player.getName() + " must have 0 actions but found " + player.getActions().size());
}
}
public void assertChoicesCount(TestPlayer player, int count) throws AssertionError {
Assert.assertEquals("(Choices of " + player.getName() + ") Count are not equal (found " + player.getChoices() + ")", count, player.getChoices().size());
}
@ -1242,7 +1258,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public void assertAllCommandsUsed() throws AssertionError {
for (Player player : currentGame.getPlayers().values()) {
TestPlayer testPlayer = (TestPlayer) player;
assertActionsCount(testPlayer, 0);
assertActionsMustBeEmpty(testPlayer);
assertChoicesCount(testPlayer, 0);
assertTargetsCount(testPlayer, 0);
}

View file

@ -194,6 +194,11 @@ public enum CounterType {
}
}
@Override
public String toString() {
return name;
}
public static CounterType findByName(String name) {
for (CounterType counterType : values()) {
if (counterType.getName().equals(name)) {

View file

@ -25,7 +25,6 @@ import mage.game.events.GameEvent;
import mage.game.events.Listener;
import mage.game.events.PlayerQueryEvent;
import mage.game.events.TableEvent;
import mage.game.match.Match;
import mage.game.match.MatchType;
import mage.game.mulligan.Mulligan;
import mage.game.permanent.Battlefield;
@ -433,7 +432,7 @@ public interface Game extends MageItem, Serializable {
// game cheats (for tests only)
void cheat(UUID ownerId, Map<Zone, String> commands);
void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard);
void cheat(UUID ownerId, UUID activePlayerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard);
// controlling the behaviour of replacement effects while permanents entering the battlefield
void setScopeRelevant(boolean scopeRelevant);

View file

@ -40,7 +40,6 @@ import mage.game.command.Emblem;
import mage.game.command.Plane;
import mage.game.events.*;
import mage.game.events.TableEvent.EventType;
import mage.game.mulligan.LondonMulligan;
import mage.game.mulligan.Mulligan;
import mage.game.permanent.Battlefield;
import mage.game.permanent.Permanent;
@ -128,7 +127,7 @@ public abstract class GameImpl implements Game, Serializable {
private int priorityTime;
private final int startLife;
protected PlayerList playerList;
protected PlayerList playerList; // auto-generated from state, don't copy
// infinite loop check (no copy of this attributes neccessary)
private int infiniteLoopCounter; // used to check if the game is in an infinite loop
@ -138,8 +137,9 @@ public abstract class GameImpl implements Game, Serializable {
// used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist)
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
// used to proceed player conceding requests
private final LinkedList<UUID> concedingPlayers = new LinkedList<>(); // used to handle asynchronous request of a player to leave the game
// temporary store for income concede commands, don't copy
private final LinkedList<UUID> concedingPlayers = new LinkedList<>();
public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) {
this.id = UUID.randomUUID();
@ -2845,7 +2845,7 @@ public abstract class GameImpl implements Game, Serializable {
}
@Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard) {
public void cheat(UUID ownerId, UUID activePlayerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard) {
Player player = getPlayer(ownerId);
if (player != null) {
loadCards(ownerId, library);
@ -2864,6 +2864,8 @@ public abstract class GameImpl implements Game, Serializable {
card.setZone(Zone.GRAVEYARD, this);
player.getGraveyard().add(card);
}
// warning, permanents go to battlefield without resolve, continuus effects must be init
for (PermanentCard permanentCard : battlefield) {
permanentCard.setZone(Zone.BATTLEFIELD, this);
permanentCard.setOwnerId(ownerId);
@ -2876,6 +2878,14 @@ public abstract class GameImpl implements Game, Serializable {
if (permanentCard.isTapped()) {
newPermanent.setTapped(true);
}
// init effects on static abilities (init continuous effects, warning, game state contains copy)
for (ContinuousEffect effect : this.getState().getContinuousEffects().getLayeredEffects(this)) {
Optional<Ability> ability = this.getState().getContinuousEffects().getLayeredEffectAbilities(effect).stream().findFirst();
if (ability.isPresent() && newPermanent.getId().equals(ability.get().getSourceId())) {
effect.init(ability.get(), this, activePlayerId); // game is not setup yet, game.activePlayer is null -- need direct id
}
}
}
applyEffects();
}