diff --git a/Mage.Client/release/cacerts b/Mage.Client/release/cacerts deleted file mode 100644 index f5011566613..00000000000 Binary files a/Mage.Client/release/cacerts and /dev/null differ diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index 8dcf3d6406f..2a23612e18f 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -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(); diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 7ee3f1ebccb..56d4e666592 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -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 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 diff --git a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java index f4103cd8554..102de5751ac 100644 --- a/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java +++ b/Mage.Client/src/main/java/mage/client/plugins/adapters/MageActionCallback.java @@ -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 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 diff --git a/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java index 2c9e7443be8..fb4c0aeb44a 100644 --- a/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java +++ b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java @@ -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; diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java index 12d1a127c1a..f549c6c9c2a 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/TextboxRuleParser.java @@ -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; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index 0b091bd16fb..573b54094e2 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -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"); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java index a6622c16c62..1bb365f6b96 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java @@ -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 } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java index 3ded259a466..1598840389a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java @@ -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) diff --git a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java index 68034c47597..a26df5a73ec 100644 --- a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java +++ b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java @@ -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); + } } diff --git a/Mage.Common/src/main/java/mage/utils/SystemUtil.java b/Mage.Common/src/main/java/mage/utils/SystemUtil.java index 9d5f3f3593b..4f71a78bca2 100644 --- a/Mage.Common/src/main/java/mage/utils/SystemUtil.java +++ b/Mage.Common/src/main/java/mage/utils/SystemUtil.java @@ -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; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayerControllableProxy.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayerControllableProxy.java new file mode 100644 index 00000000000..dd63da4bdf0 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayerControllableProxy.java @@ -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). + *

+ * 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 effectsMap, Map 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 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 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 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 blockers, CombatGroup combatGroup, java.util.List 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 getMultiAmountWithIndividualConstraints( + Outcome outcome, + List 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 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 pile1, java.util.List 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(); + } +} diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index bb1fd669b17..002a3f9d493 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -311,37 +311,6 @@ public class ComputerPlayer extends PlayerImpl { } } - if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) { - List 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 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 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 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 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 targets; TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget()); diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index de9f95c640b..705f2479182 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -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 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; + } } diff --git a/Mage.Server/config/config.xml b/Mage.Server/config/config.xml index 2aa43d6eec7..7c5e6401d7c 100644 --- a/Mage.Server/config/config.xml +++ b/Mage.Server/config/config.xml @@ -68,7 +68,7 @@ /> - + diff --git a/Mage.Server/release/config-example/config.xml b/Mage.Server/release/config-example/config.xml index 30900c0e8cc..e112b43486b 100644 --- a/Mage.Server/release/config-example/config.xml +++ b/Mage.Server/release/config-example/config.xml @@ -64,7 +64,7 @@ /> - + diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index 2a768f4ebd9..2ffbf7e00b6 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -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 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: diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index 790d85d31db..db7fa74d173 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -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); diff --git a/Mage.Server/src/test/data/config_error.xml b/Mage.Server/src/test/data/config_error.xml index 517fc0f9328..fd5bffb2f95 100644 --- a/Mage.Server/src/test/data/config_error.xml +++ b/Mage.Server/src/test/data/config_error.xml @@ -32,7 +32,7 @@ /> - + diff --git a/Mage.Sets/src/mage/cards/b/BelloBardOfTheBrambles.java b/Mage.Sets/src/mage/cards/b/BelloBardOfTheBrambles.java index a8c4d0dff67..913f3190d0b 100644 --- a/Mage.Sets/src/mage/cards/b/BelloBardOfTheBrambles.java +++ b/Mage.Sets/src/mage/cards/b/BelloBardOfTheBrambles.java @@ -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 } diff --git a/Mage.Sets/src/mage/cards/c/CaitCageBrawler.java b/Mage.Sets/src/mage/cards/c/CaitCageBrawler.java new file mode 100644 index 00000000000..b92d83818f9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaitCageBrawler.java @@ -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; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/c/CryptidInspector.java b/Mage.Sets/src/mage/cards/c/CryptidInspector.java new file mode 100644 index 00000000000..2481532b2ff --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CryptidInspector.java @@ -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); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/c/CurieEmergentIntelligence.java b/Mage.Sets/src/mage/cards/c/CurieEmergentIntelligence.java new file mode 100644 index 00000000000..b61a5940a87 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurieEmergentIntelligence.java @@ -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; + } +} diff --git a/Mage.Sets/src/mage/cards/k/KaitoBaneOfNightmares.java b/Mage.Sets/src/mage/cards/k/KaitoBaneOfNightmares.java new file mode 100644 index 00000000000..f947283d7f0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KaitoBaneOfNightmares.java @@ -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."; + } +} diff --git a/Mage.Sets/src/mage/cards/m/MangarasTome.java b/Mage.Sets/src/mage/cards/m/MangarasTome.java index 2e5e294b122..8c6cedb7cff 100644 --- a/Mage.Sets/src/mage/cards/m/MangarasTome.java +++ b/Mage.Sets/src/mage/cards/m/MangarasTome.java @@ -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()); } } diff --git a/Mage.Sets/src/mage/cards/n/NowhereToRun.java b/Mage.Sets/src/mage/cards/n/NowhereToRun.java new file mode 100644 index 00000000000..fd774a8f04b --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NowhereToRun.java @@ -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); + } +} diff --git a/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java new file mode 100644 index 00000000000..366fe7c8fa9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java @@ -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 — 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 — 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); + } +} diff --git a/Mage.Sets/src/mage/cards/s/StarDestroyer.java b/Mage.Sets/src/mage/cards/s/StarDestroyer.java index 274c770005a..02bfcf1b8a1 100644 --- a/Mage.Sets/src/mage/cards/s/StarDestroyer.java +++ b/Mage.Sets/src/mage/cards/s/StarDestroyer.java @@ -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) { diff --git a/Mage.Sets/src/mage/cards/t/ThermalDetonator.java b/Mage.Sets/src/mage/cards/t/ThermalDetonator.java index fb259b601d0..c10c5f6c530 100644 --- a/Mage.Sets/src/mage/cards/t/ThermalDetonator.java +++ b/Mage.Sets/src/mage/cards/t/ThermalDetonator.java @@ -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) { diff --git a/Mage.Sets/src/mage/cards/u/UrzasAvenger.java b/Mage.Sets/src/mage/cards/u/UrzasAvenger.java index 29c1dba39f9..d45e9f10516 100644 --- a/Mage.Sets/src/mage/cards/u/UrzasAvenger.java +++ b/Mage.Sets/src/mage/cards/u/UrzasAvenger.java @@ -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()) diff --git a/Mage.Sets/src/mage/cards/v/VampireGourmand.java b/Mage.Sets/src/mage/cards/v/VampireGourmand.java index 35643d351ca..7aff3649dbe 100644 --- a/Mage.Sets/src/mage/cards/v/VampireGourmand.java +++ b/Mage.Sets/src/mage/cards/v/VampireGourmand.java @@ -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) { diff --git a/Mage.Sets/src/mage/sets/AlaraReborn.java b/Mage.Sets/src/mage/sets/AlaraReborn.java index c4fcf0254f4..2bae33b9a98 100644 --- a/Mage.Sets/src/mage/sets/AlaraReborn.java +++ b/Mage.Sets/src/mage/sets/AlaraReborn.java @@ -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 makeBooster() { + List 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; + } + } diff --git a/Mage.Sets/src/mage/sets/Conflux.java b/Mage.Sets/src/mage/sets/Conflux.java index 82a42d710f9..6b18fe618ef 100644 --- a/Mage.Sets/src/mage/sets/Conflux.java +++ b/Mage.Sets/src/mage/sets/Conflux.java @@ -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 makeBooster() { + List 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; + } } diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java index 28e8282694e..069107adced 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java @@ -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)); diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java index e03862fc9ad..5ad516da099 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java @@ -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)); diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index e3b4309e94e..e9b9fd78221 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -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 diff --git a/Mage.Sets/src/mage/sets/FoundationsJumpstart.java b/Mage.Sets/src/mage/sets/FoundationsJumpstart.java index 4a2096c2fdf..297584d4418 100644 --- a/Mage.Sets/src/mage/sets/FoundationsJumpstart.java +++ b/Mage.Sets/src/mage/sets/FoundationsJumpstart.java @@ -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)); diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java index 465b9049d62..053f1aae252 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineTheAftermath.java @@ -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)); diff --git a/Mage.Sets/src/mage/sets/UnknownEvent.java b/Mage.Sets/src/mage/sets/UnknownEvent.java index da1fd1f58c7..776949d124a 100644 --- a/Mage.Sets/src/mage/sets/UnknownEvent.java +++ b/Mage.Sets/src/mage/sets/UnknownEvent.java @@ -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)); } } diff --git a/Mage.Tests/config/config.xml b/Mage.Tests/config/config.xml index b6d386674a2..568825bb274 100644 --- a/Mage.Tests/config/config.xml +++ b/Mage.Tests/config/config.xml @@ -3,7 +3,7 @@ - + diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java index b294eb738b3..a27502ad4f6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java @@ -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(); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java index ef83f53f4ab..624a26a3341 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java @@ -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); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/NowhereToRunTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/NowhereToRunTest.java new file mode 100644 index 00000000000..abef16e9738 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/NowhereToRunTest.java @@ -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"); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/BelloBardOfTheBramblesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/BelloBardOfTheBramblesTest.java new file mode 100644 index 00000000000..1e3c2b49bfd --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/BelloBardOfTheBramblesTest.java @@ -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); + } + + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java index c6ae191e7d8..e60aa31d41c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/combat/AttackBlockRestrictionsTest.java @@ -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 creature’s + // 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 creature’s + // 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 creature’s + // 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 creature’s + // 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 that’s 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 creature’s + // 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 that’s 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); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java index f37ff6f5aac..e69ee664e21 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java @@ -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; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java index e2da4f62218..9319ab03b16 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java @@ -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 finishes = new LinkedHashMap<>(); + private final Map 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"); + } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ff6d8181f3d..b2270995726 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -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 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 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 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 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); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index f13f18f145f..c78ea5c0fd7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -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. diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java index 4f72de91e3c..1a7d63ed24d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/cheats/LoadCheatsTest.java @@ -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 } } diff --git a/Mage/src/main/java/mage/abilities/PlayLandAbility.java b/Mage/src/main/java/mage/abilities/PlayLandAbility.java index 0288f7d415f..4425d6431e6 100644 --- a/Mage/src/main/java/mage/abilities/PlayLandAbility.java +++ b/Mage/src/main/java/mage/abilities/PlayLandAbility.java @@ -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); } - } diff --git a/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java deleted file mode 100644 index d0078125fa3..00000000000 --- a/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java +++ /dev/null @@ -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; - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsAllEffect.java index d96ac4736c6..0637319572b 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsAllEffect.java @@ -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; } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsControllerEffect.java index e82387dbea1..5c5135b365a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsControllerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayAdditionalLandsControllerEffect.java @@ -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; } diff --git a/Mage/src/main/java/mage/abilities/hint/common/CanPlayAdditionalLandsHint.java b/Mage/src/main/java/mage/abilities/hint/common/CanPlayAdditionalLandsHint.java new file mode 100644 index 00000000000..4fb318ff52f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/common/CanPlayAdditionalLandsHint.java @@ -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; + } +} diff --git a/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java b/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java index 3fd1003ca8b..665d4fd97f1 100644 --- a/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java +++ b/Mage/src/main/java/mage/filter/common/FilterCreatureOrPlayer.java @@ -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 implements FilterInPlay { - - 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 diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index c6d8c2b0aa0..0dd53c256e2 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -550,6 +550,12 @@ public interface Game extends MageItem, Serializable, Copyable { @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); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 0f82f0ae2e7..1bece076bd7 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -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 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() { diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 7137f686c7b..38c300969ca 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -663,26 +663,47 @@ public class Combat implements Serializable, Copyable { 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 { 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 { * 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 { Map minNumberOfBlockersMap = new HashMap<>(); Map 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 { 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 { } } - } - } - // 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 { 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 { } } } - } 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 { } } } - } + // 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> entry : creatureMustBlockAttackers.entrySet()) { @@ -1274,10 +1300,7 @@ public class Combat implements Serializable, Copyable { * 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 { return new Combat(this); } + @Override + public String toString() { + List 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) : "" + ); + } } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 9106b2d4abc..c5b5a9cb677 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -976,4 +976,12 @@ public class CombatGroup implements Serializable, Copyable { 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() + ); + } } diff --git a/Mage/src/main/java/mage/game/command/emblems/KaitoBaneOfNightmaresEmblem.java b/Mage/src/main/java/mage/game/command/emblems/KaitoBaneOfNightmaresEmblem.java new file mode 100644 index 00000000000..ac6fa47d0c4 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/KaitoBaneOfNightmaresEmblem.java @@ -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); + } + +} diff --git a/Mage/src/main/java/mage/game/permanent/token/HorrorEnchantmentCreatureToken.java b/Mage/src/main/java/mage/game/permanent/token/HorrorEnchantmentCreatureToken.java new file mode 100644 index 00000000000..eb3af5f787c --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/HorrorEnchantmentCreatureToken.java @@ -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); + } +} diff --git a/Mage/src/main/java/mage/game/turn/BeginningPhase.java b/Mage/src/main/java/mage/game/turn/BeginningPhase.java index c62795ccc74..5a7da443928 100644 --- a/Mage/src/main/java/mage/game/turn/BeginningPhase.java +++ b/Mage/src/main/java/mage/game/turn/BeginningPhase.java @@ -1,5 +1,3 @@ - - package mage.game.turn; import mage.constants.TurnPhase; diff --git a/Mage/src/main/java/mage/game/turn/CleanupStep.java b/Mage/src/main/java/mage/game/turn/CleanupStep.java index 5bfe000c562..be102a7816c 100644 --- a/Mage/src/main/java/mage/game/turn/CleanupStep.java +++ b/Mage/src/main/java/mage/game/turn/CleanupStep.java @@ -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 player’s 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 doesn’t 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 doesn’t 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); } diff --git a/Mage/src/main/java/mage/game/turn/EndPhase.java b/Mage/src/main/java/mage/game/turn/EndPhase.java index 03e69c4e8bc..d9dc0431cb5 100644 --- a/Mage/src/main/java/mage/game/turn/EndPhase.java +++ b/Mage/src/main/java/mage/game/turn/EndPhase.java @@ -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()) { diff --git a/Mage/src/main/java/mage/game/turn/Step.java b/Mage/src/main/java/mage/game/turn/Step.java index 24e6e7debf6..56c18402355 100644 --- a/Mage/src/main/java/mage/game/turn/Step.java +++ b/Mage/src/main/java/mage/game/turn/Step.java @@ -60,6 +60,12 @@ public abstract class Step implements Serializable, Copyable { 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; diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 4c1d76fac3e..f163c2e7e9c 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -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 player’s 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 doesn’t 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) diff --git a/Mage/src/main/java/mage/game/turn/TurnMods.java b/Mage/src/main/java/mage/game/turn/TurnMods.java index c127110ced2..6019143fe81 100644 --- a/Mage/src/main/java/mage/game/turn/TurnMods.java +++ b/Mage/src/main/java/mage/game/turn/TurnMods.java @@ -62,6 +62,14 @@ public class TurnMods extends ArrayList 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 diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index dd8114a604b..82bca7edcef 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -38,9 +38,10 @@ public class ManaPool implements Serializable { private boolean forcedToPay; // for Word of Command private final List poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes - private final Set doNotEmptyManaTypes = new HashSet<>(); - private boolean manaBecomesBlack = false; - private boolean manaBecomesColorless = false; + // empty mana pool effects + private final Set 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 it = manaItems.iterator(); diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 257f6443dc7..df84549735d 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -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 { @@ -366,7 +374,7 @@ public interface Player extends MageItem, Copyable { boolean isGameUnderControl(); /** - * Returns false in case you don't control the game. + * False in case you don't control the game. *

* Note: For effects like "You control target player during that player's * next turn". @@ -1212,8 +1220,6 @@ public interface Player extends MageItem, Copyable { */ 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 { * 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; + } } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 4492b47d95a..4eaea5fff07 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -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) { diff --git a/Mage/src/main/java/mage/target/common/TargetCreatureOrPlayer.java b/Mage/src/main/java/mage/target/common/TargetCreatureOrPlayer.java index feb8640e659..838e9a95a41 100644 --- a/Mage/src/main/java/mage/target/common/TargetCreatureOrPlayer.java +++ b/Mage/src/main/java/mage/target/common/TargetCreatureOrPlayer.java @@ -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 possibleTargets(UUID sourceControllerId, Ability source, Game game) { - Set 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 possibleTargets(UUID sourceControllerId, Game game) { - Set 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(); - } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 609861d1f91..3f85a6fa591 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -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 diff --git a/Mage/src/main/java/mage/util/GameLog.java b/Mage/src/main/java/mage/util/GameLog.java index 4e12e9fe55d..b2a6bae3b27 100644 --- a/Mage/src/main/java/mage/util/GameLog.java +++ b/Mage/src/main/java/mage/util/GameLog.java @@ -160,10 +160,6 @@ public final class GameLog { return "" + name + ""; } - public static String getSmallSecondLineText(String text) { - return "

" + text + "
"; - } - private static String getColorName(ObjectColor objectColor) { if (objectColor.isMulticolored()) { return LOG_COLOR_MULTI; diff --git a/Mage/src/main/java/mage/watchers/common/PlayerLostLifeWatcher.java b/Mage/src/main/java/mage/watchers/common/PlayerLostLifeWatcher.java index 89c03aeda2a..ad1551295d7 100644 --- a/Mage/src/main/java/mage/watchers/common/PlayerLostLifeWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/PlayerLostLifeWatcher.java @@ -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); } diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt index 643a4eefd12..a842d1c5600 100644 --- a/Mage/src/main/resources/tokens-database.txt +++ b/Mage/src/main/resources/tokens-database.txt @@ -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|