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

@ -798,7 +798,7 @@ public final class SystemUtil {
// TODO: replace by player.move? // TODO: replace by player.move?
switch (zone) { switch (zone) {
case BATTLEFIELD: case BATTLEFIELD:
CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player); CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player, false);
break; break;
case LIBRARY: case LIBRARY:
card.setZone(Zone.LIBRARY, game); card.setZone(Zone.LIBRARY, game);

View file

@ -1301,7 +1301,6 @@ public class ComputerPlayer extends PlayerImpl {
@Override @Override
public boolean priority(Game game) { public boolean priority(Game game) {
game.resumeTimer(getTurnControlledBy()); game.resumeTimer(getTurnControlledBy());
log.debug("priority");
boolean result = priorityPlay(game); boolean result = priorityPlay(game);
game.pauseTimer(getTurnControlledBy()); game.pauseTimer(getTurnControlledBy());
return result; return result;

View file

@ -24,7 +24,6 @@ import mage.players.Player;
import mage.server.Main; import mage.server.Main;
import mage.server.User; import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.util.Splitter;
import mage.util.MultiAmountMessage; import mage.util.MultiAmountMessage;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import mage.utils.timer.PriorityTimer; import mage.utils.timer.PriorityTimer;
@ -174,8 +173,8 @@ public class GameController implements GameCallback {
timer.pause(); timer.pause();
break; break;
} }
} catch (MageException ex) { } catch (MageException e) {
logger.fatal("Table event listener error ", ex); logger.fatal("Table event listener error: " + e, e);
} }
} }
); );
@ -992,29 +991,24 @@ public class GameController implements GameCallback {
} }
private void perform(UUID playerId, Command command, boolean informOthers) { private void perform(UUID playerId, Command command, boolean informOthers) {
if (game.getPlayer(playerId).isGameUnderControl()) { // is the player controlling it's own turn Player player = game.getPlayer(playerId);
if (gameSessions.containsKey(playerId)) { if (player == null) {
setupTimeout(playerId); throw new IllegalArgumentException("Can't perform command for unknown player id: " + playerId);
command.execute(playerId); }
}
// TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case Player realPlayerController = game.getPlayer(player.getTurnControlledBy());
// same for another player (can be fixed by super-duper connection) if (realPlayerController == null) {
if (informOthers) { throw new IllegalArgumentException("Can't find real turn controller for player id: " + playerId);
informOthers(playerId); }
}
} else { if (gameSessions.containsKey(realPlayerController.getId())) {
List<UUID> players = Splitter.split(game, playerId); setupTimeout(realPlayerController.getId());
for (UUID uuid : players) { command.execute(realPlayerController.getId());
if (gameSessions.containsKey(uuid)) { }
setupTimeout(uuid); // TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case
command.execute(uuid); // same for another player (can be fixed by super-duper connection)
} if (informOthers) {
} informOthers(playerId);
// TODO: if watcher disconnects then game freeze with active timer, must be fix for such use case
// same for another player (can be fixed by super-duper connection)
if (informOthers) {
informOthers(players);
}
} }
} }

View file

@ -1,25 +0,0 @@
package mage.server.util;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.game.Game;
import mage.players.Player;
/**
* @author nantuko
*/
public final class Splitter {
private Splitter(){}
public static List<UUID> split(Game game, UUID playerId) {
List<UUID> players = new ArrayList<>();
//players.add(playerId); // add original player
Player player = game.getPlayer(playerId);
if (player != null && player.getTurnControlledBy() != null) {
players.add(player.getTurnControlledBy());
}
return players;
}
}

View file

@ -81,11 +81,8 @@ class DeceiverOfFormEffect extends OneShotEffect {
&& ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard().isCreature(game)) { && ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard().isCreature(game)) {
copyFromCard = ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard(); copyFromCard = ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard();
} }
Permanent newBluePrint = null; Permanent newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
newBluePrint.assignNewId();
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, permanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, permanent.getId());
copyEffect.newId();
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);
game.addEffect(copyEffect, newAbility); game.addEffect(copyEffect, newAbility);

View file

@ -131,7 +131,6 @@ class DermotaxiCopyEffect extends OneShotEffect {
DermotaxiCopyApplier applier = new DermotaxiCopyApplier(); DermotaxiCopyApplier applier = new DermotaxiCopyApplier();
applier.apply(game, newBluePrint, source, sourcePermanent.getId()); applier.apply(game, newBluePrint, source, sourcePermanent.getId());
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, sourcePermanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, sourcePermanent.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
game.addEffect(copyEffect, source); game.addEffect(copyEffect, source);
return true; return true;

View file

@ -86,9 +86,8 @@ class DimirDoppelgangerEffect extends OneShotEffect {
CopyApplier applier = new DimirDoppelgangerCopyApplier(); CopyApplier applier = new DimirDoppelgangerCopyApplier();
applier.apply(game, newBluePrint, source, dimirDoppelganger.getId()); applier.apply(game, newBluePrint, source, dimirDoppelganger.getId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, dimirDoppelganger.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, dimirDoppelganger.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy(); // TODO: why it copy new ability instead source? Some cards use it, some miss
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);
game.addEffect(copyEffect, newAbility); game.addEffect(copyEffect, newAbility);
} }

View file

@ -99,7 +99,6 @@ class EchoingDeepsEffect extends OneShotEffect {
CopyApplier applier = new EchoingDeepsApplier(); CopyApplier applier = new EchoingDeepsApplier();
applier.apply(game, newBluePrint, source, source.getSourceId()); applier.apply(game, newBluePrint, source, source.getSourceId());
CopyEffect copyEffect = new CopyEffect(Duration.WhileOnBattlefield, newBluePrint, source.getSourceId()); CopyEffect copyEffect = new CopyEffect(Duration.WhileOnBattlefield, newBluePrint, source.getSourceId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
copyEffect.init(source, game); copyEffect.init(source, game);
game.addEffect(copyEffect, source); game.addEffect(copyEffect, source);

View file

@ -110,6 +110,7 @@ class IdentityThiefEffect extends OneShotEffect {
ContinuousEffect copyEffect = new CopyEffect(Duration.EndOfTurn, targetPermanent, source.getSourceId()); ContinuousEffect copyEffect = new CopyEffect(Duration.EndOfTurn, targetPermanent, source.getSourceId());
copyEffect.setTargetPointer(new FixedTarget(sourcePermanent.getId(), game)); copyEffect.setTargetPointer(new FixedTarget(sourcePermanent.getId(), game));
game.addEffect(copyEffect, source); game.addEffect(copyEffect, source);
UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
if (controller.moveCardsToExile(targetPermanent, source, game, true, exileZoneId, sourcePermanent.getName())) { if (controller.moveCardsToExile(targetPermanent, source, game, true, exileZoneId, sourcePermanent.getName())) {
Effect effect = new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, true); Effect effect = new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, true);

View file

@ -85,7 +85,6 @@ class LazavDimirMastermindEffect extends OneShotEffect {
CopyApplier applier = new LazavDimirMastermindCopyApplier(); CopyApplier applier = new LazavDimirMastermindCopyApplier();
applier.apply(game, newBluePrint, source, lazavDimirMastermind.getId()); applier.apply(game, newBluePrint, source, lazavDimirMastermind.getId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavDimirMastermind.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavDimirMastermind.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -115,7 +115,6 @@ class LazavTheMultifariousEffect extends OneShotEffect {
CopyApplier applier = new LazavTheMultifariousCopyApplier(); CopyApplier applier = new LazavTheMultifariousCopyApplier();
applier.apply(game, newBluePrint, source, lazavTheMultifarious.getId()); applier.apply(game, newBluePrint, source, lazavTheMultifarious.getId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavTheMultifarious.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavTheMultifarious.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -95,6 +95,7 @@ class LazotepConvertCopyEffect extends OneShotEffect {
} }
Card modifiedCopy = copyFromCard.copy(); Card modifiedCopy = copyFromCard.copy();
//Appliers must be applied before CopyEffect, its applier setting is just for copies of copies //Appliers must be applied before CopyEffect, its applier setting is just for copies of copies
// TODO: research applier usage, why it here
applier.apply(game, modifiedCopy, source, source.getSourceId()); applier.apply(game, modifiedCopy, source, source.getSourceId());
game.addEffect(new CopyEffect( game.addEffect(new CopyEffect(
Duration.Custom, modifiedCopy, source.getSourceId() Duration.Custom, modifiedCopy, source.getSourceId()

View file

@ -110,7 +110,6 @@ class LikenessLooterEffect extends OneShotEffect {
CopyApplier applier = new LikenessLooterCopyApplier(); CopyApplier applier = new LikenessLooterCopyApplier();
applier.apply(game, newBluePrint, source, permanent.getId()); applier.apply(game, newBluePrint, source, permanent.getId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, permanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, permanent.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -88,7 +88,6 @@ class OlagLudevicsHubrisEffect extends ReplacementEffectImpl {
CopyApplier applier = new OlagLudevicsHubrisCopyApplier(); CopyApplier applier = new OlagLudevicsHubrisCopyApplier();
applier.apply(game, newBluePrint, source, source.getSourceId()); applier.apply(game, newBluePrint, source, source.getSourceId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, source.getSourceId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, source.getSourceId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -99,7 +99,6 @@ class ShadowKinEffect extends OneShotEffect {
CopyApplier applier = new ShadowKinApplier(); CopyApplier applier = new ShadowKinApplier();
applier.apply(game, blueprint, source, sourcePermanent.getId()); applier.apply(game, blueprint, source, sourcePermanent.getId());
CopyEffect copyEffect = new CopyEffect(Duration.Custom, blueprint, sourcePermanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, blueprint, sourcePermanent.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -175,7 +175,6 @@ class TheMyriadPoolsCopyEffect extends OneShotEffect {
newBluePrint = new PermanentCard(copyFromCardOnStack, source.getControllerId(), game); newBluePrint = new PermanentCard(copyFromCardOnStack, source.getControllerId(), game);
newBluePrint.assignNewId(); newBluePrint.assignNewId();
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, targetPermanentToCopyTo.getId()); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, targetPermanentToCopyTo.getId());
copyEffect.newId();
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);
game.addEffect(copyEffect, newAbility); game.addEffect(copyEffect, newAbility);

View file

@ -91,7 +91,6 @@ class VolrathTheShapestealerEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
Permanent volrathTheShapestealer = game.getPermanent(source.getSourceId()); Permanent volrathTheShapestealer = game.getPermanent(source.getSourceId());
Permanent newBluePrint = null;
if (controller == null if (controller == null
|| volrathTheShapestealer == null) { || volrathTheShapestealer == null) {
return false; return false;
@ -100,12 +99,12 @@ class VolrathTheShapestealerEffect extends OneShotEffect {
if (copyFromCard == null) { if (copyFromCard == null) {
return true; return true;
} }
newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game); //newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
Card newBluePrint = copyFromCard.copy();
newBluePrint.assignNewId(); newBluePrint.assignNewId();
CopyApplier applier = new VolrathTheShapestealerCopyApplier(); CopyApplier applier = new VolrathTheShapestealerCopyApplier();
applier.apply(game, newBluePrint, source, volrathTheShapestealer.getId()); applier.apply(game, newBluePrint, source, volrathTheShapestealer.getId());
CopyEffect copyEffect = new CopyEffect(Duration.UntilYourNextTurn, newBluePrint, volrathTheShapestealer.getId()); CopyEffect copyEffect = new CopyEffect(Duration.UntilYourNextTurn, newBluePrint, volrathTheShapestealer.getId());
copyEffect.newId();
copyEffect.setApplier(applier); copyEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
copyEffect.init(newAbility, game); copyEffect.init(newAbility, game);

View file

@ -889,9 +889,10 @@ public class CopySpellTest extends CardTestPlayerBase {
} }
private void prepareZoneAndZCC(Card originalCard) { 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.setZoneChangeCounter(5, currentGame);
originalCard.setZone(Zone.STACK, currentGame); originalCard.setZone(Zone.HAND, currentGame);
} }
private void cardsMustHaveSameZoneAndZCC(Card originalCard, Card copiedCard, String infoPrefix) { 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); setStopAt(2, PhaseStep.END_TURN);
execute(); 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.constants.*;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.PutToBattlefieldInfo;
import mage.game.match.Match; import mage.game.match.Match;
import mage.game.match.MatchType; import mage.game.match.MatchType;
import mage.game.permanent.PermanentCard; 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 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<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>> graveyardCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> libraryCards = new HashMap<>(); protected Map<TestPlayer, List<Card>> libraryCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> commandCards = new HashMap<>(); protected Map<TestPlayer, List<Card>> commandCards = new HashMap<>();
@ -111,7 +112,7 @@ public abstract class MageTestPlayerBase {
EXPECTED 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 * 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) { private void parseLine(String line) {
// TODO: delete unused code
if (parserState == ParserState.EXPECTED) { if (parserState == ParserState.EXPECTED) {
expectedResults.add(line); // just remember for future use expectedResults.add(line); // just remember for future use
return; return;
@ -229,14 +231,14 @@ public abstract class MageTestPlayerBase {
if (nickname.startsWith("Computer")) { if (nickname.startsWith("Computer")) {
List<Card> cards = null; List<Card> cards = null;
List<PermanentCard> perms = null; //List<PermanentCard> perms = null;
Zone gameZone; Zone gameZone;
if ("hand".equalsIgnoreCase(zone)) { if ("hand".equalsIgnoreCase(zone)) {
gameZone = Zone.HAND; gameZone = Zone.HAND;
cards = getHandCards(getPlayer(nickname)); cards = getHandCards(getPlayer(nickname));
} else if ("battlefield".equalsIgnoreCase(zone)) { } else if ("battlefield".equalsIgnoreCase(zone)) {
gameZone = Zone.BATTLEFIELD; gameZone = Zone.BATTLEFIELD;
perms = getBattlefieldCards(getPlayer(nickname)); //perms = getBattlefieldCards(getPlayer(nickname));
} else if ("graveyard".equalsIgnoreCase(zone)) { } else if ("graveyard".equalsIgnoreCase(zone)) {
gameZone = Zone.GRAVEYARD; gameZone = Zone.GRAVEYARD;
cards = getGraveCards(getPlayer(nickname)); cards = getGraveCards(getPlayer(nickname));
@ -268,10 +270,10 @@ public abstract class MageTestPlayerBase {
Card newCard = cardInfo != null ? cardInfo.getCard() : null; Card newCard = cardInfo != null ? cardInfo.getCard() : null;
if (newCard != null) { if (newCard != null) {
if (gameZone == Zone.BATTLEFIELD) { if (gameZone == Zone.BATTLEFIELD) {
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); //Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
PermanentCard p = new PermanentCard(permCard, null, currentGame); //PermanentCard p = new PermanentCard(permCard, null, currentGame);
p.setTapped(tapped); //p.setTapped(tapped);
perms.add(p); //perms.add(p);
} else { } else {
cards.add(newCard); cards.add(newCard);
} }
@ -339,11 +341,11 @@ public abstract class MageTestPlayerBase {
return res; return res;
} }
protected List<PermanentCard> getBattlefieldCards(TestPlayer player) { protected List<PutToBattlefieldInfo> getBattlefieldCards(TestPlayer player) {
if (battlefieldCards.containsKey(player)) { if (battlefieldCards.containsKey(player)) {
return battlefieldCards.get(player); return battlefieldCards.get(player);
} }
List<PermanentCard> res = new ArrayList<>(); List<PutToBattlefieldInfo> res = new ArrayList<>();
battlefieldCards.put(player, res); battlefieldCards.put(player, res);
return res; return res;
} }
@ -437,12 +439,13 @@ public abstract class MageTestPlayerBase {
CardSetInfo testSet = new CardSetInfo(needCardName, needSetCode, "123", Rarity.COMMON); CardSetInfo testSet = new CardSetInfo(needCardName, needSetCode, "123", Rarity.COMMON);
Card newCard = new CustomTestCard(controllerPlayer.getId(), testSet, cardType, spellCost); 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) { switch (putAtZone) {
case BATTLEFIELD: case BATTLEFIELD:
getBattlefieldCards(controllerPlayer).add(permanent); getBattlefieldCards(controllerPlayer).add(new PutToBattlefieldInfo(
newCard,
false
));
break; break;
case GRAVEYARD: case GRAVEYARD:
getGraveCards(controllerPlayer).add(newCard); 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 { class CustomTestCard extends CardImpl {
static private final Map<String, Abilities<Ability>> abilitiesList = new HashMap<>(); // card name -> abilities 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 gameZone {@link mage.constants.Zone} to add cards to.
* @param player {@link Player} to add cards for. Use either playerA or * @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) { if (gameZone == Zone.BATTLEFIELD) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Card newCard = cardInfo.getCard(); Card newCard = cardInfo.getCard();
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); getBattlefieldCards(player).add(new PutToBattlefieldInfo(
newCard,
PermanentCard p = new PermanentCard(permCard, player.getId(), currentGame); tapped
p.setTapped(tapped); ));
getBattlefieldCards(player).add(p);
if (!aliasName.isEmpty()) { 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 { } else {

View file

@ -78,14 +78,16 @@ public class AddCardApiTest extends CardTestPlayerBase {
execute(); execute();
assertPermanentCount(playerA, "Memorial to Glory", 2); assertPermanentCount(playerA, "Memorial to Glory", 2);
getBattlefieldCards(playerA).stream() getBattlefieldCards(playerA)
.filter(card -> card.getName().equals("Memorial to Glory")) .stream()
.forEach(card -> Assert.assertEquals("40K", card.getExpansionSetCode())); .filter(info -> info.getCard().getName().equals("Memorial to Glory"))
.forEach(info -> Assert.assertEquals("40K", info.getCard().getExpansionSetCode()));
assertPermanentCount(playerA, "Plains", 2); assertPermanentCount(playerA, "Plains", 2);
getBattlefieldCards(playerA).stream() getBattlefieldCards(playerA)
.filter(card -> card.getName().equals("Plains")) .stream()
.forEach(card -> Assert.assertEquals("PANA", card.getExpansionSetCode())); .filter(info -> info.getCard().getName().equals("Plains"))
.forEach(info -> Assert.assertEquals("PANA", info.getCard().getExpansionSetCode()));
} }
@Test(expected = org.junit.ComparisonFailure.class) @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) { private void checkTurnControl(int turn, TestPlayer needTurnController, boolean isExtraTurn) {
runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> { runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
Player defaultTurnController = game.getPlayer(game.getActivePlayerId()); 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()), Assert.assertEquals(String.format("turn %d must be controlled by %s", turn, needTurnController.getName()),
needTurnController.getName(), realTurnController.getName()); needTurnController.getName(), realTurnController.getName());
Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")), Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")),

View file

@ -9,6 +9,7 @@ import mage.game.Game;
import mage.players.Player; import mage.players.Player;
/** /**
* TODO: delete, there are already end turn code with control reset
* @author nantuko * @author nantuko
*/ */
public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect { public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect {

View file

@ -94,15 +94,29 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
@Override @Override
public void setCopy(boolean isCopy, MageObject copiedFrom) { public void setCopy(boolean isCopy, MageObject copiedFrom) {
super.setCopy(isCopy, copiedFrom); super.setCopy(isCopy, copiedFrom);
leftHalfCard.setCopy(isCopy, copiedFrom); leftHalfCard.setCopy(isCopy, copiedFrom); // TODO: must check copiedFrom and assign sides? (??? related to #8476 ???)
rightHalfCard.setCopy(isCopy, copiedFrom); rightHalfCard.setCopy(isCopy, copiedFrom);
} }
private void setSideZones(Zone mainZone, Game game) {
switch (mainZone) {
case BATTLEFIELD:
case STACK:
throw new IllegalArgumentException("Wrong code usage: you must put to battlefield/stack only real side card (half), not main");
default:
// must keep both sides in same zone cause xmage need access to cost reduction, spell
// and other abilities before put it to stack (in playable calcs)
game.setZone(leftHalfCard.getId(), mainZone);
game.setZone(rightHalfCard.getId(), mainZone);
break;
}
checkGoodZones(game, this);
}
@Override @Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) { public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) { if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
game.getState().setZone(leftHalfCard.getId(), toZone); setSideZones(toZone, game);
game.getState().setZone(rightHalfCard.getId(), toZone);
return true; return true;
} }
return false; return false;
@ -111,21 +125,69 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
@Override @Override
public void setZone(Zone zone, Game game) { public void setZone(Zone zone, Game game) {
super.setZone(zone, game); super.setZone(zone, game);
game.setZone(leftHalfCard.getId(), zone); setSideZones(zone, game);
game.setZone(rightHalfCard.getId(), zone);
} }
@Override @Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) { public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
if (super.moveToExile(exileId, name, source, game, appliedEffects)) { if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
Zone currentZone = game.getState().getZone(getId()); setSideZones(Zone.EXILED, game);
game.getState().setZone(leftHalfCard.getId(), currentZone);
game.getState().setZone(rightHalfCard.getId(), currentZone);
return true; return true;
} }
return false; return false;
} }
/**
* Runtime check for good zones and other MDF data
*/
public static void checkGoodZones(Game game, ModalDoubleFacedCard card) {
Card leftPart = card.getLeftHalfCard();
Card rightPart = card.getRightHalfCard();
Zone zoneMain = game.getState().getZone(card.getId());
Zone zoneLeft = game.getState().getZone(leftPart.getId());
Zone zoneRight = game.getState().getZone(rightPart.getId());
// runtime check:
// * in battlefield and stack - card + one of the sides (another side in outside zone)
// * in other zones - card + both sides (need both sides due cost reductions, spell and other access before put to stack)
//
// 712.8a While a double-faced card is outside the game or in a zone other than the battlefield or stack,
// it has only the characteristics of its front face.
//
// 712.8f While a modal double-faced spell is on the stack or a modal double-faced permanent is on the battlefield,
// it has only the characteristics of the face thats up.
Zone needZoneLeft;
Zone needZoneRight;
switch (zoneMain) {
case BATTLEFIELD:
case STACK:
if (zoneMain == zoneLeft) {
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
} else if (zoneMain == zoneRight) {
needZoneLeft = Zone.OUTSIDE;
needZoneRight = zoneMain;
} else {
// impossible
needZoneLeft = zoneMain;
needZoneRight = Zone.OUTSIDE;
}
break;
default:
needZoneLeft = zoneMain;
needZoneRight = zoneMain;
break;
}
if (zoneLeft != needZoneLeft || zoneRight != needZoneRight) {
throw new IllegalStateException("Wrong code usage: MDF card uses wrong zones - " + card
+ "\r\n" + String.format("* main zone: %s", zoneMain)
+ "\r\n" + String.format("* left side: need %s, actual %s", needZoneLeft, zoneLeft)
+ "\r\n" + String.format("* right side: need %s, actual %s", needZoneRight, zoneRight));
}
}
@Override @Override
public boolean removeFromZone(Game game, Zone fromZone, Ability source) { public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
// zone contains only one main card // zone contains only one main card

View file

@ -71,9 +71,32 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
@Override @Override
public void setZone(Zone zone, Game game) { public void setZone(Zone zone, Game game) {
// see ModalDoubleFacedCard.checkGoodZones for details
game.setZone(parentCard.getId(), zone); game.setZone(parentCard.getId(), zone);
game.setZone(parentCard.getLeftHalfCard().getId(), zone); game.setZone(this.getId(), zone);
game.setZone(parentCard.getRightHalfCard().getId(), zone);
// find another side to sync
ModalDoubleFacedCardHalf otherSide;
if (!parentCard.getLeftHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getLeftHalfCard();
} else if (!parentCard.getRightHalfCard().getId().equals(this.getId())) {
otherSide = parentCard.getRightHalfCard();
} else {
throw new IllegalStateException("Wrong code usage: MDF halves must use different ids");
}
switch (zone) {
case STACK:
case BATTLEFIELD:
// stack and battlefield must have only one side
game.setZone(otherSide.getId(), Zone.OUTSIDE);
break;
default:
game.setZone(otherSide.getId(), zone);
break;
}
ModalDoubleFacedCard.checkGoodZones(game, parentCard);
} }
@Override @Override

View file

@ -555,7 +555,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
// game cheats (for tests only) // game cheats (for tests only)
void cheat(UUID ownerId, Map<Zone, String> commands); void cheat(UUID ownerId, Map<Zone, String> commands);
void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled); void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled);
// controlling the behaviour of replacement effects while permanents entering the battlefield // controlling the behaviour of replacement effects while permanents entering the battlefield
void setScopeRelevant(boolean scopeRelevant); void setScopeRelevant(boolean scopeRelevant);

View file

@ -288,6 +288,8 @@ public abstract class GameImpl implements Game {
public void loadCards(Set<Card> cards, UUID ownerId) { public void loadCards(Set<Card> cards, UUID ownerId) {
for (Card card : cards) { for (Card card : cards) {
if (card instanceof PermanentCard) { if (card instanceof PermanentCard) {
// TODO: impossible use case, can be deleted?
// trying to put permanent card to battlefield
card = ((PermanentCard) card).getCard(); card = ((PermanentCard) card).getCard();
} }
@ -2019,11 +2021,10 @@ public abstract class GameImpl implements Game {
// save original copy link (handle copy of copies too) // save original copy link (handle copy of copies too)
newBluePrint.setCopy(true, (copyFromPermanent.getCopyFrom() != null ? copyFromPermanent.getCopyFrom() : copyFromPermanent)); newBluePrint.setCopy(true, (copyFromPermanent.getCopyFrom() != null ? copyFromPermanent.getCopyFrom() : copyFromPermanent));
CopyEffect newEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId); CopyEffect newCopyEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId);
newEffect.newId(); newCopyEffect.setApplier(applier);
newEffect.setApplier(applier);
Ability newAbility = source.copy(); Ability newAbility = source.copy();
newEffect.init(newAbility, this); newCopyEffect.init(newAbility, this);
// If there are already copy effects with duration = Custom to the same object, remove the existing effects because they no longer have any effect // If there are already copy effects with duration = Custom to the same object, remove the existing effects because they no longer have any effect
if (duration == Duration.Custom) { if (duration == Duration.Custom) {
@ -2037,7 +2038,7 @@ public abstract class GameImpl implements Game {
} }
} }
} }
state.addEffect(newEffect, newAbility); state.addEffect(newCopyEffect, newAbility);
return newBluePrint; return newBluePrint;
} }
@ -3567,20 +3568,27 @@ public abstract class GameImpl implements Game {
} }
@Override @Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled) { public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled) {
// fake test ability for triggers and events // fake test ability for triggers and events
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
fakeSourceAbilityTemplate.setControllerId(ownerId); fakeSourceAbilityTemplate.setControllerId(ownerId);
Player player = getPlayer(ownerId); Player player = getPlayer(ownerId);
if (player != null) { if (player != null) {
// init cards
loadCards(ownerId, library); loadCards(ownerId, library);
loadCards(ownerId, hand); loadCards(ownerId, hand);
loadCards(ownerId, battlefield); loadCards(ownerId, battlefield
.stream()
.map(PutToBattlefieldInfo::getCard)
.collect(Collectors.toList())
);
loadCards(ownerId, graveyard); loadCards(ownerId, graveyard);
loadCards(ownerId, command); loadCards(ownerId, command);
loadCards(ownerId, exiled); loadCards(ownerId, exiled);
// move cards to zones
for (Card card : library) { for (Card card : library) {
player.getLibrary().putOnTop(card, this); player.getLibrary().putOnTop(card, this);
} }
@ -3610,10 +3618,10 @@ public abstract class GameImpl implements Game {
getExile().add(card); getExile().add(card);
} }
for (PermanentCard permanentCard : battlefield) { for (PutToBattlefieldInfo info : battlefield) {
Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy();
fakeSourceAbility.setSourceId(permanentCard.getId()); fakeSourceAbility.setSourceId(info.getCard().getId());
CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, permanentCard, player); CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, info.getCard(), player, info.isTapped());
} }
applyEffects(); applyEffects();

View file

@ -0,0 +1,27 @@
package mage.game;
import mage.cards.Card;
/**
* For tests only: put to battlefield with additional settings like tapped
*
* @author JayDi85
*/
public class PutToBattlefieldInfo {
private final Card card;
private final boolean tapped;
public PutToBattlefieldInfo(Card card, boolean tapped) {
this.card = card;
this.tapped = tapped;
}
public Card getCard() {
return card;
}
public boolean isTapped() {
return tapped;
}
}

View file

@ -439,6 +439,7 @@ public final class ZonesHandler {
if (Zone.STACK == event.getFromZone()) { if (Zone.STACK == event.getFromZone()) {
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null && !spell.isFaceDown(game)) { if (spell != null && !spell.isFaceDown(game)) {
// TODO: wtf, why only colors!? Must research and remove colors workaround
if (!card.getColor(game).equals(spell.getColor(game))) { if (!card.getColor(game).equals(spell.getColor(game))) {
// the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card // the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card
card.getColor(game).setColor(spell.getColor(game)); card.getColor(game).setColor(spell.getColor(game));

View file

@ -14,6 +14,7 @@ import mage.cards.SplitCard;
import mage.constants.SpellAbilityType; import mage.constants.SpellAbilityType;
import mage.game.Game; import mage.game.Game;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.token.Token;
import javax.annotation.processing.SupportedSourceVersion; import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion; import javax.lang.model.SourceVersion;
@ -25,18 +26,23 @@ import java.util.UUID;
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PermanentCard extends PermanentImpl { public class PermanentCard extends PermanentImpl {
protected int maxLevelCounters; protected int maxLevelCounters;
// A copy of the origin card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype) // A copy of the original card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype)
protected Card card; protected Card card; // TODO: wtf, it modified on getCard and other places, e.g. on bestow -- must be fixed!
// the number this permanent instance had // the number this permanent instance had
protected int zoneChangeCounter; protected int zoneChangeCounter;
public PermanentCard(Card card, UUID controllerId, Game game) { public PermanentCard(Card card, UUID controllerId, Game game) {
super(card.getId(), card.getOwnerId(), controllerId, card.getName()); super(card.getId(), card.getOwnerId(), controllerId, card.getName());
// runtime check: must use real card only inside
if (card instanceof PermanentCard) {
// TODO: allow?
throw new IllegalArgumentException("Wrong code usage: can't use PermanentCard inside another PermanentCard");
}
// usage check: you must put to play only real card's part // usage check: you must put to play only real card's part
// if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side // if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side
// it's a basic check and still allows to create permanent from instant or sorcery // it's a basic check and still allows to create permanent from instant or sorcery
@ -101,6 +107,7 @@ public class PermanentCard extends PermanentImpl {
} }
protected void copyFromCard(final Card card, final Game game) { protected void copyFromCard(final Card card, final Game game) {
// TODO: must research - is it copy all fields or something miss
this.name = card.getName(); this.name = card.getName();
this.abilities.clear(); this.abilities.clear();
if (this.faceDown) { if (this.faceDown) {
@ -224,6 +231,10 @@ public class PermanentCard extends PermanentImpl {
@Override @Override
public String toString() { public String toString() {
return card.toString(); return card.toString()
+ ", " + ((this instanceof Token) ? "T" : "C")
+ (this.isCopy() ? ", copy" : "")
+ ", " + this.getPower() + "/" + this.getToughness()
+ (this.isTapped() ? ", tapped" : "");
} }
} }

View file

@ -143,6 +143,6 @@ public class SpellStack extends ArrayDeque<StackObject> {
@Override @Override
public String toString() { public String toString() {
return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100) + ")"); return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100, "...") + ")");
} }
} }

View file

@ -294,10 +294,8 @@ public interface Player extends MageItem, Copyable<Player> {
void setTopCardRevealed(boolean topCardRevealed); void setTopCardRevealed(boolean topCardRevealed);
/** /**
* Get data from the client Preferences (e.g. avatarId or * User's settings like avatar or skip buttons.
* showAbilityPickerForce) * WARNING, game related code must use controlling player settings only, e.g. getControllingPlayersUserData
*
* @return
*/ */
UserData getUserData(); UserData getUserData();
@ -333,6 +331,9 @@ public interface Player extends MageItem, Copyable<Player> {
List<UUID> getTurnControllers(); List<UUID> getTurnControllers();
/**
* Current turn controller for a player (return own id for own control)
*/
UUID getTurnControlledBy(); UUID getTurnControlledBy();
/** /**
@ -360,6 +361,11 @@ public interface Player extends MageItem, Copyable<Player> {
*/ */
void setGameUnderYourControl(boolean value); void setGameUnderYourControl(boolean value);
/**
* Return player's turn control to prev player
* @param value
* @param fullRestore return turn control to own
*/
void setGameUnderYourControl(boolean value, boolean fullRestore); void setGameUnderYourControl(boolean value, boolean fullRestore);
void setTestMode(boolean value); void setTestMode(boolean value);

View file

@ -159,10 +159,12 @@ public abstract class PlayerImpl implements Player, Serializable {
protected FilterPermanent sacrificeCostFilter; protected FilterPermanent sacrificeCostFilter;
protected final List<AlternativeSourceCosts> alternativeSourceCosts = new ArrayList<>(); protected final List<AlternativeSourceCosts> alternativeSourceCosts = new ArrayList<>();
protected boolean isGameUnderControl = true; // TODO: rework turn controller to use single list (see other todos)
protected UUID turnController; //protected Stack<UUID> allTurnControllers = new Stack<>();
protected List<UUID> turnControllers = new ArrayList<>(); protected boolean isGameUnderControl = true; // TODO: replace with allTurnControllers.isEmpty
protected Set<UUID> playersUnderYourControl = new HashSet<>(); protected UUID turnController; // null on own control TODO: replace with allTurnControllers.last
protected List<UUID> turnControllers = new ArrayList<>(); // TODO: remove
protected Set<UUID> playersUnderYourControl = new HashSet<>(); // TODO: replace with game method and search in allTurnControllers
protected Set<UUID> usersAllowedToSeeHandCards = new HashSet<>(); protected Set<UUID> usersAllowedToSeeHandCards = new HashSet<>();
protected List<UUID> attachments = new ArrayList<>(); protected List<UUID> attachments = new ArrayList<>();
@ -263,14 +265,14 @@ public abstract class PlayerImpl implements Player, Serializable {
this.storedBookmark = player.storedBookmark; this.storedBookmark = player.storedBookmark;
this.topCardRevealed = player.topCardRevealed; this.topCardRevealed = player.topCardRevealed;
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards);
this.isTestMode = player.isTestMode; this.isTestMode = player.isTestMode;
this.isGameUnderControl = player.isGameUnderControl;
this.isGameUnderControl = player.isGameUnderControl;
this.turnController = player.turnController; this.turnController = player.turnController;
this.turnControllers.addAll(player.turnControllers); this.turnControllers.addAll(player.turnControllers);
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
this.passed = player.passed; this.passed = player.passed;
this.passedTurn = player.passedTurn; this.passedTurn = player.passedTurn;
@ -363,13 +365,14 @@ public abstract class PlayerImpl implements Player, Serializable {
this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts());
this.topCardRevealed = player.isTopCardRevealed(); this.topCardRevealed = player.isTopCardRevealed();
this.playersUnderYourControl.clear();
this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl());
this.isGameUnderControl = player.isGameUnderControl();
this.turnController = player.getTurnControlledBy(); this.isGameUnderControl = player.isGameUnderControl();
this.turnController = this.getId().equals(player.getTurnControlledBy()) ? null : player.getTurnControlledBy();
this.turnControllers.clear(); this.turnControllers.clear();
this.turnControllers.addAll(player.getTurnControllers()); this.turnControllers.addAll(player.getTurnControllers());
this.playersUnderYourControl.clear();
this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl());
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
this.clearCastSourceIdManaCosts(); this.clearCastSourceIdManaCosts();
@ -607,6 +610,9 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public void setTurnControlledBy(UUID playerId) { public void setTurnControlledBy(UUID playerId) {
if (playerId == null) {
throw new IllegalArgumentException("Can't add unknown player to turn controllers: " + playerId);
}
this.turnController = playerId; this.turnController = playerId;
this.turnControllers.add(playerId); this.turnControllers.add(playerId);
} }
@ -618,7 +624,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public UUID getTurnControlledBy() { public UUID getTurnControlledBy() {
return this.turnController; return this.turnController == null ? this.getId() : this.turnController;
} }
@Override @Override
@ -647,14 +653,18 @@ public abstract class PlayerImpl implements Player, Serializable {
this.isGameUnderControl = value; this.isGameUnderControl = value;
if (isGameUnderControl) { if (isGameUnderControl) {
if (fullRestore) { if (fullRestore) {
// to own
this.turnControllers.clear(); this.turnControllers.clear();
this.turnController = getId(); this.turnController = null;
this.isGameUnderControl = true;
} else { } else {
// to prev player
if (!turnControllers.isEmpty()) { if (!turnControllers.isEmpty()) {
this.turnControllers.remove(turnControllers.size() - 1); this.turnControllers.remove(turnControllers.size() - 1);
} }
if (turnControllers.isEmpty()) { if (turnControllers.isEmpty()) {
this.turnController = getId(); this.turnController = null;
this.isGameUnderControl = true;
} else { } else {
this.turnController = turnControllers.get(turnControllers.size() - 1); this.turnController = turnControllers.get(turnControllers.size() - 1);
isGameUnderControl = false; isGameUnderControl = false;
@ -2515,7 +2525,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public void concede(Game game) { public void concede(Game game) {
game.setConcedingPlayer(playerId); game.setConcedingPlayer(playerId);
lost(game);
lost(game); // it's ok to be ignored by "can't lose abilities" here (setConcedingPlayer done all work above)
} }
@Override @Override

View file

@ -1082,16 +1082,26 @@ public final class CardUtil {
} }
/** /**
* Put card to battlefield without resolve (for cheats and tests only) * Put card to battlefield without resolve/ETB (for cheats and tests only)
* *
* @param source must be non null (if you need it empty then use fakeSourceAbility) * @param source must be non-null (if you need it empty then use fakeSourceAbility)
* @param game * @param game
* @param newCard * @param newCard
* @param player * @param player
*/ */
public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player) { public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player, boolean tapped) {
// same logic as ZonesHandler->maybeRemoveFromSourceZone // same logic as ZonesHandler->maybeRemoveFromSourceZone
// runtime check: must have source
if (source == null) {
throw new IllegalArgumentException("Wrong code usage: must use source ability or fakeSourceAbility");
}
// runtime check: must use only real cards
if (newCard instanceof PermanentCard) {
throw new IllegalArgumentException("Wrong code usage: must put to battlefield only real cards, not PermanentCard");
}
// workaround to put real permanent from one side (example: you call mdf card by cheats) // workaround to put real permanent from one side (example: you call mdf card by cheats)
Card permCard = getDefaultCardSideForBattlefield(game, newCard); Card permCard = getDefaultCardSideForBattlefield(game, newCard);
@ -1105,15 +1115,16 @@ public final class CardUtil {
permanent = new PermanentCard(permCard, player.getId(), game); permanent = new PermanentCard(permCard, player.getId(), game);
} }
// put onto battlefield with possible counters // put onto battlefield with possible counters without ETB
game.getPermanentsEntering().put(permanent.getId(), permanent); game.getPermanentsEntering().put(permanent.getId(), permanent);
permCard.checkForCountersToAdd(permanent, source, game); permCard.checkForCountersToAdd(permanent, source, game);
permanent.entersBattlefield(source, game, Zone.OUTSIDE, false); permanent.entersBattlefield(source, game, Zone.OUTSIDE, false);
game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber()); game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber());
game.getPermanentsEntering().remove(permanent.getId()); game.getPermanentsEntering().remove(permanent.getId());
// workaround for special tapped status from test framework's command (addCard) // tapped status
if (permCard instanceof PermanentCard && ((PermanentCard) permCard).isTapped()) { // warning, "enters the battlefield tapped" abilities will be executed before, so don't set to false here
if (tapped) {
permanent.setTapped(true); permanent.setTapped(true);
} }