External-master #12

Merged
Failure merged 33 commits from External-master into master 2025-01-10 15:40:15 -08:00
76 changed files with 2850 additions and 674 deletions

Binary file not shown.

View file

@ -26,6 +26,7 @@ import mage.client.plugins.adapters.MageActionCallback;
import mage.client.plugins.impl.Plugins;
import mage.client.preference.MagePreferences;
import mage.client.remote.CallbackClientImpl;
import mage.client.remote.XmageURLConnection;
import mage.client.table.TablesPane;
import mage.client.table.TablesPanel;
import mage.client.tournament.TournamentPane;
@ -54,6 +55,7 @@ import net.java.truevfs.access.TArchiveDetector;
import net.java.truevfs.access.TConfig;
import net.java.truevfs.kernel.spec.FsAccessOption;
import org.apache.log4j.Logger;
import org.junit.Assert;
import org.mage.card.arcane.ManaSymbols;
import org.mage.card.arcane.SvgUtils;
import org.mage.plugins.card.images.DownloadPicturesService;
@ -196,19 +198,6 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
public MageFrame() throws MageException {
File cacertsFile = new File(System.getProperty("user.dir") + "/release/cacerts").getAbsoluteFile();
if (!cacertsFile.exists()) { // When running from the jar file the contents of the /release folder will have been expanded into the home folder as part of packaging
cacertsFile = new File(System.getProperty("user.dir") + "/cacerts").getAbsoluteFile();
}
if (cacertsFile.exists()) {
LOGGER.info("Custom (or bundled) Java certificate file (cacerts) file found");
String cacertsPath = cacertsFile.getPath();
System.setProperty("javax.net.ssl.trustStore", cacertsPath);
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
} else {
LOGGER.info("custom Java certificate file not found at: " + cacertsFile.getAbsolutePath());
}
setWindowTitle();
// mac os only: enable full screen support in java 8 (java 11+ try to use it all the time)
@ -390,6 +379,41 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
});
}
/**
* Init certificates store for https work (if java version is outdated)
* Debug with -Djavax.net.debug=SSL,trustmanager
*/
@Deprecated // TODO: replaced by enableAIAcaIssuers, delete that code after few releases (2025-01-01)
private void initSSLCertificates() {
// from dev build (runtime)
boolean cacertsUsed = false;
File cacertsFile = new File(System.getProperty("user.dir") + "/release/cacerts").getAbsoluteFile();
if (cacertsFile.exists()) {
cacertsUsed = true;
LOGGER.info("SSL certificates: used runtime cacerts bundle");
}
// from release build (jar)
// When running from the jar file the contents of the /release folder will have been expanded into the home folder as part of packaging
if (!cacertsUsed) {
cacertsFile = new File(System.getProperty("user.dir") + "/cacerts").getAbsoluteFile();
if (cacertsFile.exists()) {
cacertsUsed = true;
LOGGER.info("SSL certificates: used release cacerts bundle");
}
}
if (cacertsUsed && cacertsFile.exists()) {
String cacertsPath = cacertsFile.getPath();
System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); // cacerts file format from java 9+ instead "jks" from java 8
System.setProperty("javax.net.ssl.trustStore", cacertsPath);
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
} else {
LOGGER.info("SSL certificates: used default cacerts bundle from " + System.getProperty("java.version"));
}
System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
}
private void bootstrapSetsAndFormats() {
LOGGER.info("Loading sets and formats...");
ConstructedFormats.ensureLists();

View file

@ -30,8 +30,10 @@ import mage.constants.*;
import mage.game.events.PlayerQueryEvent;
import mage.players.PlayableObjectStats;
import mage.players.PlayableObjectsList;
import mage.util.CardUtil;
import mage.util.DebugUtil;
import mage.util.MultiAmountMessage;
import mage.util.StreamUtils;
import mage.view.*;
import org.apache.log4j.Logger;
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
@ -53,6 +55,7 @@ import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static mage.client.dialog.PreferencesDialog.*;
import static mage.constants.PlayerAction.*;
@ -1810,6 +1813,7 @@ public final class GamePanel extends javax.swing.JPanel {
// hand
if (needZone == Zone.HAND || needZone == Zone.ALL) {
// my hand
for (CardView card : lastGameData.game.getMyHand().values()) {
if (needSelectable.contains(card.getId())) {
card.setChoosable(true);
@ -1821,6 +1825,34 @@ public final class GamePanel extends javax.swing.JPanel {
card.setPlayableStats(needPlayable.getStats(card.getId()));
}
}
// opponent hands (switching by GUI's button with my hand)
List<SimpleCardView> list = lastGameData.game.getOpponentHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList());
for (SimpleCardView card : list) {
if (needSelectable.contains(card.getId())) {
card.setChoosable(true);
}
if (needChosen.contains(card.getId())) {
card.setSelected(true);
}
if (needPlayable.containsObject(card.getId())) {
card.setPlayableStats(needPlayable.getStats(card.getId()));
}
}
// watched hands (switching by GUI's button with my hand)
list = lastGameData.game.getWatchedHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList());
for (SimpleCardView card : list) {
if (needSelectable.contains(card.getId())) {
card.setChoosable(true);
}
if (needChosen.contains(card.getId())) {
card.setSelected(true);
}
if (needPlayable.containsObject(card.getId())) {
card.setPlayableStats(needPlayable.getStats(card.getId()));
}
}
}
// stack

View file

@ -98,8 +98,8 @@ public class MageActionCallback implements ActionCallback {
private MageCard prevCardPanel;
private boolean startedDragging;
private boolean isDragging; // TODO: remove drag hand code to the hand panels
private Point initialCardPos;
private Point initialMousePos;
private Point initialCardPos = null;
private Point initialMousePos = null;
private final Set<MageCard> draggingCards = new HashSet<>();
public MageActionCallback() {
@ -351,6 +351,11 @@ public class MageActionCallback implements ActionCallback {
return;
}
if (this.initialMousePos == null || this.initialCardPos == null) {
// only allow really mouse pressed, e.g. ignore draft/game update on active card draging/pressing
return;
}
Point mouse = new Point(e.getX(), e.getY());
SwingUtilities.convertPointToScreen(mouse, data.getComponent());
if (!isDragging

View file

@ -48,6 +48,12 @@ public class XmageURLConnection {
private static final AtomicLong debugLastRequestTimeMs = new AtomicLong(0);
private static final ReentrantLock debugLogsWriterlock = new ReentrantLock();
static {
// add Authority Information Access (AIA) Extension support for certificates from Windows servers like gatherer website
// fix download errors like sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
}
final String url;
Proxy proxy = null;
HttpURLConnection connection = null;

View file

@ -143,7 +143,7 @@ public final class TextboxRuleParser {
index += 5;
++outputIndex;
} else {
LOGGER.error("Bad &...; sequence `" + rule.substring(index + 1, index + 10) + "` in rule.");
LOGGER.error("Bad &...; sequence `" + rule.substring(index, Math.min(rule.length(), index + 10)) + "` in rule.");
build.append('&');
++index;
++outputIndex;

View file

@ -2457,6 +2457,9 @@ public class ScryfallImageSupportTokens {
// BLC
put("BLC/Raccoon", "https://api.scryfall.com/cards/tblc/29/en?format=image");
// DSK
put("DSK/Emblem Kaito", "https://api.scryfall.com/cards/tdsk/17/en?format=image");
// FDN
put("FDN/Beast/1", "https://api.scryfall.com/cards/tfdn/32/en?format=image");
put("FDN/Beast/2", "https://api.scryfall.com/cards/tfdn/33/en?format=image");

View file

@ -725,8 +725,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
try {
TVFS.umount();
} catch (FsSyncException e) {
logger.fatal("Couldn't unmount zip files " + e, e);
MageFrame.getInstance().showErrorDialog("Couldn't unmount zip files " + e, e);
logger.error("Couldn't unmount zip files " + e, e);
// this is not a critical error - just need to run it again - see issue #12833
}
}

View file

@ -185,7 +185,7 @@ public final class CardImageUtils {
try {
TVFS.umount();
} catch (FsSyncException e) {
LOGGER.fatal("Couldn't unmount zip files on searching broken images " + e, e);
LOGGER.error("Couldn't unmount zip files on searching broken images " + e, e);
}
// real images check is slow, so it used on images download only (not here)

View file

@ -60,4 +60,21 @@ public class DownloaderTest {
Assert.assertNotNull(stream);
Assert.assertTrue("must have image data", image.getWidth() > 0);
}
@Test
public void test_DownloadFromWindowsServers() throws IOException {
// symbols download from gatherer website
// error example: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
InputStream stream = XmageURLConnection.downloadBinary("https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=BIG&size=small&rarity=C");
Assert.assertNotNull(stream);
BufferedImage image = null;
try {
image = ImageIO.read(stream);
} catch (IOException e) {
Assert.fail("Can't download image file due error: " + e);
}
Assert.assertNotNull(stream);
Assert.assertNotNull(image);
Assert.assertTrue("must have image data", image.getWidth() > 0);
}
}

View file

@ -24,6 +24,8 @@ import mage.game.command.Plane;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.Token;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPlayer;
import mage.util.CardUtil;
import mage.util.MultiAmountMessage;
import mage.util.RandomUtil;
@ -66,6 +68,8 @@ public final class SystemUtil {
// [@mana add] -> MANA ADD
private static final String COMMAND_CARDS_ADD_TO_HAND = "@card add to hand";
private static final String COMMAND_LANDS_ADD_TO_BATTLEFIELD = "@lands add";
private static final String COMMAND_OPPONENT_UNDER_CONTROL_START = "@opponent under control start";
private static final String COMMAND_OPPONENT_UNDER_CONTROL_END = "@opponent under control end";
private static final String COMMAND_MANA_ADD = "@mana add"; // TODO: not implemented
private static final String COMMAND_RUN_CUSTOM_CODE = "@run custom code"; // TODO: not implemented
private static final String COMMAND_SHOW_OPPONENT_HAND = "@show opponent hand";
@ -80,6 +84,8 @@ public final class SystemUtil {
supportedCommands.put(COMMAND_CARDS_ADD_TO_HAND, "CARDS: ADD TO HAND");
supportedCommands.put(COMMAND_MANA_ADD, "MANA ADD");
supportedCommands.put(COMMAND_LANDS_ADD_TO_BATTLEFIELD, "LANDS: ADD TO BATTLEFIELD");
supportedCommands.put(COMMAND_OPPONENT_UNDER_CONTROL_START, "OPPONENT CONTROL: ENABLE");
supportedCommands.put(COMMAND_OPPONENT_UNDER_CONTROL_END, "OPPONENT CONTROL: DISABLE");
supportedCommands.put(COMMAND_RUN_CUSTOM_CODE, "RUN CUSTOM CODE");
supportedCommands.put(COMMAND_SHOW_OPPONENT_HAND, "SHOW OPPONENT HAND");
supportedCommands.put(COMMAND_SHOW_OPPONENT_LIBRARY, "SHOW OPPONENT LIBRARY");
@ -255,7 +261,7 @@ public final class SystemUtil {
*
* @param game
* @param commandsFilePath file path with commands in init.txt format
* @param feedbackPlayer player to execute that cheats (will see choose dialogs)
* @param feedbackPlayer player to execute that cheats (will see choose dialogs)
*/
public static void executeCheatCommands(Game game, String commandsFilePath, Player feedbackPlayer) {
@ -301,6 +307,8 @@ public final class SystemUtil {
// add default commands
initLines.add(0, String.format("[%s]", COMMAND_LANDS_ADD_TO_BATTLEFIELD));
initLines.add(1, String.format("[%s]", COMMAND_CARDS_ADD_TO_HAND));
initLines.add(2, String.format("[%s]", COMMAND_OPPONENT_UNDER_CONTROL_START));
initLines.add(3, String.format("[%s]", COMMAND_OPPONENT_UNDER_CONTROL_END));
// collect all commands
CommandGroup currentGroup = null;
@ -544,6 +552,36 @@ public final class SystemUtil {
break;
}
case COMMAND_OPPONENT_UNDER_CONTROL_START: {
Target target = new TargetPlayer().withNotTarget(true).withChooseHint("to take under your control");
Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy();
if (feedbackPlayer.chooseTarget(Outcome.GainControl, target, fakeSourceAbility, game)) {
Player targetPlayer = game.getPlayer(target.getFirstTarget());
if (targetPlayer != null && targetPlayer != feedbackPlayer) {
CardUtil.takeControlUnderPlayerStart(game, fakeSourceAbility, feedbackPlayer, targetPlayer, false);
// allow priority play again in same step (for better cheat UX)
targetPlayer.resetPassed();
}
// workaround for refresh priority dialog like avatar click (cheats called from priority in 99%)
game.firePriorityEvent(feedbackPlayer.getId());
}
break;
}
case COMMAND_OPPONENT_UNDER_CONTROL_END: {
Target target = new TargetPlayer().withNotTarget(true).withChooseHint("to free from your control");
Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy();
if (feedbackPlayer.chooseTarget(Outcome.GainControl, target, fakeSourceAbility, game)) {
Player targetPlayer = game.getPlayer(target.getFirstTarget());
if (targetPlayer != null && targetPlayer != feedbackPlayer && !targetPlayer.isGameUnderControl()) {
CardUtil.takeControlUnderPlayerEnd(game, fakeSourceAbility, feedbackPlayer, targetPlayer);
}
// workaround for refresh priority dialog like avatar click (cheats called from priority in 99%)
game.firePriorityEvent(feedbackPlayer.getId());
}
break;
}
default: {
String mes = String.format("Unknown system command: %s", runGroup.name);
errorsList.add(mes);
@ -551,7 +589,6 @@ public final class SystemUtil {
break;
}
}
sendCheatCommandsFeedback(game, feedbackPlayer, errorsList);
return;
}

View file

@ -0,0 +1,401 @@
package mage.player.ai;
import mage.MageObject;
import mage.abilities.*;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.ManaCost;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.decks.Deck;
import mage.choices.Choice;
import mage.constants.ManaType;
import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetAmount;
import mage.target.TargetCard;
import mage.util.MultiAmountMessage;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* AI player that can be taken under control by another player (AI or human).
* <p>
* Under control logic on choose dialog (under human):
* - create fake human player and assign it to real human data transfer object (for income answers);
* - call choose dialog from fake human (e.g. send choose data to real player);
* - game will process all sending and answering logic as "human under human" logic;
* - return choose dialog result without AI code processing;
*
* @author JayDi85
*/
public class ComputerPlayerControllableProxy extends ComputerPlayer7 {
private static final Logger logger = Logger.getLogger(ComputerPlayerControllableProxy.class);
Player lastControllingPlayer = null;
public ComputerPlayerControllableProxy(String name, RangeOfInfluence range, int skill) {
super(name, range, skill);
}
public ComputerPlayerControllableProxy(final ComputerPlayerControllableProxy player) {
super(player);
this.lastControllingPlayer = player;
}
@Override
public ComputerPlayerControllableProxy copy() {
return new ComputerPlayerControllableProxy(this);
}
private boolean isUnderMe(Game game) {
return game.isSimulation() || this.isGameUnderControl();
}
private Player getControllingPlayer(Game game) {
Player player = game.getPlayer(this.getTurnControlledBy());
this.lastControllingPlayer = player.prepareControllableProxy(this);
return this.lastControllingPlayer;
}
@Override
public void setResponseString(String responseString) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseString(responseString);
}
}
@Override
public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseManaType(manaTypePlayerId, responseManaType);
}
}
@Override
public void setResponseUUID(UUID responseUUID) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseUUID(responseUUID);
}
}
@Override
public void setResponseBoolean(Boolean responseBoolean) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseBoolean(responseBoolean);
}
}
@Override
public void setResponseInteger(Integer responseInteger) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseInteger(responseInteger);
}
}
@Override
public void signalPlayerCheat() {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.signalPlayerCheat();
}
}
@Override
public void signalPlayerConcede(boolean stopCurrentChooseDialog) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.signalPlayerConcede(stopCurrentChooseDialog);
}
}
@Override
public boolean priority(Game game) {
if (isUnderMe(game)) {
return super.priority(game);
} else {
Player player = getControllingPlayer(game);
try {
return player.priority(game);
} finally {
this.passed = player.isPassed(); // TODO: wtf, no needs?
}
}
}
@Override
public boolean chooseMulligan(Game game) {
if (isUnderMe(game)) {
return super.chooseMulligan(game);
} else {
return getControllingPlayer(game).chooseMulligan(game);
}
}
@Override
public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseUse(outcome, message, source, game);
} else {
return getControllingPlayer(game).chooseUse(outcome, message, source, game);
}
}
@Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
} else {
return getControllingPlayer(game).chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
}
}
@Override
public int chooseReplacementEffect(Map<String, String> effectsMap, Map<String, MageObject> objectsMap, Game game) {
if (isUnderMe(game)) {
return super.chooseReplacementEffect(effectsMap, objectsMap, game);
} else {
return getControllingPlayer(game).chooseReplacementEffect(effectsMap, objectsMap, game);
}
}
@Override
public boolean choose(Outcome outcome, Choice choice, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, choice, game);
} else {
return getControllingPlayer(game).choose(outcome, choice, game);
}
}
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, target, source, game);
} else {
return getControllingPlayer(game).choose(outcome, target, source, game);
}
}
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) {
if (isUnderMe(game)) {
return super.choose(outcome, target, source, game, options);
} else {
return getControllingPlayer(game).choose(outcome, target, source, game, options);
}
}
@Override
public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTarget(outcome, target, source, game);
} else {
return getControllingPlayer(game).chooseTarget(outcome, target, source, game);
}
}
@Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, cards, target, source, game);
} else {
return getControllingPlayer(game).choose(outcome, cards, target, source, game);
}
}
@Override
public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTarget(outcome, cards, target, source, game);
} else {
return getControllingPlayer(game).chooseTarget(outcome, cards, target, source, game);
}
}
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTargetAmount(outcome, target, source, game);
} else {
return getControllingPlayer(game).chooseTargetAmount(outcome, target, source, game);
}
}
@Override
public TriggeredAbility chooseTriggeredAbility(java.util.List<TriggeredAbility> abilities, Game game) {
if (isUnderMe(game)) {
return super.chooseTriggeredAbility(abilities, game);
} else {
return getControllingPlayer(game).chooseTriggeredAbility(abilities, game);
}
}
@Override
public boolean playMana(Ability abilityToCast, ManaCost unpaid, String promptText, Game game) {
if (isUnderMe(game)) {
return super.playMana(abilityToCast, unpaid, promptText, game);
} else {
return getControllingPlayer(game).playMana(abilityToCast, unpaid, promptText, game);
}
}
@Override
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
if (isUnderMe(game)) {
return super.announceXMana(min, max, message, game, ability);
} else {
return getControllingPlayer(game).announceXMana(min, max, message, game, ability);
}
}
@Override
public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost) {
if (isUnderMe(game)) {
return super.announceXCost(min, max, message, game, ability, variableCost);
} else {
return getControllingPlayer(game).announceXCost(min, max, message, game, ability, variableCost);
}
}
@Override
public void selectAttackers(Game game, UUID attackingPlayerId) {
if (isUnderMe(game)) {
super.selectAttackers(game, attackingPlayerId);
} else {
getControllingPlayer(game).selectAttackers(game, attackingPlayerId);
}
}
@Override
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
if (isUnderMe(game)) {
super.selectBlockers(source, game, defendingPlayerId);
} else {
getControllingPlayer(game).selectBlockers(source, game, defendingPlayerId);
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (isUnderMe(game)) {
return super.chooseAttackerOrder(attackers, game);
} else {
return getControllingPlayer(game).chooseAttackerOrder(attackers, game);
}
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (isUnderMe(game)) {
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
} else {
return getControllingPlayer(game).chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (isUnderMe(game)) {
return super.getAmount(min, max, message, game);
} else {
return getControllingPlayer(game).getAmount(min, max, message, game);
}
}
@Override
public List<Integer> getMultiAmountWithIndividualConstraints(
Outcome outcome,
List<MultiAmountMessage> messages,
int totalMin,
int totalMax,
MultiAmountType type,
Game game
) {
if (isUnderMe(game)) {
return super.getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
} else {
return getControllingPlayer(game).getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
}
}
@Override
public void sideboard(Match match, Deck deck) {
super.sideboard(match, deck);
}
@Override
public void construct(Tournament tournament, Deck deck) {
super.construct(tournament, deck);
}
@Override
public void pickCard(java.util.List<Card> cards, Deck deck, Draft draft) {
super.pickCard(cards, deck, draft);
}
@Override
public boolean activateAbility(ActivatedAbility ability, Game game) {
// TODO: need research, see HumanPlayer's code
return super.activateAbility(ability, game);
}
@Override
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
if (isUnderMe(game)) {
return super.chooseAbilityForCast(card, game, noMana);
} else {
return getControllingPlayer(game).chooseAbilityForCast(card, game, noMana);
}
}
@Override
public ActivatedAbility chooseLandOrSpellAbility(Card card, Game game, boolean noMana) {
if (isUnderMe(game)) {
return super.chooseLandOrSpellAbility(card, game, noMana);
} else {
return getControllingPlayer(game).chooseLandOrSpellAbility(card, game, noMana);
}
}
@Override
public Mode chooseMode(Modes modes, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseMode(modes, source, game);
} else {
return getControllingPlayer(game).chooseMode(modes, source, game);
}
}
@Override
public boolean choosePile(Outcome outcome, String message, java.util.List<? extends Card> pile1, java.util.List<? extends Card> pile2, Game game) {
if (isUnderMe(game)) {
return super.choosePile(outcome, message, pile1, pile2, game);
} else {
return getControllingPlayer(game).choosePile(outcome, message, pile1, pile2, game);
}
}
@Override
public void abort() {
// TODO: need research, is it require real player call? Concede/leave/timeout works by default
super.abort();
}
@Override
public void skip() {
// TODO: see abort comments above
super.skip();
}
}

View file

@ -311,37 +311,6 @@ public class ComputerPlayer extends PlayerImpl {
}
}
if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
List<Permanent> targets;
TargetCreatureOrPlayer origTarget = (TargetCreatureOrPlayer) target.getOriginalTarget();
if (outcome.isGood()) {
targets = threats(abilityControllerId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
} else {
targets = threats(randomOpponentId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
}
for (Permanent permanent : targets) {
List<UUID> alreadyTargeted = target.getTargets();
if (target.canTarget(abilityControllerId, permanent.getId(), null, game)) {
if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
target.add(permanent.getId(), game);
return true;
}
}
}
if (outcome.isGood()) {
if (target.canTarget(abilityControllerId, abilityControllerId, null, game)) {
target.add(abilityControllerId, game);
return true;
}
} else if (target.canTarget(abilityControllerId, randomOpponentId, null, game)) {
target.add(randomOpponentId, game);
return true;
}
if (!required) {
return false;
}
}
if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
List<Permanent> targets;
TargetPermanentOrPlayer origTarget = (TargetPermanentOrPlayer) target.getOriginalTarget();
@ -752,48 +721,6 @@ public class ComputerPlayer extends PlayerImpl {
return target.isChosen(game);
}
if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
List<Permanent> targets;
TargetCreatureOrPlayer origTarget = ((TargetCreatureOrPlayer) target.getOriginalTarget());
if (outcome.isGood()) {
targets = threats(abilityControllerId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
} else {
targets = threats(randomOpponentId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
}
if (targets.isEmpty()) {
if (outcome.isGood()) {
if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) {
return tryAddTarget(target, abilityControllerId, source, game);
}
} else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) {
return tryAddTarget(target, randomOpponentId, source, game);
}
}
if (targets.isEmpty() && target.isRequired(source)) {
targets = game.getBattlefield().getActivePermanents(((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), playerId, game);
}
for (Permanent permanent : targets) {
List<UUID> alreadyTargeted = target.getTargets();
if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
return tryAddTarget(target, permanent.getId(), source, game);
}
}
}
if (outcome.isGood()) {
if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) {
return tryAddTarget(target, abilityControllerId, source, game);
}
} else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) {
return tryAddTarget(target, randomOpponentId, source, game);
}
return false;
}
if (target.getOriginalTarget() instanceof TargetAnyTarget) {
List<Permanent> targets;
TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget());

View file

@ -37,6 +37,7 @@ import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.players.PlayerImpl;
import mage.players.PlayerList;
import mage.players.net.UserData;
import mage.target.Target;
import mage.target.TargetAmount;
import mage.target.TargetCard;
@ -93,10 +94,10 @@ public class HumanPlayer extends PlayerImpl {
// * - GAME thread: open response for income command and wait (go to sleep by response.wait)
// * - CALL thread: on closed response - waiting open status of player's response object (if it's too long then cancel the answer)
// * - CALL thread: on opened response - save answer to player's response object and notify GAME thread about it by response.notifyAll
// * - GAME thread: on nofify from response - check new answer value and process it (if it bad then repeat and wait the next one);
// * - GAME thread: on notify from response - check new answer value and process it (if it bad then repeat and wait the next one);
private transient Boolean responseOpenedForAnswer = false; // GAME thread waiting new answer
private transient long responseLastWaitingThreadId = 0;
private final transient PlayerResponse response = new PlayerResponse();
private final transient PlayerResponse response; // data receiver from a client side (must be shared for one player between multiple clients)
private final int RESPONSE_WAITING_TIME_SECS = 30; // waiting time before cancel current response
private final int RESPONSE_WAITING_CHECK_MS = 100; // timeout for open status check
@ -104,7 +105,7 @@ public class HumanPlayer extends PlayerImpl {
protected static FilterCreatureForCombat filterCreatureForCombat = new FilterCreatureForCombat();
protected static FilterAttackingCreature filterAttack = new FilterAttackingCreature();
protected static FilterBlockingCreature filterBlock = new FilterBlockingCreature();
protected final Choice replacementEffectChoice;
protected Choice replacementEffectChoice = null;
private static final Logger logger = Logger.getLogger(HumanPlayer.class);
protected HashSet<String> autoSelectReplacementEffects = new LinkedHashSet<>(); // must be sorted
@ -130,8 +131,12 @@ public class HumanPlayer extends PlayerImpl {
public HumanPlayer(String name, RangeOfInfluence range, int skill) {
super(name, range);
human = true;
this.human = true;
this.response = new PlayerResponse();
initReplacementDialog();
}
private void initReplacementDialog() {
replacementEffectChoice = new ChoiceImpl(true);
replacementEffectChoice.setMessage("Choose replacement effect to resolve first");
replacementEffectChoice.setSpecial(
@ -142,8 +147,20 @@ public class HumanPlayer extends PlayerImpl {
);
}
/**
* Make fake player from any other
*/
public HumanPlayer(final PlayerImpl sourcePlayer, final PlayerResponse sourceResponse) {
super(sourcePlayer);
this.human = true;
this.response = sourceResponse; // need for sync and wait user's response from a network
initReplacementDialog();
}
public HumanPlayer(final HumanPlayer player) {
super(player);
this.response = player.response;
this.replacementEffectChoice = player.replacementEffectChoice;
this.autoSelectReplacementEffects.addAll(player.autoSelectReplacementEffects);
this.currentlyUnpaidMana = player.currentlyUnpaidMana;
@ -1159,25 +1176,27 @@ public class HumanPlayer extends PlayerImpl {
// TODO: change pass and other states like passedUntilStackResolved for controlling player, not for "this"
// TODO: check and change all "this" to controling player calls, many bugs with hand, mana, skips - https://github.com/magefree/mage/issues/2088
// TODO: use controlling player in all choose dialogs (and canRespond too, what's with take control of player AI?!)
UserData controllingUserData = this.userData;
if (canRespond()) {
HumanPlayer controllingPlayer = this;
if (isGameUnderControl()) { // TODO: must be ! to get real controlling player
if (!isGameUnderControl()) {
Player player = game.getPlayer(getTurnControlledBy());
if (player instanceof HumanPlayer) {
controllingPlayer = (HumanPlayer) player;
controllingUserData = player.getUserData();
} else {
// TODO: add computer opponent here?!
}
}
// TODO: check that all skips and stops used from real controlling player
// like holdingPriority (is it a bug here?)
if (getJustActivatedType() != null && !holdingPriority) {
if (controllingPlayer.getUserData().isPassPriorityCast()
if (controllingUserData.isPassPriorityCast()
&& getJustActivatedType() == AbilityType.SPELL) {
setJustActivatedType(null);
pass(game);
return false;
}
if (controllingPlayer.getUserData().isPassPriorityActivation()
if (controllingUserData.isPassPriorityActivation()
&& getJustActivatedType().isNonManaActivatedAbility()) {
setJustActivatedType(null);
pass(game);
@ -1252,7 +1271,7 @@ public class HumanPlayer extends PlayerImpl {
// it's main step
if (!skippedAtLeastOnce
|| (!playerId.equals(game.getActivePlayerId())
&& !controllingPlayer.getUserData().getUserSkipPrioritySteps().isStopOnAllMainPhases())) {
&& !controllingUserData.getUserSkipPrioritySteps().isStopOnAllMainPhases())) {
skippedAtLeastOnce = true;
if (passWithManaPoolCheck(game)) {
return false;
@ -1274,8 +1293,7 @@ public class HumanPlayer extends PlayerImpl {
// it's end of turn step
if (!skippedAtLeastOnce
|| (playerId.equals(game.getActivePlayerId())
&& !controllingPlayer
.getUserData()
&& !controllingUserData
.getUserSkipPrioritySteps()
.isStopOnAllEndPhases())) {
skippedAtLeastOnce = true;
@ -1295,7 +1313,7 @@ public class HumanPlayer extends PlayerImpl {
}
if (!dontCheckPassStep
&& checkPassStep(game, controllingPlayer)) {
&& checkPassStep(game, controllingUserData)) {
if (passWithManaPoolCheck(game)) {
return false;
}
@ -1308,8 +1326,7 @@ public class HumanPlayer extends PlayerImpl {
if (passedUntilStackResolved) {
if (haveNewObjectsOnStack
&& (playerId.equals(game.getActivePlayerId())
&& controllingPlayer
.getUserData()
&& controllingUserData
.getUserSkipPrioritySteps()
.isStopOnStackNewObjects())) {
// new objects on stack -- disable "pass until stack resolved"
@ -1433,17 +1450,17 @@ public class HumanPlayer extends PlayerImpl {
return response.getUUID();
}
private boolean checkPassStep(Game game, HumanPlayer controllingPlayer) {
private boolean checkPassStep(Game game, UserData controllingUserData) {
try {
if (playerId.equals(game.getActivePlayerId())) {
return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType());
return !controllingUserData.getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType());
} else {
return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType());
return !controllingUserData.getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType());
}
} catch (NullPointerException ex) {
if (controllingPlayer.getUserData() != null) {
if (controllingPlayer.getUserData().getUserSkipPrioritySteps() != null) {
if (controllingUserData != null) {
if (controllingUserData.getUserSkipPrioritySteps() != null) {
if (game.getStep() != null) {
if (game.getTurnStepType() == null) {
logger.error("game.getTurnStepType() == null");
@ -2928,20 +2945,9 @@ public class HumanPlayer extends PlayerImpl {
protected boolean passWithManaPoolCheck(Game game) {
if (userData.confirmEmptyManaPool()
&& game.getStack().isEmpty() && getManaPool().count() > 0) {
String activePlayerText;
if (game.isActivePlayer(playerId)) {
activePlayerText = "Your turn";
} else {
activePlayerText = game.getPlayer(game.getActivePlayerId()).getName() + "'s turn";
}
String priorityPlayerText = "";
if (!isGameUnderControl()) {
priorityPlayerText = " / priority " + game.getPlayer(game.getPriorityPlayerId()).getName();
}
// TODO: chooseUse and other dialogs must be under controlling player
if (!chooseUse(Outcome.Detriment, GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool. Pass regardless?")
+ GameLog.getSmallSecondLineText(activePlayerText + " / " + game.getTurnStepType().toString() + priorityPlayerText), null, game)) {
&& game.getStack().isEmpty() && getManaPool().count() > 0 && getManaPool().canLostManaOnEmpty()) {
String message = GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lose. Pass anyway?");
if (!chooseUse(Outcome.Detriment, message, null, game)) {
sendPlayerAction(PlayerAction.PASS_PRIORITY_CANCEL_ALL_ACTIONS, game, null);
return false;
}
@ -2950,11 +2956,6 @@ public class HumanPlayer extends PlayerImpl {
return true;
}
@Override
public String getHistory() {
return "no available";
}
private boolean gameInCheckPlayableState(Game game) {
return gameInCheckPlayableState(game, false);
}
@ -2972,4 +2973,14 @@ public class HumanPlayer extends PlayerImpl {
}
return false;
}
@Override
public Player prepareControllableProxy(Player playerUnderControl) {
// make fake player, e.g. transform computer player to human player for choose dialogs under control
HumanPlayer fakePlayer = new HumanPlayer((PlayerImpl) playerUnderControl, this.response);
if (!fakePlayer.getTurnControlledBy().equals(this.getId())) {
throw new IllegalArgumentException("Wrong code usage: controllable proxy must be controlled by " + this.getName());
}
return fakePlayer;
}
}

View file

@ -68,7 +68,7 @@
/>
<playerTypes>
<playerType name="Human" jar="mage-player-human.jar" className="mage.player.human.HumanPlayer"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
<playerType name="Computer - draftbot" jar="mage-player-ai-draft-bot.jar" className="mage.player.ai.ComputerDraftPlayer"/>
</playerTypes>

View file

@ -64,7 +64,7 @@
/>
<playerTypes>
<playerType name="Human" jar="mage-player-human-${project.version}.jar" className="mage.player.human.HumanPlayer"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma-${project.version}.jar" className="mage.player.ai.ComputerPlayer7"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma-${project.version}.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
<playerType name="Computer - draftbot" jar="mage-player-ai-draftbot-${project.version}.jar" className="mage.player.ai.ComputerDraftPlayer"/>
</playerTypes>
<gameTypes>

View file

@ -786,23 +786,23 @@ public class GameController implements GameCallback {
}
public void sendPlayerUUID(UUID userId, final UUID data) {
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerUUID(data));
sendMessage(userId, playerId -> sendDirectPlayerUUID(playerId, data));
}
public void sendPlayerString(UUID userId, final String data) {
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerString(data));
sendMessage(userId, playerId -> sendDirectPlayerString(playerId, data));
}
public void sendPlayerManaType(UUID userId, final UUID manaTypePlayerId, final ManaType data) {
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerManaType(data, manaTypePlayerId));
sendMessage(userId, playerId -> sendDirectPlayerManaType(playerId, manaTypePlayerId, data));
}
public void sendPlayerBoolean(UUID userId, final Boolean data) {
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerBoolean(data));
sendMessage(userId, playerId -> sendDirectPlayerBoolean(playerId, data));
}
public void sendPlayerInteger(UUID userId, final Integer data) {
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerInteger(data));
sendMessage(userId, playerId -> sendDirectPlayerInteger(playerId, data));
}
private void updatePriorityTimers() {
@ -906,14 +906,14 @@ public class GameController implements GameCallback {
perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options));
}
private void informOthers(UUID playerId) {
private void informOthers(UUID waitingPlayerId) {
StringBuilder message = new StringBuilder();
if (game.getStep() != null) {
message.append(game.getTurnStepType().toString()).append(" - ");
}
message.append("Waiting for ").append(game.getPlayer(playerId).getLogName());
message.append("Waiting for ").append(game.getPlayer(waitingPlayerId).getLogName());
for (final Entry<UUID, GameSessionPlayer> entry : getGameSessionsMap().entrySet()) {
if (!entry.getKey().equals(playerId)) {
if (!entry.getKey().equals(waitingPlayerId)) {
entry.getValue().inform(message.toString());
}
}
@ -1030,7 +1030,7 @@ public class GameController implements GameCallback {
// TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case
// same for another player (can be fixed by super-duper connection)
if (informOthers) {
informOthers(playerId);
informOthers(realPlayerController.getId());
}
}
@ -1055,7 +1055,8 @@ public class GameController implements GameCallback {
} else {
// otherwise execute the action under other player's control
for (UUID controlled : player.getPlayersUnderYourControl()) {
if (gameSessions.containsKey(controlled) && game.getPriorityPlayerId().equals(controlled)) {
Player controlledPlayer = game.getPlayer(controlled);
if ((gameSessions.containsKey(controlled) || controlledPlayer.isComputer()) && game.getPriorityPlayerId().equals(controlled)) {
stopResponseIdleTimeout();
command.execute(controlled);
}
@ -1098,7 +1099,6 @@ public class GameController implements GameCallback {
@FunctionalInterface
interface Command {
void execute(UUID player);
}
@ -1138,6 +1138,81 @@ public class GameController implements GameCallback {
return newGameSessionWatchers;
}
private void sendDirectPlayerUUID(UUID playerId, UUID data) {
// real player
GameSessionPlayer session = getGameSession(playerId);
if (session != null) {
session.sendPlayerUUID(data);
return;
}
// computer under control
Player player = game.getPlayer(playerId);
if (player != null && player.isComputer()) {
player.setResponseUUID(data);
}
}
private void sendDirectPlayerString(UUID playerId, String data) {
// real player
GameSessionPlayer session = getGameSession(playerId);
if (session != null) {
session.sendPlayerString(data);
return;
}
// computer under control
Player player = game.getPlayer(playerId);
if (player != null && player.isComputer()) {
player.setResponseString(data);
}
}
private void sendDirectPlayerManaType(UUID playerId, UUID manaTypePlayerId, ManaType manaType) {
// real player
GameSessionPlayer session = getGameSession(playerId);
if (session != null) {
session.sendPlayerManaType(manaTypePlayerId, manaType);
return;
}
// computer under control
Player player = game.getPlayer(playerId);
if (player != null && player.isComputer()) {
player.setResponseManaType(manaTypePlayerId, manaType);
}
}
private void sendDirectPlayerBoolean(UUID playerId, Boolean data) {
// real player
GameSessionPlayer session = getGameSession(playerId);
if (session != null) {
session.sendPlayerBoolean(data);
return;
}
// computer under control
Player player = game.getPlayer(playerId);
if (player != null && player.isComputer()) {
player.setResponseBoolean(data);
}
}
private void sendDirectPlayerInteger(UUID playerId, Integer data) {
// real player
GameSessionPlayer session = getGameSession(playerId);
if (session != null) {
session.sendPlayerInteger(data);
return;
}
// computer under control
Player player = game.getPlayer(playerId);
if (player != null && player.isComputer()) {
player.setResponseInteger(data);
}
}
private GameSessionPlayer getGameSession(UUID playerId) {
// TODO: check parent callers - there are possible problems with sync, can be related to broken "fix" logs too
// It modify players data, but:

View file

@ -183,7 +183,7 @@ public class GameSessionPlayer extends GameSessionWatcher {
game.getPlayer(playerId).setResponseString(data);
}
public void sendPlayerManaType(ManaType manaType, UUID manaTypePlayerId) {
public void sendPlayerManaType(UUID manaTypePlayerId, ManaType manaType) {
game.getPlayer(playerId).setResponseManaType(manaTypePlayerId, manaType);
}
@ -212,13 +212,14 @@ public class GameSessionPlayer extends GameSessionWatcher {
// game view calculation can take some time and can be called from non-game thread,
// so use copy for thread save (protection from ConcurrentModificationException)
Game sourceGame = game.copy();
Player player = sourceGame.getPlayer(playerId); // null for watcher
GameView gameView = new GameView(sourceGame.getState(), sourceGame, playerId, null);
if (player != null) {
if (gameView.getPriorityPlayerName().equals(player.getName())) {
gameView.setCanPlayObjects(player.getPlayableObjects(sourceGame, Zone.ALL));
}
// playable info (if opponent under control then show opponent's playable)
Player player = sourceGame.getPlayer(playerId); // null for watcher
Player priorityPlayer = sourceGame.getPlayer(sourceGame.getPriorityPlayerId());
Player controllingPlayer = priorityPlayer == null ? null : sourceGame.getPlayer(priorityPlayer.getTurnControlledBy());
if (controllingPlayer != null && player == controllingPlayer) {
gameView.setCanPlayObjects(priorityPlayer.getPlayableObjects(sourceGame, Zone.ALL));
}
processControlledPlayers(sourceGame, player, gameView);

View file

@ -32,7 +32,7 @@
/>
<playerTypes>
<playerType name="Human" jar="mage-player-human.jar" className="mage.player.human.HumanPlayer"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
<playerType name="Computer - draftbot" jar="mage-player-ai-draft-bot.jar" className="mage.player.ai.ComputerDraftPlayer"/>
</playerTypes>

View file

@ -78,12 +78,7 @@ class BelloBardOfTheBramblesEffect extends ContinuousEffectImpl {
"\"Whenever this creature deals combat damage to a player, draw a card.\"";
this.dependendToTypes.add(DependencyType.EnchantmentAddingRemoving); // Enchanted Evening
this.dependendToTypes.add(DependencyType.AuraAddingRemoving); // Cloudform
this.dependendToTypes.add(DependencyType.BecomeForest); // Song of the Dryads
this.dependendToTypes.add(DependencyType.BecomeMountain);
this.dependendToTypes.add(DependencyType.BecomePlains);
this.dependendToTypes.add(DependencyType.BecomeSwamp);
this.dependendToTypes.add(DependencyType.BecomeIsland);
this.dependendToTypes.add(DependencyType.ArtifactAddingRemoving); // March of the Machines
this.dependencyTypes.add(DependencyType.BecomeCreature); // Conspiracy
}

View file

@ -0,0 +1,123 @@
package mage.cards.c;
import java.util.UUID;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.MyTurnCondition;
import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.IndestructibleAbility;
import mage.cards.*;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
/**
*
* @author Grath
*/
public final class CaitCageBrawler extends CardImpl {
public CaitCageBrawler(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(1);
this.toughness = new MageInt(1);
// During your turn, Cait, Cage Brawler has indestructible.
this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(
new GainAbilitySourceEffect(IndestructibleAbility.getInstance()),
MyTurnCondition.instance, "during your turn, {this} has indestructible"
)));
// Whenever Cait attacks, you and defending player each draw a card, then discard a card. Put two +1/+1 counters on Cait if you discarded the card with the highest mana value among those cards or tied for highest.
this.addAbility(new AttacksTriggeredAbility(new CaitCageBrawlerEffect(), false, null, SetTargetPointer.PLAYER));
}
private CaitCageBrawler(final CaitCageBrawler card) {
super(card);
}
@Override
public CaitCageBrawler copy() {
return new CaitCageBrawler(this);
}
}
class CaitCageBrawlerEffect extends OneShotEffect {
public CaitCageBrawlerEffect() {
super(Outcome.Benefit);
this.staticText = "you and defending player each draw a card, then discard a card. Put two +1/+1 counters on " +
"{this} if you discarded the card with the highest mana value among those cards or tied for highest.";
}
protected CaitCageBrawlerEffect(final CaitCageBrawlerEffect effect) {
super(effect);
}
@Override
public CaitCageBrawlerEffect copy() {
return new CaitCageBrawlerEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (controller == null
|| sourceObject == null) {
return false;
}
controller.drawCards(1, source, game);
Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source));
if (opponent != null) {
opponent.drawCards(1, source, game);
}
int mvController = Integer.MIN_VALUE;
Card cardController = null;
int mvOpponent = Integer.MIN_VALUE;
Card cardOpponent = null;
TargetCard controllerTarget = new TargetCard(Zone.HAND, new FilterCard());
if (controller.choose(Outcome.Discard, controller.getHand(), controllerTarget, source, game)) {
Card card = controller.getHand().get(controllerTarget.getFirstTarget(), game);
if (card != null) {
cardController = card;
mvController = card.getManaValue();
}
}
TargetCard opponentTarget = new TargetCard(Zone.HAND, new FilterCard());
if (opponent != null && opponent.choose(Outcome.Discard, opponent.getHand(), opponentTarget, source, game)) {
Card card = opponent.getHand().get(opponentTarget.getFirstTarget(), game);
if (card != null) {
cardOpponent = card;
mvOpponent = card.getManaValue();
}
}
if (cardOpponent != null) {
opponent.discard(cardOpponent, false, source, game);
}
if (cardController != null) {
controller.discard(cardController, false, source, game);
if (mvController > mvOpponent) {
new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)).apply(game, source);
}
}
return true;
}
}

View file

@ -0,0 +1,61 @@
package mage.cards.c;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility;
import mage.abilities.common.TurnedFaceUpAllTriggeredAbility;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.VigilanceAbility;
import mage.abilities.meta.OrTriggeredAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.card.FaceDownPredicate;
/**
* @author jackd149
*/
public final class CryptidInspector extends CardImpl {
private static final FilterPermanent filter1 = new FilterPermanent("a face-down permanent");
private static final FilterPermanent filter2 = new FilterControlledPermanent("Cryptid Inspector or another permanent you control");
static {
filter1.add(FaceDownPredicate.instance);
}
public CryptidInspector(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}");
this.power = new MageInt(2);
this.toughness = new MageInt(3);
this.subtype.add(SubType.ELF);
this.subtype.add(SubType.WARRIOR);
this.addAbility(VigilanceAbility.getInstance());
// Whenever a face-down permanent you control enters and whenever Cryptid Inspector or another permanent you control is turned face up,
// put a +1/+1 counter on Cryptid Inspector.
this.addAbility(new OrTriggeredAbility(
Zone.BATTLEFIELD,
new AddCountersSourceEffect(CounterType.P1P1.createInstance()),
false,
"Whenever a face-down permanent you control enters and "
+ "whenever Cryptid Inspector or another permanent you control is turned face up, ",
new EntersBattlefieldControlledTriggeredAbility(null, filter1),
new TurnedFaceUpAllTriggeredAbility(null, filter2)
));
}
private CryptidInspector(final CryptidInspector card){
super(card);
}
@Override
public CryptidInspector copy() {
return new CryptidInspector(this);
}
}

View file

@ -0,0 +1,161 @@
package mage.cards.c;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.common.ExileTargetCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.Outcome;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.filter.predicate.permanent.TokenPredicate;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.MageInt;
import mage.MageObject;
import mage.target.common.TargetControlledPermanent;
import mage.util.functions.CopyApplier;
/**
*
* @author jam1garner
*/
public final class CurieEmergentIntelligence extends CardImpl {
private static final FilterControlledPermanent filter =
new FilterControlledPermanent("another nontoken artifact creature you control");
static {
filter.add(AnotherPredicate.instance);
filter.add(Predicates.and(
CardType.ARTIFACT.getPredicate(),
CardType.CREATURE.getPredicate()
));
filter.add(TokenPredicate.FALSE);
}
public CurieEmergentIntelligence(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ROBOT);
this.power = new MageInt(1);
this.toughness = new MageInt(3);
// Whenever Curie, Emergent Intelligence deals combat damage to a player, draw cards equal to its base power.
this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(
new DrawCardSourceControllerEffect(CurieEmergentIntelligenceValue.NON_NEGATIVE).setText("draw cards equal to its base power"), false
));
// {1}{U}, Exile another nontoken artifact creature you control: Curie becomes a copy of the exiled creature, except it has
// "Whenever this creature deals combat damage to a player, draw cards equal to its base power."
Ability ability = new SimpleActivatedAbility(new CurieEmergentIntelligenceCopyEffect(), new ManaCostsImpl<>("{1}{U}"));
ability.addCost(new ExileTargetCost(new TargetControlledPermanent(filter)));
this.addAbility(ability);
}
private CurieEmergentIntelligence(final CurieEmergentIntelligence card) {
super(card);
}
@Override
public CurieEmergentIntelligence copy() {
return new CurieEmergentIntelligence(this);
}
}
enum CurieEmergentIntelligenceValue implements DynamicValue {
NON_NEGATIVE;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
Permanent sourcePermanent = sourceAbility.getSourcePermanentOrLKI(game);
if (sourcePermanent == null) {
return 0;
}
// Minimum of 0 needed to account for Spinal Parasite
return Math.max(0, sourcePermanent.getPower().getModifiedBaseValue());
}
@Override
public CurieEmergentIntelligenceValue copy() {
return this;
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return "{this}'s power";
}
}
class CurieEmergentIntelligenceCopyEffect extends OneShotEffect {
private static final CopyApplier applier = new CopyApplier() {
@Override
public boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId) {
blueprint.getAbilities().add(new DealsCombatDamageToAPlayerTriggeredAbility(
new DrawCardSourceControllerEffect(CurieEmergentIntelligenceValue.NON_NEGATIVE).setText("draw cards equal to its base power"), false
));
return true;
}
};
CurieEmergentIntelligenceCopyEffect() {
super(Outcome.Benefit);
this.setText("{this} becomes a copy of the exiled creature, except it has \"Whenever this creature deals combat damage to a player, draw cards equal to its base power.\"");
}
private CurieEmergentIntelligenceCopyEffect(final CurieEmergentIntelligenceCopyEffect effect) {
super(effect);
}
@Override
public CurieEmergentIntelligenceCopyEffect copy() {
return new CurieEmergentIntelligenceCopyEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (Cost c : source.getCosts()) {
if (c.isPaid() && c instanceof ExileTargetCost) {
for (Permanent exiled : ((ExileTargetCost) c).getPermanents()) {
if (exiled != null) {
game.copyPermanent(
Duration.WhileOnBattlefield,
exiled,
source.getSourceId(), source, applier
);
return true;
} else {
return false;
}
}
}
}
return false;
}
}

View file

@ -0,0 +1,132 @@
package mage.cards.k;
import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.MyTurnCondition;
import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.GetEmblemEffect;
import mage.abilities.effects.common.TapTargetEffect;
import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.effects.keyword.SurveilEffect;
import mage.abilities.hint.common.MyTurnHint;
import mage.abilities.keyword.HexproofAbility;
import mage.abilities.keyword.NinjutsuAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.command.emblems.KaitoBaneOfNightmaresEmblem;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.custom.CreatureToken;
import mage.target.common.TargetCreaturePermanent;
import mage.watchers.common.PlayerLostLifeWatcher;
import java.util.UUID;
/**
*
* @author jackd149
*/
public final class KaitoBaneOfNightmares extends CardImpl {
public KaitoBaneOfNightmares(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{U}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.KAITO);
this.setStartingLoyalty(4);
// Ninjutsu {1}{U}{B}
this.addAbility(new NinjutsuAbility("{1}{U}{B}"));
// During your turn, as long as Kaito has one or more loyalty counters on him, he's a 3/4 Ninja creature and has hexproof.
this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(
new BecomesCreatureSourceEffect(
new CreatureToken(3, 4, "3/4 Ninja creature")
.withSubType(SubType.NINJA)
.withAbility(HexproofAbility.getInstance()), null, Duration.WhileOnBattlefield
), KaitoBaneOfNightmaresCondition.instance, "During your turn, as long as {this} has one or more loyalty counters on him, " +
"he's a 3/4 Ninja creature and has hexproof."
)).addHint(MyTurnHint.instance));
// +1: You get an emblem with "Ninjas you control get +1/+1."
this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new KaitoBaneOfNightmaresEmblem()), 1));
// 0: Surveil 2. Then draw a card for each opponent who lost life this turn.
Ability ability = new LoyaltyAbility(new SurveilEffect(2), 0);
ability.addEffect(new DrawCardSourceControllerEffect(KaitoBaneOfNightmaresCount.instance));
this.addAbility(ability, new PlayerLostLifeWatcher());
// -2: Tap target creature. Put two stun counters on it.
Ability minusTwoAbility = new LoyaltyAbility(new TapTargetEffect(), -2);
minusTwoAbility.addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance(2))
.setText("Put two stun counters on it"));
minusTwoAbility.addTarget(new TargetCreaturePermanent());
this.addAbility(minusTwoAbility);
}
private KaitoBaneOfNightmares(final KaitoBaneOfNightmares card) {
super(card);
}
@Override
public KaitoBaneOfNightmares copy() {
return new KaitoBaneOfNightmares(this);
}
}
enum KaitoBaneOfNightmaresCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
if (!MyTurnCondition.instance.apply(game, source)){
return false;
}
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent == null) {
return false;
}
int loyaltyCount = permanent.getCounters(game).getCount(CounterType.LOYALTY);
return loyaltyCount > 0;
}
}
enum KaitoBaneOfNightmaresCount implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
PlayerLostLifeWatcher watcher = game.getState().getWatcher(PlayerLostLifeWatcher.class);
if (watcher != null) {
return watcher.getNumberOfOpponentsWhoLostLife(sourceAbility.getControllerId(), game);
}
return 0;
}
@Override
public KaitoBaneOfNightmaresCount copy() {
return instance;
}
@Override
public String toString() {
return "1";
}
@Override
public String getMessage() {
return "opponent who lost life this turn.";
}
}

View file

@ -16,7 +16,6 @@ import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCardInLibrary;
@ -112,6 +111,7 @@ class MangarasTomeReplacementEffect extends ReplacementEffectImpl {
controller.moveCards(card, Zone.HAND, source, game);
}
}
used = true; // one time use
return true;
}
@ -122,6 +122,6 @@ class MangarasTomeReplacementEffect extends ReplacementEffectImpl {
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return source.isControlledBy(event.getPlayerId());
return !used && source.isControlledBy(event.getPlayerId());
}
}

View file

@ -0,0 +1,128 @@
package mage.cards.n;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.AsThoughEffect;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.keyword.FlashAbility;
import mage.abilities.keyword.WardAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AsThoughEffectType;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.common.TargetOpponentsCreaturePermanent;
import java.util.UUID;
/**
* @author markort147
*/
public final class NowhereToRun extends CardImpl {
public NowhereToRun(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}");
// Flash
this.addAbility(FlashAbility.getInstance());
// When Nowhere to Run enters, target creature an opponent controls gets -3/-3 until end of turn.
Ability etbAbility = new EntersBattlefieldTriggeredAbility(new BoostTargetEffect(-3, -3, Duration.EndOfTurn));
etbAbility.addTarget(new TargetOpponentsCreaturePermanent());
this.addAbility(etbAbility);
// Creatures your opponents control can be the targets of spells and abilities as though they didn't have hexproof. Ward abilities of those creatures don't trigger.
Ability staticAbility = new SimpleStaticAbility(new NowhereToRunHexproofEffect());
staticAbility.addEffect(new NowhereToRunWardEffect());
this.addAbility(staticAbility);
}
private NowhereToRun(final NowhereToRun card) {
super(card);
}
@Override
public NowhereToRun copy() {
return new NowhereToRun(this);
}
}
class NowhereToRunHexproofEffect extends AsThoughEffectImpl {
NowhereToRunHexproofEffect() {
super(AsThoughEffectType.HEXPROOF, Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "Creatures your opponents control "
+ "can be the targets of spells and "
+ "abilities as though they didn't "
+ "have hexproof.";
}
private NowhereToRunHexproofEffect(final NowhereToRunHexproofEffect effect) {
super(effect);
}
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
if (affectedControllerId.equals(source.getControllerId())) {
Permanent creature = game.getPermanent(sourceId);
return creature != null
&& creature.isCreature(game)
&& game.getOpponents(source.getControllerId()).contains(creature.getControllerId());
}
return false;
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public AsThoughEffect copy() {
return new NowhereToRunHexproofEffect(this);
}
}
class NowhereToRunWardEffect extends ContinuousRuleModifyingEffectImpl {
NowhereToRunWardEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "Ward abilities of those creatures don't trigger.";
}
private NowhereToRunWardEffect(final NowhereToRunWardEffect effect) {
super(effect);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType().equals(GameEvent.EventType.NUMBER_OF_TRIGGERS);
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Permanent permanent = game.getPermanent(event.getSourceId());
if (permanent == null || !permanent.isCreature(game)) {
return false;
}
if (!game.getOpponents(source.getControllerId()).contains(permanent.getControllerId())) {
return false;
}
return getValue("targetAbility") instanceof WardAbility;
}
@Override
public ContinuousEffect copy() {
return new NowhereToRunWardEffect(this);
}
}

View file

@ -0,0 +1,120 @@
package mage.cards.p;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AsEntersBattlefieldAbility;
import mage.abilities.common.DiesCreatureTriggeredAbility;
import mage.abilities.condition.common.ModeChoiceSourceCondition;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.decorator.ConditionalTriggeredAbility;
import mage.abilities.effects.common.ChooseModeEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility;
import mage.constants.*;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterNonlandPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.HorrorEnchantmentCreatureToken;
import mage.players.Player;
import mage.target.TargetPermanent;
/**
* @author Cguy7777
*/
public final class PhenomenonInvestigators extends CardImpl {
public PhenomenonInvestigators(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}");
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.DETECTIVE);
this.power = new MageInt(3);
this.toughness = new MageInt(4);
// As Phenomenon Investigators enters, choose Believe or Doubt.
this.addAbility(new AsEntersBattlefieldAbility(
new ChooseModeEffect("Believe or Doubt?", "Believe", "Doubt")));
// * Believe -- Whenever a nontoken creature you control dies, create a 2/2 black Horror enchantment creature token.
this.addAbility(new ConditionalTriggeredAbility(
new DiesCreatureTriggeredAbility(
new CreateTokenEffect(new HorrorEnchantmentCreatureToken()),
false,
StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN),
new ModeChoiceSourceCondition("Believe"),
"&bull Believe &mdash; Whenever a nontoken creature you control dies, " +
"create a 2/2 black Horror enchantment creature token."));
// * Doubt -- At the beginning of your end step, you may return a nonland permanent you own to your hand. If you do, draw a card.
this.addAbility(new ConditionalTriggeredAbility(
new BeginningOfEndStepTriggeredAbility(
new DoIfCostPaid(
new DrawCardSourceControllerEffect(1),
new PhenomenonInvestigatorsReturnCost())),
new ModeChoiceSourceCondition("Doubt"),
"&bull Doubt &mdash; At the beginning of your end step, you may return a nonland permanent " +
"you own to your hand. If you do, draw a card."));
}
private PhenomenonInvestigators(final PhenomenonInvestigators card) {
super(card);
}
@Override
public PhenomenonInvestigators copy() {
return new PhenomenonInvestigators(this);
}
}
class PhenomenonInvestigatorsReturnCost extends CostImpl {
private static final FilterPermanent filter = new FilterNonlandPermanent("a nonland permanent you own");
static {
filter.add(TargetController.YOU.getOwnerPredicate());
}
PhenomenonInvestigatorsReturnCost() {
this.addTarget(new TargetPermanent(filter).withNotTarget(true));
text = "return a nonland permanent you own to your hand";
}
private PhenomenonInvestigatorsReturnCost(final PhenomenonInvestigatorsReturnCost cost) {
super(cost);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player controller = game.getPlayer(controllerId);
if (controller != null) {
if (this.getTargets().choose(Outcome.ReturnToHand, controllerId, source.getSourceId(), source, game)) {
Permanent permanentToReturn = game.getPermanent(this.getTargets().getFirstTarget());
if (permanentToReturn == null) {
return false;
}
controller.moveCards(permanentToReturn, Zone.HAND, ability, game);
paid = true;
}
}
return paid;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return this.getTargets().canChoose(controllerId, source, game);
}
@Override
public PhenomenonInvestigatorsReturnCost copy() {
return new PhenomenonInvestigatorsReturnCost(this);
}
}

View file

@ -1,7 +1,5 @@
package mage.cards.s;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
@ -14,7 +12,6 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.common.FilterCreatureOrPlayer;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
@ -22,8 +19,9 @@ import mage.game.permanent.token.TIEFighterToken;
import mage.target.common.TargetCreatureOrPlayer;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
*
* @author Styxo
*/
public final class StarDestroyer extends CardImpl {
@ -33,7 +31,7 @@ public final class StarDestroyer extends CardImpl {
static {
filter1.add(CardType.ARTIFACT.getPredicate());
filter3.getCreatureFilter().add(Predicates.not(SubType.STARSHIP.getPredicate()));
filter3.getPermanentFilter().add(Predicates.not(SubType.STARSHIP.getPredicate()));
}
public StarDestroyer(UUID ownerId, CardSetInfo setInfo) {

View file

@ -1,6 +1,5 @@
package mage.cards.t;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
@ -10,24 +9,22 @@ import mage.abilities.keyword.SpaceflightAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.common.FilterCreatureOrPlayer;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.target.common.TargetCreatureOrPlayer;
import java.util.UUID;
/**
*
* @author NinthWorld
*/
public final class ThermalDetonator extends CardImpl {
private static final FilterCreatureOrPlayer filter = new FilterCreatureOrPlayer("creature without spaceflight or target player");
private static final FilterCreaturePermanent filterCreature = new FilterCreaturePermanent();
static {
filter.getCreatureFilter().add(Predicates.not(new AbilityPredicate(SpaceflightAbility.class)));
filter.getPermanentFilter().add(Predicates.not(new AbilityPredicate(SpaceflightAbility.class)));
}
public ThermalDetonator(UUID ownerId, CardSetInfo setInfo) {

View file

@ -1,4 +1,3 @@
package mage.cards.u;
import mage.MageInt;
@ -6,7 +5,7 @@ import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.GainsChoiceOfAbilitiesEffect;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.effects.common.continuous.BoostSourceEffect;
import mage.abilities.keyword.BandingAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.abilities.keyword.FlyingAbility;
@ -14,12 +13,12 @@ import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import java.util.UUID;
/**
*
* @author Styxo & L_J
*/
public final class UrzasAvenger extends CardImpl {
@ -32,7 +31,7 @@ public final class UrzasAvenger extends CardImpl {
this.toughness = new MageInt(4);
// {0}: Urza's Avenger gets -1/-1 and gains your choice of banding, flying, first strike, or trample until end of turn.
Ability ability = new SimpleActivatedAbility(new BoostTargetEffect(-1, -1)
Ability ability = new SimpleActivatedAbility(new BoostSourceEffect(-1, -1, Duration.EndOfTurn)
.setText("{this} gets -1/-1"), new ManaCostsImpl<>("{0}"));
ability.addEffect(new GainsChoiceOfAbilitiesEffect(GainsChoiceOfAbilitiesEffect.TargetType.Source, "", true,
BandingAbility.getInstance(), FlyingAbility.getInstance(), FirstStrikeAbility.getInstance(), TrampleAbility.getInstance())

View file

@ -5,7 +5,7 @@ import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.costs.common.SacrificeTargetCost;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.combat.CantBlockSourceEffect;
import mage.abilities.effects.common.combat.CantBeBlockedSourceEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -31,7 +31,7 @@ public final class VampireGourmand extends CardImpl {
this.addAbility(new AttacksTriggeredAbility(new DoIfCostPaid(
new DrawCardSourceControllerEffect(1),
new SacrificeTargetCost(StaticFilters.FILTER_ANOTHER_CREATURE)
).addEffect(new CantBlockSourceEffect(Duration.EndOfTurn).concatBy("and"))));
).addEffect(new CantBeBlockedSourceEffect(Duration.EndOfTurn).concatBy("and"))));
}
private VampireGourmand(final VampireGourmand card) {

View file

@ -1,10 +1,16 @@
package mage.sets;
import mage.cards.ExpansionSet;
import mage.collation.BoosterCollator;
import mage.collation.BoosterStructure;
import mage.collation.CardRun;
import mage.collation.RarityConfiguration;
import mage.constants.Rarity;
import mage.constants.SetType;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author BetaSteward_at_googlemail.com
@ -176,4 +182,61 @@ public final class AlaraReborn extends ExpansionSet {
cards.add(new SetCardInfo("Zealous Persecution", 85, Rarity.UNCOMMON, mage.cards.z.ZealousPersecution.class));
}
@Override
public BoosterCollator createCollator() {
return new AlaraRebornCollator();
}
}
// Booster collation info from https://vm1.substation33.com/tiera/t/lethe/arb.html
// Using USA collation
class AlaraRebornCollator implements BoosterCollator {
private final CardRun commonA = new CardRun(true, "51", "74", "122", "45", "52", "14", "134", "29", "75", "138", "43", "13", "59", "80", "135", "27", "7", "143", "46", "72", "96", "22", "134", "4", "80", "144", "17", "46", "96", "19", "7", "138", "105", "132", "95", "75", "51", "22", "4", "122", "56", "45", "143", "74", "29", "13", "48", "139", "56", "72", "17", "5", "144", "27", "43", "14", "139", "105", "52", "48", "132", "19", "59", "5", "135", "95");
private final CardRun commonB = new CardRun(true, "78", "131", "9", "55", "40", "107", "69", "18", "112", "61", "10", "40", "125", "79", "20", "107", "3", "55", "35", "116", "32", "79", "63", "112", "3", "66", "88", "142", "41", "32", "63", "141", "84", "54", "116", "66", "35", "20", "131", "38", "9", "54", "141", "78", "41", "84", "18", "142", "69", "61", "125", "10", "38", "88");
private final CardRun uncommonA = new CardRun(false, "1", "11", "23", "25", "34", "39", "62", "65", "68", "85", "89", "93", "99", "100", "101", "111", "120", "133", "136", "137", "140", "145");
private final CardRun uncommonB = new CardRun(false, "15", "16", "21", "26", "33", "44", "50", "57", "64", "76", "77", "83", "87", "94", "102", "108", "115", "127");
private final CardRun rare = new CardRun(false, "2", "2", "6", "6", "8", "8", "12", "12", "24", "24", "28", "28", "30", "30", "31", "31", "36", "36", "42", "42", "47", "47", "49", "49", "58", "58", "60", "60", "67", "67", "70", "70", "71", "71", "73", "73", "81", "81", "82", "82", "86", "86", "90", "90", "92", "92", "97", "97", "98", "98", "103", "103", "104", "104", "106", "106", "114", "114", "118", "118", "119", "119", "121", "121", "123", "123", "126", "126", "129", "129", "37", "53", "91", "109", "110", "113", "117", "124", "128", "130");
private final CardRun land = new CardRun(false, "ALA_230", "ALA_231", "ALA_232", "ALA_233", "ALA_234", "ALA_235", "ALA_236", "ALA_237", "ALA_238", "ALA_239", "ALA_240", "ALA_241", "ALA_242", "ALA_243", "ALA_244", "ALA_245", "ALA_246", "ALA_247", "ALA_248", "ALA_249");
private final BoosterStructure AAAAAABBBB = new BoosterStructure(
commonA, commonA, commonA, commonA, commonA, commonA,
commonB, commonB, commonB, commonB
);
private final BoosterStructure AAAAABBBBB = new BoosterStructure(
commonA, commonA, commonA, commonA, commonA,
commonB, commonB, commonB, commonB, commonB
);
private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB);
private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB);
private final BoosterStructure R1 = new BoosterStructure(rare);
private final BoosterStructure L1 = new BoosterStructure(land);
// In order for equal numbers of each common to exist, the average booster must contain:
// 5.5 A commons (11 / 2)
// 4.5 B commons ( 9 / 2)
private final RarityConfiguration commonRuns = new RarityConfiguration(
AAAAAABBBB,
AAAAABBBBB
);
// In order for equal numbers of each uncommon to exist, the average booster must contain:
// 1.65 A uncommons (33 / 20)
// 1.35 B uncommons (27 / 20)
// These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs
private final RarityConfiguration uncommonRuns = new RarityConfiguration(
AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB,
ABB, ABB, ABB, ABB, ABB, ABB, ABB
);
private final RarityConfiguration rareRuns = new RarityConfiguration(R1);
private final RarityConfiguration landRuns = new RarityConfiguration(L1);
@Override
public List<String> makeBooster() {
List<String> booster = new ArrayList<>();
booster.addAll(commonRuns.getNext().makeRun());
booster.addAll(uncommonRuns.getNext().makeRun());
booster.addAll(rareRuns.getNext().makeRun());
booster.addAll(landRuns.getNext().makeRun());
return booster;
}
}

View file

@ -1,10 +1,16 @@
package mage.sets;
import mage.cards.ExpansionSet;
import mage.collation.BoosterCollator;
import mage.collation.BoosterStructure;
import mage.collation.CardRun;
import mage.collation.RarityConfiguration;
import mage.constants.Rarity;
import mage.constants.SetType;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author BetaSteward_at_googlemail.com
@ -175,4 +181,60 @@ public final class Conflux extends ExpansionSet {
cards.add(new SetCardInfo("Zombie Outlander", 133, Rarity.COMMON, mage.cards.z.ZombieOutlander.class));
}
@Override
public BoosterCollator createCollator() {
return new ConfluxCollator();
}
}
// Booster collation info from https://vm1.substation33.com/tiera/t/lethe/cfx.html
// Using USA collation
class ConfluxCollator implements BoosterCollator {
private final CardRun commonA = new CardRun(true, "8", "97", "28", "63", "50", "130", "134", "42", "4", "72", "36", "78", "138", "133", "84", "7", "63", "56", "105", "32", "4", "137", "81", "72", "27", "135", "8", "54", "132", "97", "61", "28", "3", "56", "94", "60", "36", "54", "137", "16", "133", "78", "7", "37", "134", "67", "27", "40", "130", "94", "3", "50", "61", "32", "84", "135", "42", "60", "105", "37", "16", "132", "40", "81", "67", "138");
private final CardRun commonB = new CardRun(true, "90", "47", "39", "70", "51", "2", "119", "29", "106", "9", "90", "57", "86", "76", "21", "128", "68", "51", "109", "6", "39", "85", "52", "21", "106", "68", "119", "86", "19", "29", "57", "131", "70", "96", "9", "144", "128", "22", "6", "69", "47", "122", "76", "85", "19", "52", "109", "22", "96", "2", "144", "131", "69", "122");
private final CardRun uncommonA = new CardRun(false, "141", "5", "23", "41", "103", "24", "62", "82", "107", "83", "65", "66", "112", "114", "139", "13", "14", "143", "91", "129", "38", "55");
private final CardRun uncommonB = new CardRun(false, "1", "43", "104", "25", "45", "46", "111", "15", "89", "123", "34", "124", "125", "126", "93", "145", "73", "74");
private final CardRun rare = new CardRun(false, "58", "58", "59", "59", "99", "99", "100", "100", "79", "79", "80", "80", "142", "142", "44", "44", "136", "136", "108", "108", "64", "64", "110", "110", "30", "30", "48", "48", "113", "113", "116", "116", "10", "10", "11", "11", "31", "31", "118", "118", "87", "87", "49", "49", "140", "140", "88", "88", "71", "71", "17", "17", "53", "53", "33", "33", "18", "18", "92", "92", "127", "127", "35", "35", "75", "75", "20", "20", "77", "77", "98", "101", "102", "26", "115", "117", "12", "120", "121", "95");
private final CardRun land = new CardRun(false, "ALA_230", "ALA_231", "ALA_232", "ALA_233", "ALA_234", "ALA_235", "ALA_236", "ALA_237", "ALA_238", "ALA_239", "ALA_240", "ALA_241", "ALA_242", "ALA_243", "ALA_244", "ALA_245", "ALA_246", "ALA_247", "ALA_248", "ALA_249");
private final BoosterStructure AAAAAABBBB = new BoosterStructure(
commonA, commonA, commonA, commonA, commonA, commonA,
commonB, commonB, commonB, commonB
);
private final BoosterStructure AAAAABBBBB = new BoosterStructure(
commonA, commonA, commonA, commonA, commonA,
commonB, commonB, commonB, commonB, commonB
);
private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB);
private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB);
private final BoosterStructure R1 = new BoosterStructure(rare);
private final BoosterStructure L1 = new BoosterStructure(land);
// In order for equal numbers of each common to exist, the average booster must contain:
// 5.5 A commons (11 / 2)
// 4.5 B commons ( 9 / 2)
private final RarityConfiguration commonRuns = new RarityConfiguration(
AAAAAABBBB,
AAAAABBBBB
);
// In order for equal numbers of each uncommon to exist, the average booster must contain:
// 1.65 A uncommons (33 / 20)
// 1.35 B uncommons (27 / 20)
// These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs
private final RarityConfiguration uncommonRuns = new RarityConfiguration(
AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB,
ABB, ABB, ABB, ABB, ABB, ABB, ABB
);
private final RarityConfiguration rareRuns = new RarityConfiguration(R1);
private final RarityConfiguration landRuns = new RarityConfiguration(L1);
@Override
public List<String> makeBooster() {
List<String> booster = new ArrayList<>();
booster.addAll(commonRuns.getNext().makeRun());
booster.addAll(uncommonRuns.getNext().makeRun());
booster.addAll(rareRuns.getNext().makeRun());
booster.addAll(landRuns.getNext().makeRun());
return booster;
}
}

View file

@ -52,6 +52,7 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Conductive Machete", 244, Rarity.UNCOMMON, mage.cards.c.ConductiveMachete.class));
cards.add(new SetCardInfo("Coordinated Clobbering", 173, Rarity.UNCOMMON, mage.cards.c.CoordinatedClobbering.class));
cards.add(new SetCardInfo("Cracked Skull", 88, Rarity.COMMON, mage.cards.c.CrackedSkull.class));
cards.add(new SetCardInfo("Cryptid Inspector", 174, Rarity.COMMON, mage.cards.c.CryptidInspector.class));
cards.add(new SetCardInfo("Cult Healer", 2, Rarity.COMMON, mage.cards.c.CultHealer.class));
cards.add(new SetCardInfo("Cursed Recording", 131, Rarity.RARE, mage.cards.c.CursedRecording.class));
cards.add(new SetCardInfo("Cursed Windbreaker", 47, Rarity.UNCOMMON, mage.cards.c.CursedWindbreaker.class));
@ -131,6 +132,10 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Irreverent Gremlin", 142, Rarity.UNCOMMON, mage.cards.i.IrreverentGremlin.class));
cards.add(new SetCardInfo("Island", 273, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Jump Scare", 17, Rarity.COMMON, mage.cards.j.JumpScare.class));
cards.add(new SetCardInfo("Kaito, Bane of Nightmares", 220, Rarity.MYTHIC, mage.cards.k.KaitoBaneOfNightmares.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Kaito, Bane of Nightmares", 328, Rarity.MYTHIC, mage.cards.k.KaitoBaneOfNightmares.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Kaito, Bane of Nightmares", 354, Rarity.MYTHIC, mage.cards.k.KaitoBaneOfNightmares.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Kaito, Bane of Nightmares", 409, Rarity.MYTHIC, mage.cards.k.KaitoBaneOfNightmares.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Killer's Mask", 104, Rarity.UNCOMMON, mage.cards.k.KillersMask.class));
cards.add(new SetCardInfo("Kona, Rescue Beastie", 187, Rarity.RARE, mage.cards.k.KonaRescueBeastie.class));
cards.add(new SetCardInfo("Lakeside Shack", 262, Rarity.COMMON, mage.cards.l.LakesideShack.class));
@ -161,6 +166,7 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Neglected Manor", 264, Rarity.COMMON, mage.cards.n.NeglectedManor.class));
cards.add(new SetCardInfo("Niko, Light of Hope", 224, Rarity.MYTHIC, mage.cards.n.NikoLightOfHope.class));
cards.add(new SetCardInfo("Norin, Swift Survivalist", 145, Rarity.UNCOMMON, mage.cards.n.NorinSwiftSurvivalist.class));
cards.add(new SetCardInfo("Nowhere to Run", 111, Rarity.UNCOMMON, mage.cards.n.NowhereToRun.class));
cards.add(new SetCardInfo("Oblivious Bookworm", 225, Rarity.UNCOMMON, mage.cards.o.ObliviousBookworm.class));
cards.add(new SetCardInfo("Omnivorous Flytrap", 192, Rarity.RARE, mage.cards.o.OmnivorousFlytrap.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Omnivorous Flytrap", 322, Rarity.RARE, mage.cards.o.OmnivorousFlytrap.class, NON_FULL_USE_VARIOUS));

View file

@ -194,6 +194,7 @@ public final class DuskmournHouseOfHorrorCommander extends ExpansionSet {
cards.add(new SetCardInfo("Oversimplify", 228, Rarity.RARE, mage.cards.o.Oversimplify.class));
cards.add(new SetCardInfo("Overwhelming Stampede", 192, Rarity.RARE, mage.cards.o.OverwhelmingStampede.class));
cards.add(new SetCardInfo("Persistent Constrictor", 22, Rarity.RARE, mage.cards.p.PersistentConstrictor.class));
cards.add(new SetCardInfo("Phenomenon Investigators", 38, Rarity.RARE, mage.cards.p.PhenomenonInvestigators.class));
cards.add(new SetCardInfo("Ponder", 73, Rarity.COMMON, mage.cards.p.Ponder.class));
cards.add(new SetCardInfo("Portent", 74, Rarity.COMMON, mage.cards.p.Portent.class));
cards.add(new SetCardInfo("Primordial Mist", 123, Rarity.RARE, mage.cards.p.PrimordialMist.class));

View file

@ -76,6 +76,10 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Caesar, Legion's Emperor", 529, Rarity.MYTHIC, mage.cards.c.CaesarLegionsEmperor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Caesar, Legion's Emperor", 867, Rarity.MYTHIC, mage.cards.c.CaesarLegionsEmperor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Caesar, Legion's Emperor", 1064, Rarity.MYTHIC, mage.cards.c.CaesarLegionsEmperor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Cait, Cage Brawler", 96, Rarity.RARE, mage.cards.c.CaitCageBrawler.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Cait, Cage Brawler", 409, Rarity.RARE, mage.cards.c.CaitCageBrawler.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Cait, Cage Brawler", 624, Rarity.RARE, mage.cards.c.CaitCageBrawler.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Cait, Cage Brawler", 937, Rarity.RARE, mage.cards.c.CaitCageBrawler.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Canopy Vista", 255, Rarity.RARE, mage.cards.c.CanopyVista.class));
cards.add(new SetCardInfo("Canyon Slough", 256, Rarity.RARE, mage.cards.c.CanyonSlough.class));
cards.add(new SetCardInfo("Captain of the Watch", 157, Rarity.RARE, mage.cards.c.CaptainOfTheWatch.class));
@ -103,6 +107,10 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Crucible of Worlds", 357, Rarity.MYTHIC, mage.cards.c.CrucibleOfWorlds.class));
cards.add(new SetCardInfo("Crush Contraband", 158, Rarity.UNCOMMON, mage.cards.c.CrushContraband.class));
cards.add(new SetCardInfo("Cultivate", 196, Rarity.UNCOMMON, mage.cards.c.Cultivate.class));
cards.add(new SetCardInfo("Curie, Emergent Intelligence", 30, Rarity.RARE, mage.cards.c.CurieEmergentIntelligence.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Curie, Emergent Intelligence", 374, Rarity.RARE, mage.cards.c.CurieEmergentIntelligence.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Curie, Emergent Intelligence", 558, Rarity.RARE, mage.cards.c.CurieEmergentIntelligence.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Curie, Emergent Intelligence", 902, Rarity.RARE, mage.cards.c.CurieEmergentIntelligence.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Darkwater Catacombs", 260, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class));
cards.add(new SetCardInfo("Deadly Dispute", 184, Rarity.COMMON, mage.cards.d.DeadlyDispute.class));
cards.add(new SetCardInfo("Desdemona, Freedom's Edge", 101, Rarity.RARE, mage.cards.d.DesdemonaFreedomsEdge.class, NON_FULL_USE_VARIOUS));
@ -430,7 +438,10 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Windbrisk Heights", 315, Rarity.RARE, mage.cards.w.WindbriskHeights.class));
cards.add(new SetCardInfo("Winding Constrictor", 223, Rarity.UNCOMMON, mage.cards.w.WindingConstrictor.class));
cards.add(new SetCardInfo("Woodland Cemetery", 316, Rarity.RARE, mage.cards.w.WoodlandCemetery.class));
cards.add(new SetCardInfo("Yes Man, Personal Securitron", 29, Rarity.RARE, mage.cards.y.YesManPersonalSecuritron.class));
cards.add(new SetCardInfo("Yes Man, Personal Securitron", 29, Rarity.RARE, mage.cards.y.YesManPersonalSecuritron.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Yes Man, Personal Securitron", 373, Rarity.RARE, mage.cards.y.YesManPersonalSecuritron.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Yes Man, Personal Securitron", 557, Rarity.RARE, mage.cards.y.YesManPersonalSecuritron.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Yes Man, Personal Securitron", 901, Rarity.RARE, mage.cards.y.YesManPersonalSecuritron.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Young Deathclaws", 125, Rarity.UNCOMMON, mage.cards.y.YoungDeathclaws.class));
cards.removeIf(setCardInfo -> IkoriaLairOfBehemoths.mutateNames.contains(setCardInfo.getName())); // remove when mutate is implemented

View file

@ -19,7 +19,7 @@ public final class FoundationsJumpstart extends ExpansionSet {
super("Foundations Jumpstart", "J25", ExpansionSet.buildDate(2024, 11, 15), SetType.EXPANSION);
this.blockName = "Foundations"; // for sorting in GUI
this.hasBasicLands = true;
this.hasBoosters = false; // temporary
this.hasBoosters = false;
cards.add(new SetCardInfo("Abandon Reason", 513, Rarity.UNCOMMON, mage.cards.a.AbandonReason.class));
cards.add(new SetCardInfo("Academy Journeymage", 281, Rarity.COMMON, mage.cards.a.AcademyJourneymage.class));

View file

@ -19,7 +19,7 @@ public final class MarchOfTheMachineTheAftermath extends ExpansionSet {
super("March of the Machine: The Aftermath", "MAT", ExpansionSet.buildDate(2023, 5, 12), SetType.SUPPLEMENTAL_STANDARD_LEGAL);
this.blockName = "March of the Machine";
this.hasBasicLands = false;
this.hasBoosters = false; // temporary
this.hasBoosters = false;
cards.add(new SetCardInfo("Animist's Might", 120, Rarity.UNCOMMON, mage.cards.a.AnimistsMight.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Animist's Might", 20, Rarity.UNCOMMON, mage.cards.a.AnimistsMight.class, NON_FULL_USE_VARIOUS));

View file

@ -21,6 +21,6 @@ public final class UnknownEvent extends ExpansionSet {
this.hasBasicLands = false;
this.hasBoosters = false;
cards.add(new SetCardInfo("More of That Strange Oil", "CU13", Rarity.COMMON, mage.cards.m.MoreOfThatStrangeOil.class));
cards.add(new SetCardInfo("More of That Strange Oil...", "CU13", Rarity.COMMON, mage.cards.m.MoreOfThatStrangeOil.class));
}
}

View file

@ -3,7 +3,7 @@
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../Config.xsd">
<server serverAddress="0.0.0.0" serverName="mage-server" port="17171" maxGameThreads="10" maxSecondsIdle="600"/>
<playerTypes>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
</playerTypes>
<gameTypes>

View file

@ -6,6 +6,9 @@ import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseAI;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @author JayDi85
*/
@ -21,6 +24,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
// 2 x 2/2 vs 0 - can't lose any attackers
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2
checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -35,6 +40,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -49,6 +56,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1
checkAttackers("x1 attack", 1, playerA, "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -63,6 +72,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1
checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -75,6 +86,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
public void test_Attack_1_small_vs_0() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
checkAttackers("x1 attack", 1, playerA, "Arbor Elf");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -88,6 +101,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2
checkAttackers("no attack", 1, playerA, "");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -101,6 +116,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 2); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2
checkAttackers("no attack", 1, playerA, "");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
@ -114,6 +131,11 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 15); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9
String needAttackers = IntStream.rangeClosed(1, 15)
.mapToObj(x -> "Balduvian Bears")
.collect(Collectors.joining("^"));
checkAttackers("x15 attack", 1, playerA, needAttackers);
block(1, playerB, "Ancient Brontodon", "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
@ -131,6 +153,10 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9
block(1, playerB, "Ancient Brontodon", "Balduvian Bears");
String needAttackers = IntStream.rangeClosed(1, 10)
.mapToObj(x -> "Balduvian Bears")
.collect(Collectors.joining("^"));
checkAttackers("x10 attack", 1, playerA, needAttackers);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -146,6 +172,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9
block(1, playerB, "Ancient Brontodon", "Goblin Brigand");
checkAttackers("forced x1 attack", 1, playerA, "Goblin Brigand");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -163,6 +190,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
block(1, playerB, "Ancient Brontodon:0", "Goblin Brigand:0");
block(1, playerB, "Ancient Brontodon:1", "Goblin Brigand:1");
checkAttackers("forced x2 attack", 1, playerA, "Goblin Brigand", "Goblin Brigand");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -183,6 +211,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerB, "Trove of Temptation", 1); // 9/9
block(1, playerB, "Ancient Brontodon", "Arbor Elf");
checkAttackers("forced x1 attack", 1, playerA, "Arbor Elf");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -201,6 +230,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerB, "Seeker of Slaanesh", 1); // 3/3
block(1, playerB, "Seeker of Slaanesh", "Arbor Elf");
checkAttackers("forced x1 attack", 1, playerA, "Arbor Elf");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -217,6 +247,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Chainbreaker", 1); // 3/3, but with 2x -1/-1 counters
checkAttackers("x1 attack", 1, playerA, "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();

View file

@ -19,6 +19,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 blocker", 1, playerB, "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -37,8 +38,9 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
attack(1, playerA, "Arbor Elf");
// ai must block
// ai must block by optimal blocker
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "Balduvian Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -58,6 +60,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 blocker", 1, playerB, "Arbor Elf");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -78,6 +81,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must not block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("no blockers", 1, playerB, "");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -100,6 +104,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must not block
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("no blockers", 1, playerB, "");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -123,6 +128,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must block bigger attacker and survive (6/6 must block 5/5)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "Colossal Dreadmaw");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -151,6 +157,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must block bigger attacker and survive (3/3 must block 2/2)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
@ -175,6 +182,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
// ai must use smaller blocker and survive (3/3 must block 2/2)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);

View file

@ -0,0 +1,106 @@
package org.mage.test.cards.enchantments;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author markort147
*/
public class NowhereToRunTest extends CardTestPlayerBase {
// Prevent ward from triggering on opponent's creatures
@Test
public void testWardPreventingOnOpponentsCreatures() {
addCard(Zone.BATTLEFIELD, playerB, "Waterfall Aerialist", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.HAND, playerA, "Nowhere to Run", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nowhere to Run");
addTarget(playerA, "Waterfall Aerialist");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Waterfall Aerialist", 0);
}
// Does not prevent ward from triggering on own creatures
@Test
public void testWardOnOwnCreatures() {
addCard(Zone.BATTLEFIELD, playerA, "Waterfall Aerialist", 1);
addCard(Zone.BATTLEFIELD, playerA, "Nowhere to Run", 1);
addCard(Zone.HAND, playerB, "Swords to Plowshares", 1);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Swords to Plowshares");
addTarget(playerB, "Waterfall Aerialist");
setChoice(playerB, false);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Waterfall Aerialist", 1);
assertGraveyardCount(playerB, "Swords to Plowshares", 1);
}
// Prevent hexproof on opponent's creatures
@Test
public void testHexproofOnCreatures() {
addCard(Zone.BATTLEFIELD, playerB, "Gladecover Scout", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Nowhere to Run", 1);
addCard(Zone.HAND, playerA, "Go for the Throat", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Go for the Throat");
addTarget(playerA, "Gladecover Scout");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerB, "Gladecover Scout", 0);
}
// Does not prevent hexproof on non-creature permanents
@Test
public void testHexproofOnOtherPermanents() {
addCard(Zone.BATTLEFIELD, playerB, "Valgavoth's Lair", 1);
setChoice(playerB, "Red");
addCard(Zone.BATTLEFIELD, playerA, "Nowhere to Run", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, "Stone Rain", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stone Rain");
addTarget(playerA, "Valgavoth's Lair");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
try {
execute();
} catch (Throwable e) {
if (!e.getMessage().contains("Targets list was setup by addTarget with [Valgavoth's Lair], but not used")) {
Assert.fail("must throw error about bad targets, but got:\n" + e.getMessage());
}
return;
}
Assert.fail("must throw exception on execute");
}
}

View file

@ -0,0 +1,156 @@
package org.mage.test.cards.single.blb;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.IndestructibleAbility;
import mage.abilities.mana.GreenManaAbility;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class BelloBardOfTheBramblesTest extends CardTestPlayerBase {
private static final String bello = "Bello, Bard of the Brambles";
private static final String ashaya = "Ashaya, Soul of the Wild"; // Has type addition for lands
private static final String cityOnFire = "City on Fire"; // Is a 4+ cmc non-creature, non-aura enchantment
private static final String thranDynamo = "Thran Dynamo"; // Is a 4+ cmc non-creature, non-equipment artifact
private static final String aggravatedAssault = "Aggravated Assault"; // Is a 3 cmc non-creature, non-aura enchantment
private static final String abandonedSarcophagus = "Abandoned Sarcophagus"; // Is a 3 cmc non-creature, non-equipment artifact
private static final String bearUmbra = "Bear Umbra"; // Is a 4 cmc non-creature, aura enchantment
private static final String tangleweave = "Tangleweave Armor"; // Is a 4 cmc non-creature, equipment artifact
private static final String forest = "Forest";
private static final String mountain = "Mountain";
private static final String bear = "Grizzly Bears";
// During your turn, each non-Equipment artifact and non-Aura enchantment you control with mana value 4 or greater is a 4/4 Elemental creature in addition to its other types and has indestructible, haste, and "Whenever this creature deals combat damage to a player, draw a card."
// City on Fire should become a 4/4 Elemental creature with indestructible, haste, and "Whenever this creature deals combat damage to a player, draw a card."
// Thran Dynamo should become a 4/4 Elemental creature with indestructible, haste, and "Whenever this creature deals combat damage to a player, draw a card."
@Test
public void testBello() {
initBelloTest();
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
ensurePermanentHasBelloEffects(cityOnFire);
ensurePermanentHasBelloEffects(thranDynamo);
}
// Ensures that overlapping land and creature type addition effects are handled properly
// This was an issue encountered between Ashaya and Bello
// While both were on the field, creatures weren't Forests, and artifacts and enchantements that met Bello's criteria weren't 4/4 Elementals
@Test
public void testBelloTypeAddition(){
initBelloTest();
addCard(Zone.BATTLEFIELD, playerA, ashaya);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assert that Ashaya is not affected by Bello
assertType(ashaya, CardType.CREATURE, SubType.ELEMENTAL);
assertType(ashaya, CardType.LAND, SubType.FOREST);
assertAbility(playerA, ashaya, new GreenManaAbility(), true);
ensurePermanentDoesNotHaveBelloEffects(ashaya);
// Assert that City on Fire is affected by Bello and Ashaya
assertType(cityOnFire, CardType.LAND, SubType.FOREST);
ensurePermanentHasBelloEffects(cityOnFire);
}
// Ensures that Bello does not affect Equipment artifacts
// Tangleweave Armor should not be affected by Bello
@Test
public void testBelloEquipment(){
initBelloTest();
addCard(Zone.BATTLEFIELD, playerA, tangleweave);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assert that Equipment is not affected by Bello
ensurePermanentDoesNotHaveBelloEffects(tangleweave);
}
// Ensures that Bello does not affect Aura enchantments
// Aura should not be affected by Bello
@Test
public void testBelloAura(){
initBelloTest();
addCard(Zone.BATTLEFIELD, playerA, bear);
addCard(Zone.HAND, playerA, bearUmbra);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bearUmbra, bear);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assert that Aura is not affected by Bello
assertAttachedTo(playerA, bearUmbra, bear, true);
ensurePermanentDoesNotHaveBelloEffects(bearUmbra);
}
// Ensures that Bello does not affect less-than-3 cmc non-creature, non-aura enchantments
// Aggravated Assault should not be affected by Bello
@Test
public void testBelloLessThanFourCmcEnchantment(){
initBelloTest();
addCard(Zone.BATTLEFIELD, playerA, aggravatedAssault);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assert that Aggravated Assault is not affected by Bello
ensurePermanentDoesNotHaveBelloEffects(aggravatedAssault);
}
// Ensures that Bello does not affect less-than-3 cmc non-creature, non-equipment artifacts
// Abandoned Sarcophagus should not be affected by Bello
@Test
public void testBelloLessThanFourCmcArtifact(){
initBelloTest();
addCard(Zone.BATTLEFIELD, playerA, abandonedSarcophagus);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bello);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assert that Abandoned Sarcophagus is not affected by Bello
ensurePermanentDoesNotHaveBelloEffects(abandonedSarcophagus);
}
private void initBelloTest(){
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, forest, 10);
addCard(Zone.BATTLEFIELD, playerA, mountain, 10);
addCard(Zone.BATTLEFIELD, playerA, cityOnFire);
addCard(Zone.BATTLEFIELD, playerA, thranDynamo);
addCard(Zone.HAND, playerA, bello);
}
private void ensurePermanentHasBelloEffects(String permanentName){
assertType(permanentName, CardType.CREATURE, SubType.ELEMENTAL);
assertPowerToughness(playerA, permanentName, 4, 4);
assertAbility(playerA, permanentName, IndestructibleAbility.getInstance(), true);
assertAbility(playerA, permanentName, HasteAbility.getInstance(), true);
assertAbility(playerA, permanentName, new DealsCombatDamageToAPlayerTriggeredAbility(new DrawCardSourceControllerEffect(1), false), true);
}
private void ensurePermanentDoesNotHaveBelloEffects(String permanentName){
assertAbility(playerA, permanentName, IndestructibleAbility.getInstance(), false);
assertAbility(playerA, permanentName, HasteAbility.getInstance(), false);
assertAbility(playerA, permanentName, new DealsCombatDamageToAPlayerTriggeredAbility(new DrawCardSourceControllerEffect(1), false), false);
}
}

View file

@ -5,15 +5,17 @@ import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.Permanent;
import org.junit.Assert;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* Test restrictions for choosing attackers and blockers.
*
* @author noxx
* @author noxx, JayDi85
*/
public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@ -337,10 +339,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
/*
* Mogg Flunkies cannot attack alone. Cards like Goblin Assault force all goblins to attack each turn.
* Mogg Flunkies should not be able to attack.
*/
*/
@Test
public void testMustAttackButCannotAttackAlone()
{
public void testMustAttackButCannotAttackAlone() {
/* Mogg Flunkies {1}{R} 3/3
Creature Goblin
Mogg Flunkies can't attack or block alone.
@ -434,11 +435,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@Test
public void underworldCerberusBlockedByOneTest() {
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
addCard(Zone.BATTLEFIELD, playerA, "Underworld Cerberus");
addCard(Zone.BATTLEFIELD, playerB, "Memnite"); // 1/1
@ -450,18 +451,18 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
try {
execute();
fail("Expected exception not thrown");
} catch(UnsupportedOperationException e) {
} catch (UnsupportedOperationException e) {
assertEquals("Underworld Cerberus is blocked by 1 creature(s). It has to be blocked by 3 or more.", e.getMessage());
}
}
@Test
public void underworldCerberusBlockedByTwoTest() {
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
addCard(Zone.BATTLEFIELD, playerA, "Underworld Cerberus");
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 2); // 1/1
@ -474,7 +475,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
try {
execute();
fail("Expected exception not thrown");
} catch(UnsupportedOperationException e) {
} catch (UnsupportedOperationException e) {
assertEquals("Underworld Cerberus is blocked by 2 creature(s). It has to be blocked by 3 or more.", e.getMessage());
}
}
@ -482,11 +483,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@Test
public void underworldCerberusBlockedByThreeTest() {
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
addCard(Zone.BATTLEFIELD, playerA, "Underworld Cerberus");
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1
@ -511,17 +512,17 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
@Test
public void underworldCerberusBlockedByTenTest() {
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
/* Underworld Cerberus {3}{B}{3} 6/6
* Underworld Cerberus can't be blocked except by three or more creatures.
* Cards in graveyards can't be the targets of spells or abilities.
* When Underworld Cerberus dies, exile it and each player returns all creature cards from their graveyard to their hand.
*/
addCard(Zone.BATTLEFIELD, playerA, "Underworld Cerberus");
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
// Blocked by 10 creatures - this is acceptable as it's >3
attack(3, playerA, "Underworld Cerberus");
for(int i = 0; i < 10; i++) {
for (int i = 0; i < 10; i++) {
block(3, playerB, "Memnite:" + i, "Underworld Cerberus");
}
@ -541,24 +542,24 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertLife(playerA, 20);
assertLife(playerB, 20);
}
@Test
public void irresistiblePreyMustBeBlockedTest() {
addCard(Zone.BATTLEFIELD, playerA, "Llanowar Elves");
addCard(Zone.BATTLEFIELD, playerA, "Alpha Myr");
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.HAND, playerA, "Irresistible Prey");
addCard(Zone.BATTLEFIELD, playerB, "Bronze Sable");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Irresistible Prey", "Llanowar Elves"); // must be blocked
attack(1, playerA, "Llanowar Elves");
attack(1, playerA, "Alpha Myr");
// attempt to block the creature that doesn't have "must be blocked"
block(1, playerB, "Bronze Sable", "Alpha Myr");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
@ -568,4 +569,381 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBase {
assertTapped("Alpha Myr", true);
assertLife(playerB, 18);
}
}
@Test
public void test_MustBeBlocked_nothing() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("no blocker", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 4);
}
@Test
public void test_MustBeBlocked_1_blocker() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
}
@Test
public void test_MustBeBlocked_many_blockers_good() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
// take first available blocker
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
}
@Test
public void test_MustBeBlocked_many_blockers_bad() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
// take first available blocker
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertPermanentCount(playerA, "Fear of Being Hunted", 1);
}
@Test
@Ignore
// TODO: enable and duplicate for AI -- after implement choose blocker logic and isHuman replace by ~isComputer
public void test_MustBeBlocked_many_blockers_optimal() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
}
@Test
public void test_MustBlocking_zero_blockers() {
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("no blocker", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 2);
assertPermanentCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
public void test_MustBlocking_full_blockers() {
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
public void test_MustBlocking_many_blockers() {
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 5); // 1/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
@Ignore
// TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more.
// It's auto-fix in block configuration, so exception must be fixed cause AI works with it
public void test_MustBlocking_low_blockers() {
// possible bug: exception on wrong block configuration
// if effect require x3 blockers, but opponent has only 1 then it must use 1 blocker anyway
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
attack(1, playerA, "Sonorous Howlbonder");
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("one possible blocker", 1, playerB, "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
public void test_MustBeBlockedWithMenace_0_blockers() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("no blocker", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 4);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
public void test_MustBeBlockedWithMenace_all_blockers() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 2); // 1/1
// If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
public void test_MustBeBlockedWithMenace_many_blockers() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
// If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
@Test
public void test_MustBeBlockedWithMenace_low_blockers_auto() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1
// If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
//
// If a creature is required to block a creature with menace, another creature must also block that creature
// if able. If none can, the creature thats required to block can block another creature or not block at all.
// (2020-04-17)
// auto-fix block config inside
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("no blockers", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 4);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
@Test
@Ignore
// TODO: need exception fix java.lang.UnsupportedOperationException: Alley Strangler is blocked by 1 creature(s). It has to be blocked by 2 or more.
// It's ok to have such exception in unit tests from manual setup
// If it's impossible to auto-fix, then keep that error and ignore the test
public void test_MustBeBlockedWithMenace_low_blockers_manual() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); // 1/1
// If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
//
// If a creature is required to block a creature with menace, another creature must also block that creature
// if able. If none can, the creature thats required to block can block another creature or not block at all.
// (2020-04-17)
// define blocker manual
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
block(1, playerB, "Memnite", "Alley Strangler");
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("no blockers", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 4);
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
}

View file

@ -104,18 +104,17 @@ public class LoadCallbackClient implements CallbackClient {
GameClientMessage message = (GameClientMessage) callback.getData();
this.gameView = message.getGameView();
log.info(getLogStartInfo() + " target: " + message.getMessage());
switch (message.getMessage()) {
case "Select a starting player":
session.sendPlayerUUID(gameId, playerId);
return;
case "Select a card to discard":
log.info(getLogStartInfo() + "hand size: " + gameView.getMyHand().size());
SimpleCardView card = gameView.getMyHand().values().iterator().next();
session.sendPlayerUUID(gameId, card.getId());
return;
default:
log.error(getLogStartInfo() + "unknown GAME_TARGET message: " + message.toString());
return;
if (message.getMessage().startsWith("Select a starting player")) {
session.sendPlayerUUID(gameId, playerId);
return;
} else if (message.getMessage().startsWith("Select a card to discard")) {
log.info(getLogStartInfo() + "hand size: " + gameView.getMyHand().size());
SimpleCardView card = gameView.getMyHand().values().iterator().next();
session.sendPlayerUUID(gameId, card.getId());
return;
} else {
log.error(getLogStartInfo() + "unknown GAME_TARGET message: " + message.toString());
return;
}
}

View file

@ -27,6 +27,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* Intended to test Mage server under different load patterns.
@ -214,7 +215,7 @@ public class LoadTest {
}
}
public void playTwoAIGame(String gameName, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) {
public void playTwoAIGame(String gameName, Integer taskNumber, TasksProgress tasksProgress, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) {
Assert.assertFalse("need deck colors", deckColors.isEmpty());
Assert.assertFalse("need allowed sets", deckAllowedSets.isEmpty());
@ -249,9 +250,13 @@ public class LoadTest {
TableView checkGame = monitor.getTable(tableId).orElse(null);
TableState state = (checkGame == null ? null : checkGame.getTableState());
tasksProgress.update(taskNumber, state == TableState.FINISHED, gameView == null ? 0 : gameView.getTurn());
String globalProgress = tasksProgress.getInfo();
if (gameView != null && checkGame != null) {
logger.info(checkGame.getTableName() + ": ---");
logger.info(String.format("%s: turn %d, step %s, state %s",
logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---");
logger.info(String.format("%s, %s: turn %d, step %s, state %s",
globalProgress,
checkGame.getTableName(),
gameView.getTurn(),
gameView.getStep().toString(),
@ -278,7 +283,8 @@ public class LoadTest {
if (Objects.equals(gameView.getActivePlayerId(), p.getPlayerId())) {
activeInfo = " (active)";
}
logger.info(String.format("%s, status: %s - Life=%d; Lib=%d;%s",
logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d;%s",
globalProgress,
checkGame.getTableName(),
p.getName(),
p.getLife(),
@ -286,7 +292,7 @@ public class LoadTest {
activeInfo
));
});
logger.info(checkGame.getTableName() + ": ---");
logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---");
}
// ping to keep active session
@ -312,7 +318,9 @@ public class LoadTest {
LoadTestGameResultsList gameResults = new LoadTestGameResultsList();
long randomSeed = RandomUtil.nextInt();
LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed);
playTwoAIGame("Single AI game", randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
TasksProgress tasksProgress = new TasksProgress();
tasksProgress.update(1, true, 0);
playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
printGameResults(gameResults);
}
@ -347,15 +355,16 @@ public class LoadTest {
LoadTestGameResultsList gameResults = new LoadTestGameResultsList();
try {
TasksProgress tasksProgress = new TasksProgress();
for (int i = 0; i < seedsList.size(); i++) {
int gameIndex = i;
tasksProgress.update(gameIndex + 1, true, 0);
long randomSeed = seedsList.get(i);
logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed);
Future gameTask = executerService.submit(() -> {
String gameName = "AI game #" + (gameIndex + 1);
LoadTestGameResult gameResult = gameResults.createGame(gameIndex + 1, gameName, randomSeed);
playTwoAIGame(gameName, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
});
if (!isRunParallel) {
@ -571,6 +580,40 @@ public class LoadTest {
return createSimpleGameOptions(gameName, gameTypeView, session, PlayerType.COMPUTER_MAD);
}
private static class TasksProgress {
private String info;
private final Map<Integer, Boolean> finishes = new LinkedHashMap<>();
private final Map<Integer, Integer> turns = new LinkedHashMap<>();
synchronized public void update(Integer taskNumber, boolean newFinish, Integer newTurn) {
Boolean oldFinish = this.finishes.getOrDefault(taskNumber, false);
Integer oldTurn = this.turns.getOrDefault(taskNumber, 0);
if (!this.finishes.containsKey(taskNumber)
|| !Objects.equals(oldFinish, newFinish)
|| !Objects.equals(oldTurn, newTurn)) {
this.finishes.put(taskNumber, newFinish);
this.turns.put(taskNumber, newTurn);
updateInfo();
}
}
private void updateInfo() {
// example: progress [=00, +01, +01, =12, =15, =01, +61]
String res = this.finishes.keySet().stream()
.map(taskNumber -> String.format("%s%02d",
this.finishes.getOrDefault(taskNumber, false) ? "=" : "+",
this.turns.getOrDefault(taskNumber, 0)
))
.collect(Collectors.joining(", "));
this.info = String.format("progress [%s]", res);
}
public String getInfo() {
return this.info;
}
}
private class LoadPlayer {
String userName;
@ -798,23 +841,23 @@ public class LoadTest {
}
public int getLife1() {
return this.finalGameView.getPlayers().get(0).getLife();
return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(0).getLife();
}
public int getLife2() {
return this.finalGameView.getPlayers().get(1).getLife();
return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getLife();
}
public int getTurn() {
return this.finalGameView.getTurn();
return finalGameView == null ? 0 : this.finalGameView.getTurn();
}
public int getDurationMs() {
return (int) ((this.timeEnded.getTime() - this.timeStarted.getTime()));
return finalGameView == null ? 0 : ((int) ((this.timeEnded.getTime() - this.timeStarted.getTime())));
}
public int getTotalErrorsCount() {
return this.finalGameView.getTotalErrorsCount();
return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount();
}
}
@ -886,15 +929,15 @@ public class LoadTest {
}
private int getAvgTurn() {
return this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
}
private int getAvgLife1() {
return this.values().stream().mapToInt(LoadTestGameResult::getLife1).sum() / this.size();
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife1).sum() / this.size();
}
private int getAvgLife2() {
return this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
}
private int getTotalDurationMs() {
@ -902,11 +945,12 @@ public class LoadTest {
}
private int getAvgDurationMs() {
return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size();
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size();
}
private int getAvgDurationPerTurnMs() {
return getAvgDurationMs() / getAvgTurn();
int turns = getAvgTurn();
return turns == 0 ? 0 : getAvgDurationMs() / getAvgTurn();
}
}
@ -952,5 +996,9 @@ public class LoadTest {
gameResults.printResultHeader();
gameResults.printResultData();
gameResults.printResultTotal();
if (gameResults.getAvgTurn() == 0) {
Assert.fail("Games can't start, make sure you are run a localhost server before running current load test");
}
}
}

View file

@ -28,10 +28,7 @@ import mage.filter.common.*;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.filter.predicate.permanent.SummoningSicknessPredicate;
import mage.game.Game;
import mage.game.GameImpl;
import mage.game.Graveyard;
import mage.game.Table;
import mage.game.*;
import mage.game.combat.CombatGroup;
import mage.game.command.CommandObject;
import mage.game.draft.Draft;
@ -52,6 +49,7 @@ import mage.target.common.*;
import mage.util.CardUtil;
import mage.util.MultiAmountMessage;
import mage.util.RandomUtil;
import mage.watchers.common.AttackedOrBlockedThisCombatWatcher;
import org.apache.log4j.Logger;
import org.junit.Assert;
@ -839,6 +837,20 @@ public class TestPlayer implements Player {
wasProccessed = true;
}
// check attacking: attackers list
if (params[0].equals(CHECK_COMMAND_ATTACKERS) && params.length == 2) {
assertAttackers(action, game, computerPlayer, params[1]);
actions.remove(action);
wasProccessed = true;
}
// check attacking: attackers list
if (params[0].equals(CHECK_COMMAND_BLOCKERS) && params.length == 2) {
assertBlockers(action, game, computerPlayer, params[1]);
actions.remove(action);
wasProccessed = true;
}
// check playable ability: ability text, must have
if (params[0].equals(CHECK_COMMAND_PLAYABLE_ABILITY) && params.length == 3) {
assertPlayableAbility(action, game, computerPlayer, params[1], Boolean.parseBoolean(params[2]));
@ -1418,6 +1430,64 @@ public class TestPlayer implements Player {
}
}
private void assertAttackers(PlayerAction action, Game game, Player player, String attackers) {
AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class);
Assert.assertNotNull(watcher);
List<String> actualAttackers = watcher.getAttackedThisTurnCreatures().stream()
.map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki
.filter(Objects::nonNull)
.filter(o -> o instanceof ControllableOrOwnerable)
.map(o -> (ControllableOrOwnerable) o)
.filter(o -> o.getControllerOrOwnerId().equals(player.getId()))
.map(o -> ((MageObject) o).getName())
.sorted()
.collect(Collectors.toList());
List<String> needAttackers = Arrays.stream(attackers.split("\\^"))
.filter(s -> !s.equals(TestPlayer.ATTACK_SKIP))
.sorted()
.collect(Collectors.toList());
if (!actualAttackers.equals(needAttackers)) {
printStart(game, action.getActionName());
System.out.println(String.format("Need attackers: %d", needAttackers.size()));
needAttackers.forEach(s -> System.out.println(" - " + s));
System.out.println(String.format("Actual attackers: %d", actualAttackers.size()));
actualAttackers.forEach(s -> System.out.println(" - " + s));
printEnd();
Assert.fail("Found wrong attackers");
}
}
private void assertBlockers(PlayerAction action, Game game, Player player, String blockers) {
AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class);
Assert.assertNotNull(watcher);
List<String> actualBlockers = watcher.getBlockedThisTurnCreatures().stream()
.map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki
.filter(Objects::nonNull)
.filter(o -> o instanceof ControllableOrOwnerable)
.map(o -> (ControllableOrOwnerable) o)
.filter(o -> o.getControllerOrOwnerId().equals(player.getId()))
.map(o -> ((MageObject) o).getName())
.sorted()
.collect(Collectors.toList());
List<String> needBlockers = Arrays.stream(blockers.split("\\^"))
.filter(s -> !s.equals(TestPlayer.BLOCK_SKIP))
.sorted()
.collect(Collectors.toList());
if (!actualBlockers.equals(needBlockers)) {
printStart(game, action.getActionName());
System.out.println(String.format("Need blockers: %d", needBlockers.size()));
needBlockers.forEach(s -> System.out.println(" - " + s));
System.out.println(String.format("Actual blockers: %d", actualBlockers.size()));
actualBlockers.forEach(s -> System.out.println(" - " + s));
printEnd();
Assert.fail("Found wrong blockers");
}
}
private void assertMayAttackDefender(PlayerAction action, Game game, Player controller, String permanentName, Player defender, boolean expectedMayAttack) {
Permanent attackingPermanent = findPermanentWithAssert(action, game, controller, permanentName);
@ -2216,13 +2286,10 @@ public class TestPlayer implements Player {
// TODO: Allow to choose a player with TargetPermanentOrPlayer
if ((target.getOriginalTarget() instanceof TargetPermanent)
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) // player target not implemented yet
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet
FilterPermanent filterPermanent;
if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
filterPermanent = ((TargetPermanentOrPlayer) target.getOriginalTarget()).getFilterPermanent();
} else if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
filterPermanent = ((TargetCreatureOrPlayer) target.getOriginalTarget()).getFilterCreature();
} else {
filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter();
}
@ -2446,8 +2513,6 @@ public class TestPlayer implements Player {
// player
if (target.getOriginalTarget() instanceof TargetPlayer
|| target.getOriginalTarget() instanceof TargetAnyTarget
|| target.getOriginalTarget() instanceof TargetCreatureOrPlayer
|| target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) {
if (!targetDefinition.startsWith("targetPlayer=")) {
@ -2469,8 +2534,6 @@ public class TestPlayer implements Player {
// permanent in battlefield
if ((target.getOriginalTarget() instanceof TargetPermanent)
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)
|| (target.getOriginalTarget() instanceof TargetAnyTarget)
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer)
|| (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard)) {
for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) {
if (targetDefinition.startsWith("targetPlayer=")) {
@ -2494,9 +2557,6 @@ public class TestPlayer implements Player {
}
}
Filter filter = target.getOriginalTarget().getFilter();
if (filter instanceof FilterCreatureOrPlayer) {
filter = ((FilterCreatureOrPlayer) filter).getCreatureFilter();
}
if (filter instanceof FilterPermanentOrPlayer) {
filter = ((FilterPermanentOrPlayer) filter).getPermanentFilter();
}
@ -4484,10 +4544,6 @@ public class TestPlayer implements Player {
return AIPlayer;
}
public String getHistory() {
return computerPlayer.getHistory();
}
@Override
public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides) {
return computerPlayer.rollPlanarDie(outcome, source, game, numberChaosSides, numberPlanarSides);

View file

@ -87,6 +87,8 @@ 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_PLAYABLE_ABILITY = "PLAYABLE_ABILITY";
public static final String CHECK_COMMAND_ATTACKERS = "ATTACKERS";
public static final String CHECK_COMMAND_BLOCKERS = "BLOCKERS";
public static final String CHECK_COMMAND_MAY_ATTACK_DEFENDER = "MAY_ATTACK_DEFENDER";
public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT";
public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED";
@ -428,7 +430,33 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
}
/**
* Checks whether or not a creature can attack on a given turn a defender (player only for now, could be extended to permanents)
* Make sure in last declared attackers
*
* @param attackers in any order, use empty string or params for no attackers check
*/
public void checkAttackers(String checkName, int turnNum, TestPlayer player, String... attackers) {
String list = String.join("^", attackers);
if (list.isEmpty()) {
list = TestPlayer.ATTACK_SKIP;
}
check(checkName, turnNum, PhaseStep.DECLARE_ATTACKERS, player, CHECK_COMMAND_ATTACKERS, list);
}
/**
* Make sure in last declared blockers
*
* @param blockers in any order, use empty string or params for no blockers check
*/
public void checkBlockers(String checkName, int turnNum, TestPlayer player, String... blockers) {
String list = String.join("^", blockers);
if (list.isEmpty()) {
list = TestPlayer.BLOCK_SKIP;
}
check(checkName, turnNum, PhaseStep.DECLARE_BLOCKERS, player, CHECK_COMMAND_BLOCKERS, list);
}
/**
* Checks whether a creature can attack on a given turn a defender (player only for now, could be extended to permanents)
*
* @param checkName String to show up if the check fails, for display purposes only.
* @param turnNum The turn number to check on.

View file

@ -55,11 +55,11 @@ public class LoadCheatsTest extends CardTestPlayerBase {
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
setChoice(playerA, "5"); // choose [group 3]: 5 = 2 default menus + 3 group
setChoice(playerA, "7"); // choose [group 3]: 7 = 4 default menus + 3 group
SystemUtil.executeCheatCommands(currentGame, commandsFile, playerA);
assertHandCount(playerA, "Razorclaw Bear", 1);
assertPermanentCount(playerA, "Mountain", 3);
assertHandCount(playerA, "Island", 10); // by cheats
assertHandCount(playerA, "Island", 10); // possible fail: changed in amount of default cheat commands
}
}

View file

@ -1,14 +1,13 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.abilities.hint.common.CanPlayAdditionalLandsHint;
import mage.cards.Card;
import mage.constants.AbilityType;
import mage.constants.AsThoughEffectType;
import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import java.util.HashMap;
import java.util.Set;
import java.util.UUID;
@ -21,6 +20,8 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
super(AbilityType.PLAY_LAND, Zone.HAND);
this.usesStack = false;
this.name = "Play " + cardName;
this.addHint(CanPlayAdditionalLandsHint.instance);
}
protected PlayLandAbility(final PlayLandAbility ability) {
@ -43,7 +44,7 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
}
//20091005 - 114.2a
if(!game.isActivePlayer(playerId)
if (!game.isActivePlayer(playerId)
|| !game.getPlayer(playerId).canPlayLand()
|| !game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
@ -54,16 +55,15 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
if (!approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
Zone zone = game.getState().getZone(sourceId);
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
if (card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
approvingObjects.add(new ApprovingObject(this, game));
}
}
if(approvingObjects.isEmpty()) {
if (approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
} else {
return new ActivationStatus(approvingObjects);
}
}
@ -87,5 +87,4 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
public PlayLandAbility copy() {
return new PlayLandAbility(this);
}
}

View file

@ -1,42 +0,0 @@
package mage.abilities.common.delayed;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* @author nantuko
*/
public class AtTheEndOfTurnStepPostDelayedTriggeredAbility extends DelayedTriggeredAbility {
public AtTheEndOfTurnStepPostDelayedTriggeredAbility(Effect effect) {
this(effect, false);
}
public AtTheEndOfTurnStepPostDelayedTriggeredAbility(Effect effect, boolean usesStack) {
super(effect);
this.usesStack = usesStack;
setTriggerPhrase("At end of turn ");
}
public AtTheEndOfTurnStepPostDelayedTriggeredAbility(AtTheEndOfTurnStepPostDelayedTriggeredAbility ability) {
super(ability);
}
@Override
public AtTheEndOfTurnStepPostDelayedTriggeredAbility copy() {
return new AtTheEndOfTurnStepPostDelayedTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.END_TURN_STEP_POST;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return true;
}
}

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
@ -9,6 +8,7 @@ import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
/**
* Each player may play an additional land on each of their turns.
@ -49,14 +49,10 @@ public class PlayAdditionalLandsAllEffect extends ContinuousEffectImpl {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(game.getActivePlayerId());
if (player != null) {
if (numExtraLands == Integer.MAX_VALUE) {
player.setLandsPerTurn(Integer.MAX_VALUE);
} else {
player.setLandsPerTurn(player.getLandsPerTurn() + numExtraLands);
}
return true;
if (player == null) {
return false;
}
player.setLandsPerTurn(CardUtil.overflowInc(player.getLandsPerTurn(), numExtraLands));
return true;
}
}

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
@ -37,14 +36,11 @@ public class PlayAdditionalLandsControllerEffect extends ContinuousEffectImpl {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player != null) {
if (player.getLandsPerTurn() == Integer.MAX_VALUE || this.additionalCards == Integer.MAX_VALUE) {
player.setLandsPerTurn(Integer.MAX_VALUE);
} else {
player.setLandsPerTurn(player.getLandsPerTurn() + this.additionalCards);
}
return true;
if (player == null) {
return false;
}
player.setLandsPerTurn(CardUtil.overflowInc(player.getLandsPerTurn(), additionalCards));
return true;
}

View file

@ -0,0 +1,45 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
import mage.abilities.hint.Hint;
import mage.abilities.hint.HintUtils;
import mage.game.Game;
import mage.players.Player;
/**
* Global hint for all lands
*
* @author JayDi85
*/
public enum CanPlayAdditionalLandsHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
Player controller = game.getPlayer(ability.getControllerId());
if (controller == null) {
return "";
}
// hide hint on default 1 land settings (useless to show)
if (controller.getLandsPerTurn() == 1) {
return "";
}
String stats = String.format(" (played %d of %s)",
controller.getLandsPlayed(),
(controller.getLandsPerTurn() == Integer.MAX_VALUE ? "any" : String.valueOf(controller.getLandsPerTurn()))
);
if (controller.canPlayLand()) {
return HintUtils.prepareText("Can play more lands" + stats, null, HintUtils.HINT_ICON_GOOD);
} else {
return HintUtils.prepareText("Can't play lands" + stats, null, HintUtils.HINT_ICON_BAD);
}
}
@Override
public Hint copy() {
return instance;
}
}

View file

@ -1,79 +1,22 @@
package mage.filter.common;
import mage.MageItem;
import mage.abilities.Ability;
import mage.filter.FilterImpl;
import mage.filter.FilterInPlay;
import mage.filter.FilterPlayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
public class FilterCreatureOrPlayer extends FilterImpl<MageItem> implements FilterInPlay<MageItem> {
protected FilterCreaturePermanent creatureFilter;
protected final FilterPlayer playerFilter;
public class FilterCreatureOrPlayer extends FilterPermanentOrPlayer {
public FilterCreatureOrPlayer() {
this("creature or player");
}
public FilterCreatureOrPlayer(String name) {
super(name);
creatureFilter = new FilterCreaturePermanent();
playerFilter = new FilterPlayer();
super(name, new FilterCreaturePermanent(), new FilterPlayer());
}
protected FilterCreatureOrPlayer(final FilterCreatureOrPlayer filter) {
super(filter);
this.creatureFilter = filter.creatureFilter.copy();
this.playerFilter = filter.playerFilter.copy();
}
@Override
public boolean checkObjectClass(Object object) {
return true;
}
@Override
public boolean match(MageItem o, Game game) {
if (super.match(o, game)) {
if (o instanceof Player) {
return playerFilter.match((Player) o, game);
} else if (o instanceof Permanent) {
return creatureFilter.match((Permanent) o, game);
}
}
return false;
}
@Override
public boolean match(MageItem o, UUID playerId, Ability source, Game game) {
if (super.match(o, game)) { // process predicates
if (o instanceof Player) {
return playerFilter.match((Player) o, playerId, source, game);
} else if (o instanceof Permanent) {
return creatureFilter.match((Permanent) o, playerId, source, game);
}
}
return false;
}
public FilterCreaturePermanent getCreatureFilter() {
return this.creatureFilter;
}
public FilterPlayer getPlayerFilter() {
return this.playerFilter;
}
public void setCreatureFilter(FilterCreaturePermanent creatureFilter) {
this.creatureFilter = creatureFilter;
}
@Override

View file

@ -550,6 +550,12 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
@Deprecated // TODO: must research usage and remove it from all non engine code (example: Bestow ability, ProcessActions must be used instead)
boolean checkStateAndTriggered();
/**
* Play priority by all players
*
* @param activePlayerId starting priority player
* @param resuming false to reset passed priority and ask it again
*/
void playPriority(UUID activePlayerId, boolean resuming);
void resetControlAfterSpellResolve(UUID topId);

View file

@ -845,6 +845,10 @@ public abstract class GameImpl implements Game {
// concede for itself
// stop current player dialog and execute concede
currentPriorityPlayer.signalPlayerConcede(true);
} else if (currentPriorityPlayer.getTurnControlledBy().equals(playerId)) {
// concede for itself while controlling another player
// stop current player dialog and execute concede
currentPriorityPlayer.signalPlayerConcede(true);
} else {
// concede for another player
// allow current player to continue and check concede on any next priority
@ -1423,6 +1427,7 @@ public abstract class GameImpl implements Game {
newWatchers.add(new CreaturesDiedWatcher());
newWatchers.add(new TemptedByTheRingWatcher());
newWatchers.add(new SpellsCastWatcher());
newWatchers.add(new AttackedOrBlockedThisCombatWatcher()); // required for tests
// runtime check - allows only GAME scope (one watcher per game)
newWatchers.forEach(watcher -> {
@ -2986,21 +2991,35 @@ public abstract class GameImpl implements Game {
}
String message;
if (this.canPlaySorcery(playerId)) {
message = "Play spells and abilities.";
message = "Play spells and abilities";
} else {
message = "Play instants and activated abilities.";
message = "Play instants and activated abilities";
}
playerQueryEventSource.select(playerId, message);
message += getControllingPlayerHint(playerId);
Player player = this.getPlayer(playerId);
playerQueryEventSource.select(player.getTurnControlledBy(), message);
getState().clearLookedAt();
getState().clearRevealed();
}
private String getControllingPlayerHint(UUID playerId) {
Player player = this.getPlayer(playerId);
Player controllingPlayer = this.getPlayer(player.getTurnControlledBy());
if (player != controllingPlayer) {
return " (as " + player.getLogName() + ")";
} else {
return "";
}
}
@Override
public synchronized void fireSelectEvent(UUID playerId, String message) {
if (simulation) {
return;
}
playerQueryEventSource.select(playerId, message);
playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId));
}
@Override
@ -3008,7 +3027,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.select(playerId, message, options);
playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId), options);
}
@Override
@ -3016,7 +3035,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.playMana(playerId, message, options);
playerQueryEventSource.playMana(playerId, message + getControllingPlayerHint(playerId), options);
}
@Override
@ -3024,7 +3043,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.playXMana(playerId, message);
playerQueryEventSource.playXMana(playerId, message + getControllingPlayerHint(playerId));
}
@Override
@ -3037,7 +3056,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.ask(playerId, message.getMessage(), source, addMessageToOptions(message, options));
playerQueryEventSource.ask(playerId, message.getMessage() + getControllingPlayerHint(playerId), source, addMessageToOptions(message, options));
}
@Override
@ -3049,7 +3068,7 @@ public abstract class GameImpl implements Game {
if (object != null) {
objectName = object.getName();
}
playerQueryEventSource.chooseAbility(playerId, message, objectName, choices);
playerQueryEventSource.chooseAbility(playerId, message + getControllingPlayerHint(playerId), objectName, choices);
}
@Override
@ -3057,7 +3076,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.chooseMode(playerId, message, modes);
playerQueryEventSource.chooseMode(playerId, message + getControllingPlayerHint(playerId), modes);
}
@Override
@ -3065,7 +3084,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.target(playerId, message.getMessage(), targets, required, addMessageToOptions(message, options));
playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), targets, required, addMessageToOptions(message, options));
}
@Override
@ -3073,7 +3092,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.target(playerId, message.getMessage(), cards, required, addMessageToOptions(message, options));
playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), cards, required, addMessageToOptions(message, options));
}
/**
@ -3086,7 +3105,7 @@ public abstract class GameImpl implements Game {
*/
@Override
public void fireSelectTargetTriggeredAbilityEvent(UUID playerId, String message, List<TriggeredAbility> abilities) {
playerQueryEventSource.target(playerId, message, abilities);
playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), abilities);
}
@Override
@ -3094,7 +3113,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.target(playerId, message, perms, required);
playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), perms, required);
}
@Override
@ -3102,7 +3121,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.amount(playerId, message, min, max);
playerQueryEventSource.amount(playerId, message + getControllingPlayerHint(playerId), min, max);
}
@Override
@ -3127,7 +3146,7 @@ public abstract class GameImpl implements Game {
if (simulation) {
return;
}
playerQueryEventSource.choosePile(playerId, message, pile1, pile2);
playerQueryEventSource.choosePile(playerId, message + getControllingPlayerHint(playerId), pile1, pile2);
}
@Override
@ -3635,7 +3654,7 @@ public abstract class GameImpl implements Game {
/**
* Reset objects stored for Last Known Information. (Happens if all effects
* are applied und stack is empty)
* are applied and stack is empty)
*/
@Override
public void resetLKI() {

View file

@ -663,26 +663,47 @@ public class Combat implements Serializable, Copyable<Combat> {
if (defender == null) {
continue;
}
boolean choose = true;
if (blockController == null) {
controller = defender;
} else {
controller = blockController;
}
while (choose) {
// choosing until good block configuration
while (true) {
// declare normal blockers
// TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
controller.selectBlockers(source, game, defenderId);
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
return;
}
if (!game.getCombat().checkBlockRestrictions(defender, game)) {
if (controller.isHuman()) { // only human player can decide to do the block in another way
continue;
}
// check multiple restrictions by permanents and effects, reset on invalid blocking configuration, try to auto-fix
// TODO: wtf, some checks contains AI related code inside -- it must be reworked and moved to computer classes?!
// check 1 of 3
boolean isValidBlock = game.getCombat().checkBlockRestrictions(defender, game);
if (!isValidBlock) {
makeSureItsNotComputer(controller);
continue;
}
choose = !game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
if (!choose) {
choose = !game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
// check 2 of 3
isValidBlock = game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
if (!isValidBlock) {
makeSureItsNotComputer(controller);
continue;
}
// check 3 of 3
isValidBlock = game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
if (!isValidBlock) {
makeSureItsNotComputer(controller);
continue;
}
// all valid, can finish now
break;
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defenderId, defenderId));
@ -695,6 +716,15 @@ public class Combat implements Serializable, Copyable<Combat> {
TraceUtil.traceCombatIfNeeded(game, game.getCombat());
}
private void makeSureItsNotComputer(Player controller) {
if (controller.isComputer() || !controller.isHuman()) {
// TODO: wtf, AI will freeze forever here in games with attacker/blocker restrictions,
// but it pass in some use cases due random choices. AI must deside blocker configuration
// in one attempt
//throw new IllegalStateException("AI can't find good blocker configuration, report it to github");
}
}
/**
* Add info about attacker blocked by blocker to the game log
*/
@ -852,10 +882,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* attacking creature fulfills both the restriction and the requirement, so
* that's the only option.
*
* @param player
* @param controller
* @param game
* @return
* @return false on invalid block configuration e.g. player must choose new blockers
*/
public boolean checkBlockRequirementsAfter(Player player, Player controller, Game game) {
// Get once a list of all opponents in range
@ -866,7 +893,7 @@ public class Combat implements Serializable, Copyable<Combat> {
Map<UUID, Integer> minNumberOfBlockersMap = new HashMap<>();
Map<UUID, Integer> minPossibleBlockersMap = new HashMap<>();
// check mustBlock requirements of creatures from opponents of attacking player
// FIND attackers and potential blockers for "must be blocked" effects
for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, player.getId(), game)) {
// creature is controlled by an opponent of the attacker
if (opponents.contains(creature.getControllerId())) {
@ -985,7 +1012,7 @@ public class Combat implements Serializable, Copyable<Combat> {
if (toBeBlockedCreature != null) {
CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature);
if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) {
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers());
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers()); // TODO: fail on multiple effects 1 + 2 min blockers?
Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature);
if (toBeBlockedCreaturePermanent != null) {
minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy());
@ -1069,12 +1096,10 @@ public class Combat implements Serializable, Copyable<Combat> {
}
}
}
}
// check if for attacking creatures with mustBeBlockedByAtLeastX requirements are fulfilled
// APPLY potential blockers to attackers with "must be blocked" effects
for (UUID toBeBlockedCreatureId : mustBeBlockedByAtLeastX.keySet()) {
for (CombatGroup combatGroup : game.getCombat().getGroups()) {
if (combatGroup.getAttackers().contains(toBeBlockedCreatureId)) {
@ -1097,6 +1122,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (!requirementFulfilled) {
// creature is not blocked but has possible blockers
if (controller.isHuman()) {
// HUMAN logic - send warning about wrong blocker config and repeat declare
// TODO: replace isHuman by !isComputer for working unit tests
Permanent toBeBlockedCreature = game.getPermanent(toBeBlockedCreatureId);
if (toBeBlockedCreature != null) {
// check if all possible blocker block other creatures they are forced to block
@ -1115,9 +1142,8 @@ public class Combat implements Serializable, Copyable<Combat> {
}
}
}
} else {
// take the first potential blocker from the set to block for the AI
// AI logic - auto-fix wrong blocker config (take the first potential blocker)
for (UUID possibleBlockerId : mustBeBlockedByAtLeastX.get(toBeBlockedCreatureId)) {
String blockRequiredMessage = isCreatureDoingARequiredBlock(
possibleBlockerId, toBeBlockedCreatureId, mustBeBlockedByAtLeastX, game);
@ -1140,8 +1166,8 @@ public class Combat implements Serializable, Copyable<Combat> {
}
}
}
}
// check if creatures are forced to block but do not block at all or block creatures they are not forced to block
StringBuilder sb = new StringBuilder();
for (Map.Entry<UUID, Set<UUID>> entry : creatureMustBlockAttackers.entrySet()) {
@ -1274,10 +1300,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* Checks the canBeBlockedCheckAfter RestrictionEffect Is the block still
* valid after all block decisions are done
*
* @param player
* @param controller
* @param game
* @return
* @return false on invalid block configuration e.g. player must choose new blockers
*/
public boolean checkBlockRestrictionsAfter(Player player, Player controller, Game game) {
// Restrictions applied to blocking creatures
@ -1906,4 +1929,18 @@ public class Combat implements Serializable, Copyable<Combat> {
return new Combat(this);
}
@Override
public String toString() {
List<String> res = new ArrayList<>();
for (int i = 0; i < this.groups.size(); i++) {
res.add(String.format("group %d with %s",
i + 1,
this.groups.get(i)
));
}
return String.format("%d groups%s",
this.groups.size(),
this.groups.size() > 0 ? ": " + String.join("; ", res) : ""
);
}
}

View file

@ -976,4 +976,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) {
return blocker.getLethalDamage(attacker.getId(), game);
}
@Override
public String toString() {
return String.format("%d attackers, %d blockers",
this.getAttackers().size(),
this.getBlockers().size()
);
}
}

View file

@ -0,0 +1,31 @@
package mage.game.command.emblems;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.BoostControlledEffect;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.command.Emblem;
/**
* @author jackd149
*/
public final class KaitoBaneOfNightmaresEmblem extends Emblem {
public KaitoBaneOfNightmaresEmblem() {
super("Emblem Kaito");
FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.NINJA, "Ninjas you control");
this.getAbilities().add(new SimpleStaticAbility(Zone.COMMAND, new BoostControlledEffect(1, 1, Duration.EndOfGame, filter, false)));
}
private KaitoBaneOfNightmaresEmblem(final KaitoBaneOfNightmaresEmblem card) {
super(card);
}
@Override
public KaitoBaneOfNightmaresEmblem copy() {
return new KaitoBaneOfNightmaresEmblem(this);
}
}

View file

@ -0,0 +1,30 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author Cguy7777
*/
public class HorrorEnchantmentCreatureToken extends TokenImpl {
public HorrorEnchantmentCreatureToken() {
super("Horror Token", "2/2 black Horror enchantment creature token");
cardType.add(CardType.ENCHANTMENT);
cardType.add(CardType.CREATURE);
color.setBlack(true);
subtype.add(SubType.HORROR);
power = new MageInt(2);
toughness = new MageInt(2);
}
private HorrorEnchantmentCreatureToken(final HorrorEnchantmentCreatureToken token) {
super(token);
}
@Override
public HorrorEnchantmentCreatureToken copy() {
return new HorrorEnchantmentCreatureToken(this);
}
}

View file

@ -1,5 +1,3 @@
package mage.game.turn;
import mage.constants.TurnPhase;

View file

@ -1,14 +1,12 @@
package mage.game.turn;
import java.util.UUID;
import mage.constants.PhaseStep;
import mage.game.Game;
import mage.game.events.GameEvent.EventType;
import mage.players.Player;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
@ -30,19 +28,31 @@ public class CleanupStep extends Step {
super.beginStep(game, activePlayerId);
Player activePlayer = game.getPlayer(activePlayerId);
game.getState().setPriorityPlayerId(activePlayer.getId());
//20091005 - 514.1
// 514.1
// First, if the active players hand contains more cards than his or her maximum hand size
// (normally seven), he or she discards enough cards to reduce his or her hand size to that number.
// This turn-based action doesnt use the stack.
if (activePlayer.isInGame()) {
activePlayer.discardToMax(game);
}
//20100423 - 514.2
// 514.2
// Second, the following actions happen simultaneously: all damage marked on permanents
// (including phased-out permanents) is removed and all "until end of turn" and "this turn"
// effects end. This turn-based action doesnt use the stack.
game.getBattlefield().endOfTurn(game);
game.getState().removeEotEffects(game);
// 514.3
// Normally, no player receives priority during the cleanup step, so no spells can be cast
// and no abilities can be activated. However, this rule is subject to the following exception: 514.3a
//
// Look at EndPhase code to process 514.3
}
@Override
public void endStep(Game game, UUID activePlayerId) {
Player activePlayer = game.getPlayer(activePlayerId);
activePlayer.setGameUnderYourControl(true);
super.endStep(game, activePlayerId);
}

View file

@ -31,16 +31,21 @@ public class EndPhase extends Phase {
game.getState().increaseStepNum();
game.getTurn().setEndTurnRequested(false); // so triggers trigger again
prePriority(game, activePlayerId);
// 514.3a At this point, the game checks to see if any state-based actions would be performed
// 514.3.
// Normally, no player receives priority during the cleanup step, so no spells can be cast and
// no abilities can be activated. However, this rule is subject to the following exception:
// 514.3a
// At this point, the game checks to see if any state-based actions would be performed
// and/or any triggered abilities are waiting to be put onto the stack (including those that
// trigger "at the beginning of the next cleanup step"). If so, those state-based actions are
// performed, then those triggered abilities are put on the stack, then the active player gets
// priority. Players may cast spells and activate abilities. Once the stack is empty and all players
// pass in succession, another cleanup step begins
if (game.checkStateAndTriggered()) {
// Queues a new cleanup step
game.informPlayers("State-based actions or triggers happened on cleanup step, so players get priority due 514.3a");
// queues a new cleanup step and request new priorities
game.getState().getTurnMods().add(new TurnMod(activePlayerId).withExtraStep(new CleanupStep()));
// resume priority
if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) {
currentStep.priority(game, activePlayerId, false);
if (game.executingRollback()) {

View file

@ -60,6 +60,12 @@ public abstract class Step implements Serializable, Copyable<Step> {
stepPart = StepPart.PRE;
}
/**
* Play priority by all players
*
* @param activePlayerId starting priority player
* @param resuming false to reset passed priority and ask it again
*/
public void priority(Game game, UUID activePlayerId, boolean resuming) {
if (hasPriority) {
stepPart = StepPart.PRIORITY;

View file

@ -107,12 +107,16 @@ public class Turn implements Serializable {
));
return true;
}
logStartOfTurn(game, activePlayer);
checkTurnIsControlledByOtherPlayer(game, activePlayer.getId());
logStartOfTurn(game, activePlayer);
resetCounts();
this.activePlayerId = activePlayer.getId();
resetCounts();
this.currentPhase = null;
// turn control must be called after potential turn skip due 720.1.
checkTurnIsControlledByOtherPlayer(game, activePlayer.getId());
game.getPlayer(activePlayer.getId()).beginTurn(game);
for (Phase phase : phases) {
if (game.isPaused() || game.checkIfGameIsOver()) {
@ -220,6 +224,28 @@ public class Turn implements Serializable {
}
private void checkTurnIsControlledByOtherPlayer(Game game, UUID activePlayerId) {
// 720.1.
// Some cards allow a player to control another player during that players next turn.
// This effect applies to the next turn that the affected player actually takes.
// The affected player is controlled during the entire turn; the effect doesnt end until
// the beginning of the next turn.
//
// 720.1b
// If a turn is skipped, any pending player-controlling effects wait until the player who would be
// affected actually takes a turn.
// remove old under control
game.getPlayers().values().forEach(player -> {
if (player.isInGame() && !player.isGameUnderControl()) {
Player controllingPlayer = game.getPlayer(player.getTurnControlledBy());
if (player != controllingPlayer && controllingPlayer != null) {
game.informPlayers(controllingPlayer.getLogName() + " lost control over " + player.getLogName());
}
player.setGameUnderYourControl(true);
}
});
// add new under control
TurnMod newControllerMod = game.getState().getTurnMods().useNextNewController(activePlayerId);
if (newControllerMod != null && !newControllerMod.getNewControllerId().equals(activePlayerId)) {
// game logs added in child's call (controlPlayersTurn)

View file

@ -62,6 +62,14 @@ public class TurnMods extends ArrayList<TurnMod> implements Serializable, Copyab
}
public TurnMod useNextNewController(UUID playerId) {
// 720.1a
// Multiple player-controlling effects that affect the same player overwrite each other.
// The last one to be created is the one that works.
//
// 720.1b
// If a turn is skipped, any pending player-controlling effects wait until the player
// who would be affected actually takes a turn.
TurnMod lastNewControllerMod = null;
// find last/actual mod

View file

@ -38,9 +38,10 @@ public class ManaPool implements Serializable {
private boolean forcedToPay; // for Word of Command
private final List<ManaPoolItem> poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>();
private boolean manaBecomesBlack = false;
private boolean manaBecomesColorless = false;
// empty mana pool effects
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>(); // keep some colors
private boolean manaBecomesBlack = false; // replace all pool by black
private boolean manaBecomesColorless = false; // replace all pool by colorless
private static final class ConditionalManaInfo {
private final ManaType manaType;
@ -147,10 +148,10 @@ public class ManaPool implements Serializable {
if (ability.getSourceId().equals(mana.getSourceId())
|| !(mana.getSourceObject() instanceof Spell)
|| ((Spell) mana.getSourceObject())
.getAbilities(game)
.stream()
.flatMap(a -> a.getAllEffects().stream())
.anyMatch(ManaEffect.class::isInstance)) {
.getAbilities(game)
.stream()
.flatMap(a -> a.getAllEffects().stream())
.anyMatch(ManaEffect.class::isInstance)) {
continue; // if any of the above cases, not an alt mana payment ability, thus excluded by filter
}
}
@ -253,6 +254,28 @@ public class ManaPool implements Serializable {
manaItems.clear();
}
public boolean canLostManaOnEmpty() {
for (ManaPoolItem item : manaItems) {
for (ManaType manaType : ManaType.values()) {
if (item.get(manaType) == 0) {
continue;
}
if (doNotEmptyManaTypes.contains(manaType)) {
continue;
}
if (manaBecomesBlack) {
continue;
}
if (manaBecomesColorless) {
continue;
}
// found real mana to empty
return true;
}
}
return false;
}
public int emptyPool(Game game) {
int total = 0;
Iterator<ManaPoolItem> it = manaItems.iterator();

View file

@ -46,7 +46,15 @@ import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
* Warning, if you add new choose dialogs then must implement it for:
* - PlayerImpl (only if it use another default dialogs inside)
* - HumanPlayer (support client-server in human games)
* - ComputerPlayer (support AI in computer games)
* - StubPlayer (temp)
* - ComputerPlayerControllableProxy (support control of one player type over another player type)
* - TestPlayer (support unit tests)
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public interface Player extends MageItem, Copyable<Player> {
@ -366,7 +374,7 @@ public interface Player extends MageItem, Copyable<Player> {
boolean isGameUnderControl();
/**
* Returns false in case you don't control the game.
* False in case you don't control the game.
* <p>
* Note: For effects like "You control target player during that player's
* next turn".
@ -1212,8 +1220,6 @@ public interface Player extends MageItem, Copyable<Player> {
*/
boolean addTargets(Ability ability, Game game);
String getHistory();
boolean hasDesignation(DesignationType designationName);
void addDesignation(Designation designation);
@ -1253,4 +1259,8 @@ public interface Player extends MageItem, Copyable<Player> {
* so that's method helps to find real player that used by a game (in most use cases it's a PlayerImpl)
*/
Player getRealPlayer();
default Player prepareControllableProxy(Player playerUnderControl) {
return this;
}
}

View file

@ -7,7 +7,6 @@ import mage.abilities.ActivatedAbility.ActivationStatus;
import mage.abilities.common.PassAbility;
import mage.abilities.common.PlayLandAsCommanderAbility;
import mage.abilities.common.WhileSearchingPlayFromLibraryAbility;
import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility;
import mage.abilities.costs.*;
import mage.abilities.costs.mana.AlternateManaPaymentAbility;
import mage.abilities.costs.mana.ManaCost;
@ -15,7 +14,6 @@ import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.RestrictionUntapNotMoreThanEffect;
import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect;
import mage.abilities.keyword.*;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.abilities.mana.ManaOptions;
@ -609,11 +607,7 @@ public abstract class PlayerImpl implements Player, Serializable {
if (!playerUnderControl.hasLeft() && !playerUnderControl.hasLost()) {
playerUnderControl.setGameUnderYourControl(false);
}
DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility(
new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), playerUnderControl.getLogName()));
ability.setSourceId(getId());
ability.setControllerId(getId());
game.addDelayedTriggeredAbility(ability, null);
// control will reset on start of the turn
}
}
@ -5378,11 +5372,6 @@ public abstract class PlayerImpl implements Player, Serializable {
return true;
}
@Override
public String getHistory() {
return "no available";
}
@Override
public boolean hasDesignation(DesignationType designationName) {
for (Designation designation : designations) {

View file

@ -1,26 +1,11 @@
package mage.target.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.constants.Zone;
import mage.filter.Filter;
import mage.filter.common.FilterCreatureOrPlayer;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetImpl;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
public class TargetCreatureOrPlayer extends TargetImpl {
protected FilterCreatureOrPlayer filter;
public class TargetCreatureOrPlayer extends TargetPermanentOrPlayer {
public TargetCreatureOrPlayer() {
this(1, 1, new FilterCreatureOrPlayer());
@ -31,178 +16,11 @@ public class TargetCreatureOrPlayer extends TargetImpl {
}
public TargetCreatureOrPlayer(int minNumTargets, int maxNumTargets, FilterCreatureOrPlayer filter) {
this.minNumberOfTargets = minNumTargets;
this.maxNumberOfTargets = maxNumTargets;
this.zone = Zone.ALL;
this.filter = filter;
this.targetName = filter.getMessage();
super(minNumTargets, maxNumTargets, filter, false);
}
protected TargetCreatureOrPlayer(final TargetCreatureOrPlayer target) {
super(target);
this.filter = target.filter.copy();
}
@Override
public Filter getFilter() {
return this.filter;
}
@Override
public boolean canTarget(UUID id, Game game) {
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
return filter.match(permanent, game);
}
Player player = game.getPlayer(id);
return filter.match(player, game);
}
@Override
public boolean canTarget(UUID id, Ability source, Game game) {
return canTarget(source.getControllerId(), id, source, game);
}
@Override
public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) {
Permanent permanent = game.getPermanent(id);
Player player = game.getPlayer(id);
if (source != null) {
MageObject targetSource = game.getObject(source);
if (permanent != null) {
return permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game) && filter.match(permanent, source.getControllerId(), source, game);
}
if (player != null) {
return player.canBeTargetedBy(targetSource, source.getControllerId(), source, game) && filter.match(player, game);
}
}
if (permanent != null) {
return filter.match(permanent, game);
}
return filter.match(player, game);
}
/**
* Checks if there are enough {@link Permanent} or {@link Player} that can
* be chosen. Should only be used for Ability targets since this checks for
* protection, shroud etc.
*
* @param sourceControllerId - controller of the target event source
* @param source
* @param game
* @return - true if enough valid {@link Permanent} or {@link Player} exist
*/
@Override
public boolean canChoose(UUID sourceControllerId, Ability source, Game game) {
int count = 0;
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null && player.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
/**
* Checks if there are enough {@link Permanent} or {@link Player} that can
* be selected. Should not be used for Ability targets since this does not
* check for protection, shroud etc.
*
* @param sourceControllerId - controller of the select event
* @param game
* @return - true if enough valid {@link Permanent} or {@link Player} exist
*/
@Override
public boolean canChoose(UUID sourceControllerId, Game game) {
int count = 0;
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
if (filter.match(permanent, sourceControllerId, null, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
Set<UUID> possibleTargets = new HashSet<>();
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& player.canBeTargetedBy(targetSource, sourceControllerId, source, game)
&& filter.getPlayerFilter().match(player, sourceControllerId, source, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)
&& filter.getCreatureFilter().match(permanent, sourceControllerId, source, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
Set<UUID> possibleTargets = new HashSet<>();
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null && filter.getPlayerFilter().match(player, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
if (filter.getCreatureFilter().match(permanent, sourceControllerId, null, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder();
for (UUID targetId : getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
sb.append(permanent.getLogName()).append(' ');
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
sb.append(player.getLogName()).append(' ');
}
}
}
return sb.toString().trim();
}
@Override
@ -210,7 +28,4 @@ public class TargetCreatureOrPlayer extends TargetImpl {
return new TargetCreatureOrPlayer(this);
}
public FilterCreaturePermanent getFilterCreature() {
return filter.getCreatureFilter().copy();
}
}

View file

@ -1380,7 +1380,7 @@ public final class CardUtil {
public static void takeControlUnderPlayerEnd(Game game, Ability source, Player controller, Player playerUnderControl) {
playerUnderControl.setGameUnderYourControl(true, false);
if (!playerUnderControl.getTurnControlledBy().equals(controller.getId())) {
game.informPlayers(controller + " return control of the turn to " + playerUnderControl.getLogName() + CardUtil.getSourceLogName(game, source));
game.informPlayers(controller.getLogName() + " return control of the turn to " + playerUnderControl.getLogName() + CardUtil.getSourceLogName(game, source));
controller.getPlayersUnderYourControl().remove(playerUnderControl.getId());
}
}
@ -2127,9 +2127,10 @@ public final class CardUtil {
return null;
}
// not started game
// T0 - for not started game
// T2 - for starting of the turn
if (gameState.getTurn().getStep() == null) {
return "T0";
return "T" + gameState.getTurnNum();
}
// normal game

View file

@ -160,10 +160,6 @@ public final class GameLog {
return "<font color='" + LOG_COLOR_PLAYER_CONFIRM + "'>" + name + "</font>";
}
public static String getSmallSecondLineText(String text) {
return "<div style='font-size:11pt'>" + text + "</div>";
}
private static String getColorName(ObjectColor objectColor) {
if (objectColor.isMulticolored()) {
return LOG_COLOR_MULTI;

View file

@ -59,6 +59,19 @@ public class PlayerLostLifeWatcher extends Watcher {
return amount;
}
public int getNumberOfOpponentsWhoLostLife(UUID playerId, Game game) {
int numPlayersLostLife = 0;
for (UUID opponentId : this.amountOfLifeLostThisTurn.keySet()) {
Player opponent = game.getPlayer(opponentId);
if (opponent != null && opponent.hasOpponent(playerId, game)) {
if (this.amountOfLifeLostThisTurn.getOrDefault(opponentId, 0) > 0) {
numPlayersLostLife++;
}
}
}
return numPlayersLostLife;
}
public int getLifeLostLastTurn(UUID playerId) {
return amountOfLifeLostLastTurn.getOrDefault(playerId, 0);
}

View file

@ -136,6 +136,7 @@
|Generate|EMBLEM:M3C|Emblem Vivien|||VivienReidEmblem|
|Generate|EMBLEM:ACR|Emblem Capitoline Triad|||TheCapitolineTriadEmblem|
|Generate|EMBLEM:BLB|Emblem Ral|||RalCracklingWitEmblem|
|Generate|EMBLEM:DSK|Emblem Kaito|||KaitoBaneOfNightmaresEmblem|
|Generate|EMBLEM:FDN|Emblem Kaito|||KaitoCunningInfiltratorEmblem|
|Generate|EMBLEM:FDN|Emblem Vivien|||VivienReidEmblem|