game engine, tests and other fixes:

tests: fixed wrong permanent structure for battlefield cards (addCard command);
tests: added docs and additional runtime checks;
game: Modal double-faced cards - improved support, no more other side effects on battlefield;
game: Copy abilities - improved stability and cards support;
game: Player under control - improved stability and related cards support (possible NPE errors, additional runtime checks);
server: fixed bloated logs with game timer;
AI: fixed wrong timer in computer games;
This commit is contained in:
Oleg Agafonov 2024-02-17 19:35:44 +04:00
parent 824e4c6b7a
commit 229e8d3075
35 changed files with 303 additions and 151 deletions

View file

@ -889,9 +889,10 @@ public class CopySpellTest extends CardTestPlayerBase {
}
private void prepareZoneAndZCC(Card originalCard) {
// prepare custom zcc and zone for copy testing
// prepare custom zcc and zone for copy testing (it's not real game)
// HAND zone is safest way (some card types require diff zones for stack/battlefield, e.g. MDFC)
originalCard.setZoneChangeCounter(5, currentGame);
originalCard.setZone(Zone.STACK, currentGame);
originalCard.setZone(Zone.HAND, currentGame);
}
private void cardsMustHaveSameZoneAndZCC(Card originalCard, Card copiedCard, String infoPrefix) {

View file

@ -999,4 +999,33 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase {
setStopAt(2, PhaseStep.END_TURN);
execute();
}
@Test
public void test_Battlefield_MustHaveAbilitiesFromOneSideOnly() {
// possible bug: test framework adds second side abilities
// left side - Reidane, God of the Worthy:
// Snow lands your opponents control enter the battlefield tapped.
// right side - Valkmira, Protector's Shield:
// If a source an opponent controls would deal damage to you or a permanent you control, prevent 1 of that damage.
// Whenever you or another permanent you control becomes the target of a spell or ability an opponent controls,
// counter that spell or ability unless its controller pays {1}.
addCard(Zone.BATTLEFIELD, playerB, "Reidane, God of the Worthy", 1);
//
addCard(Zone.HAND, playerA, "Snow-Covered Forest", 1);
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
// cast, second side effects must be ignored (e.g. counter trigger)
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snow-Covered Forest");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt");
addTarget(playerA, playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertTappedCount("Snow-Covered Forest", true, 1);
assertLife(playerB, 20 - 3);
}
}

View file

@ -22,6 +22,7 @@ import mage.cards.repository.CardRepository;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.PutToBattlefieldInfo;
import mage.game.match.Match;
import mage.game.match.MatchType;
import mage.game.permanent.PermanentCard;
@ -72,7 +73,7 @@ public abstract class MageTestPlayerBase {
protected Pattern pattern = Pattern.compile("([a-zA-Z]*):([\\w]*):([a-zA-Z ,\\-.!'\\d]*):([\\d]*)(:\\{tapped\\})?");
protected Map<TestPlayer, List<Card>> handCards = new HashMap<>();
protected Map<TestPlayer, List<PermanentCard>> battlefieldCards = new HashMap<>();
protected Map<TestPlayer, List<PutToBattlefieldInfo>> battlefieldCards = new HashMap<>(); // cards + additional status like tapped
protected Map<TestPlayer, List<Card>> graveyardCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> libraryCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> commandCards = new HashMap<>();
@ -111,7 +112,7 @@ public abstract class MageTestPlayerBase {
EXPECTED
}
protected ParserState parserState;
protected ParserState parserState; // TODO: remove outdated and unsued code
/**
* Expected results of the test. Read from test case in {@link String} based
@ -216,6 +217,7 @@ public abstract class MageTestPlayerBase {
}
private void parseLine(String line) {
// TODO: delete unused code
if (parserState == ParserState.EXPECTED) {
expectedResults.add(line); // just remember for future use
return;
@ -229,14 +231,14 @@ public abstract class MageTestPlayerBase {
if (nickname.startsWith("Computer")) {
List<Card> cards = null;
List<PermanentCard> perms = null;
//List<PermanentCard> perms = null;
Zone gameZone;
if ("hand".equalsIgnoreCase(zone)) {
gameZone = Zone.HAND;
cards = getHandCards(getPlayer(nickname));
} else if ("battlefield".equalsIgnoreCase(zone)) {
gameZone = Zone.BATTLEFIELD;
perms = getBattlefieldCards(getPlayer(nickname));
//perms = getBattlefieldCards(getPlayer(nickname));
} else if ("graveyard".equalsIgnoreCase(zone)) {
gameZone = Zone.GRAVEYARD;
cards = getGraveCards(getPlayer(nickname));
@ -268,10 +270,10 @@ public abstract class MageTestPlayerBase {
Card newCard = cardInfo != null ? cardInfo.getCard() : null;
if (newCard != null) {
if (gameZone == Zone.BATTLEFIELD) {
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
PermanentCard p = new PermanentCard(permCard, null, currentGame);
p.setTapped(tapped);
perms.add(p);
//Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
//PermanentCard p = new PermanentCard(permCard, null, currentGame);
//p.setTapped(tapped);
//perms.add(p);
} else {
cards.add(newCard);
}
@ -339,11 +341,11 @@ public abstract class MageTestPlayerBase {
return res;
}
protected List<PermanentCard> getBattlefieldCards(TestPlayer player) {
protected List<PutToBattlefieldInfo> getBattlefieldCards(TestPlayer player) {
if (battlefieldCards.containsKey(player)) {
return battlefieldCards.get(player);
}
List<PermanentCard> res = new ArrayList<>();
List<PutToBattlefieldInfo> res = new ArrayList<>();
battlefieldCards.put(player, res);
return res;
}
@ -437,12 +439,13 @@ public abstract class MageTestPlayerBase {
CardSetInfo testSet = new CardSetInfo(needCardName, needSetCode, "123", Rarity.COMMON);
Card newCard = new CustomTestCard(controllerPlayer.getId(), testSet, cardType, spellCost);
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
PermanentCard permanent = new PermanentCard(permCard, controllerPlayer.getId(), currentGame);
switch (putAtZone) {
case BATTLEFIELD:
getBattlefieldCards(controllerPlayer).add(permanent);
getBattlefieldCards(controllerPlayer).add(new PutToBattlefieldInfo(
newCard,
false
));
break;
case GRAVEYARD:
getGraveCards(controllerPlayer).add(newCard);
@ -565,7 +568,7 @@ public abstract class MageTestPlayerBase {
}
}
// custom card with global abilities list to init (can contains abilities per card name)
// custom card with global abilities list to init (can contain abilities per card name)
class CustomTestCard extends CardImpl {
static private final Map<String, Abilities<Ability>> abilitiesList = new HashMap<>(); // card name -> abilities

View file

@ -631,7 +631,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
/**
* Add any amount of cards to specified zone of specified player.
* Add any amount of cards to specified zone of specified player without resolve/ETB
*
* @param gameZone {@link mage.constants.Zone} to add cards to.
* @param player {@link Player} to add cards for. Use either playerA or
@ -684,14 +684,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
if (gameZone == Zone.BATTLEFIELD) {
for (int i = 0; i < count; i++) {
Card newCard = cardInfo.getCard();
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
PermanentCard p = new PermanentCard(permCard, player.getId(), currentGame);
p.setTapped(tapped);
getBattlefieldCards(player).add(p);
getBattlefieldCards(player).add(new PutToBattlefieldInfo(
newCard,
tapped
));
if (!aliasName.isEmpty()) {
player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), p.getId());
// TODO: is it bugged with double faced cards (wrong ref)?
player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), newCard.getId());
}
}
} else {

View file

@ -78,14 +78,16 @@ public class AddCardApiTest extends CardTestPlayerBase {
execute();
assertPermanentCount(playerA, "Memorial to Glory", 2);
getBattlefieldCards(playerA).stream()
.filter(card -> card.getName().equals("Memorial to Glory"))
.forEach(card -> Assert.assertEquals("40K", card.getExpansionSetCode()));
getBattlefieldCards(playerA)
.stream()
.filter(info -> info.getCard().getName().equals("Memorial to Glory"))
.forEach(info -> Assert.assertEquals("40K", info.getCard().getExpansionSetCode()));
assertPermanentCount(playerA, "Plains", 2);
getBattlefieldCards(playerA).stream()
.filter(card -> card.getName().equals("Plains"))
.forEach(card -> Assert.assertEquals("PANA", card.getExpansionSetCode()));
getBattlefieldCards(playerA)
.stream()
.filter(info -> info.getCard().getName().equals("Plains"))
.forEach(info -> Assert.assertEquals("PANA", info.getCard().getExpansionSetCode()));
}
@Test(expected = org.junit.ComparisonFailure.class)

View file

@ -16,7 +16,7 @@ public class ExtraTurnsTest extends CardTestPlayerBase {
private void checkTurnControl(int turn, TestPlayer needTurnController, boolean isExtraTurn) {
runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
Player defaultTurnController = game.getPlayer(game.getActivePlayerId());
Player realTurnController = defaultTurnController.getTurnControlledBy() == null ? defaultTurnController : game.getPlayer(defaultTurnController.getTurnControlledBy());
Player realTurnController = game.getPlayer(defaultTurnController.getTurnControlledBy());
Assert.assertEquals(String.format("turn %d must be controlled by %s", turn, needTurnController.getName()),
needTurnController.getName(), realTurnController.getName());
Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")),