forked from External/mage
External-master #12
76 changed files with 2850 additions and 674 deletions
Binary file not shown.
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ import mage.constants.*;
|
|||
import mage.game.events.PlayerQueryEvent;
|
||||
import mage.players.PlayableObjectStats;
|
||||
import mage.players.PlayableObjectsList;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.DebugUtil;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.util.StreamUtils;
|
||||
import mage.view.*;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
|
||||
|
|
@ -53,6 +55,7 @@ import java.util.*;
|
|||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static mage.client.dialog.PreferencesDialog.*;
|
||||
import static mage.constants.PlayerAction.*;
|
||||
|
|
@ -1810,6 +1813,7 @@ public final class GamePanel extends javax.swing.JPanel {
|
|||
|
||||
// hand
|
||||
if (needZone == Zone.HAND || needZone == Zone.ALL) {
|
||||
// my hand
|
||||
for (CardView card : lastGameData.game.getMyHand().values()) {
|
||||
if (needSelectable.contains(card.getId())) {
|
||||
card.setChoosable(true);
|
||||
|
|
@ -1821,6 +1825,34 @@ public final class GamePanel extends javax.swing.JPanel {
|
|||
card.setPlayableStats(needPlayable.getStats(card.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
// opponent hands (switching by GUI's button with my hand)
|
||||
List<SimpleCardView> list = lastGameData.game.getOpponentHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList());
|
||||
for (SimpleCardView card : list) {
|
||||
if (needSelectable.contains(card.getId())) {
|
||||
card.setChoosable(true);
|
||||
}
|
||||
if (needChosen.contains(card.getId())) {
|
||||
card.setSelected(true);
|
||||
}
|
||||
if (needPlayable.containsObject(card.getId())) {
|
||||
card.setPlayableStats(needPlayable.getStats(card.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
// watched hands (switching by GUI's button with my hand)
|
||||
list = lastGameData.game.getWatchedHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList());
|
||||
for (SimpleCardView card : list) {
|
||||
if (needSelectable.contains(card.getId())) {
|
||||
card.setChoosable(true);
|
||||
}
|
||||
if (needChosen.contains(card.getId())) {
|
||||
card.setSelected(true);
|
||||
}
|
||||
if (needPlayable.containsObject(card.getId())) {
|
||||
card.setPlayableStats(needPlayable.getStats(card.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stack
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ public class MageActionCallback implements ActionCallback {
|
|||
private MageCard prevCardPanel;
|
||||
private boolean startedDragging;
|
||||
private boolean isDragging; // TODO: remove drag hand code to the hand panels
|
||||
private Point initialCardPos;
|
||||
private Point initialMousePos;
|
||||
private Point initialCardPos = null;
|
||||
private Point initialMousePos = null;
|
||||
private final Set<MageCard> draggingCards = new HashSet<>();
|
||||
|
||||
public MageActionCallback() {
|
||||
|
|
@ -351,6 +351,11 @@ public class MageActionCallback implements ActionCallback {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.initialMousePos == null || this.initialCardPos == null) {
|
||||
// only allow really mouse pressed, e.g. ignore draft/game update on active card draging/pressing
|
||||
return;
|
||||
}
|
||||
|
||||
Point mouse = new Point(e.getX(), e.getY());
|
||||
SwingUtilities.convertPointToScreen(mouse, data.getComponent());
|
||||
if (!isDragging
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,401 @@
|
|||
package mage.player.ai;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.*;
|
||||
import mage.abilities.costs.VariableCost;
|
||||
import mage.abilities.costs.mana.ManaCost;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.decks.Deck;
|
||||
import mage.choices.Choice;
|
||||
import mage.constants.ManaType;
|
||||
import mage.constants.MultiAmountType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.RangeOfInfluence;
|
||||
import mage.game.Game;
|
||||
import mage.game.combat.CombatGroup;
|
||||
import mage.game.draft.Draft;
|
||||
import mage.game.match.Match;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.tournament.Tournament;
|
||||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import mage.target.TargetAmount;
|
||||
import mage.target.TargetCard;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI player that can be taken under control by another player (AI or human).
|
||||
* <p>
|
||||
* Under control logic on choose dialog (under human):
|
||||
* - create fake human player and assign it to real human data transfer object (for income answers);
|
||||
* - call choose dialog from fake human (e.g. send choose data to real player);
|
||||
* - game will process all sending and answering logic as "human under human" logic;
|
||||
* - return choose dialog result without AI code processing;
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class ComputerPlayerControllableProxy extends ComputerPlayer7 {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ComputerPlayerControllableProxy.class);
|
||||
|
||||
Player lastControllingPlayer = null;
|
||||
|
||||
public ComputerPlayerControllableProxy(String name, RangeOfInfluence range, int skill) {
|
||||
super(name, range, skill);
|
||||
}
|
||||
|
||||
public ComputerPlayerControllableProxy(final ComputerPlayerControllableProxy player) {
|
||||
super(player);
|
||||
this.lastControllingPlayer = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerPlayerControllableProxy copy() {
|
||||
return new ComputerPlayerControllableProxy(this);
|
||||
}
|
||||
|
||||
private boolean isUnderMe(Game game) {
|
||||
return game.isSimulation() || this.isGameUnderControl();
|
||||
}
|
||||
|
||||
private Player getControllingPlayer(Game game) {
|
||||
Player player = game.getPlayer(this.getTurnControlledBy());
|
||||
this.lastControllingPlayer = player.prepareControllableProxy(this);
|
||||
return this.lastControllingPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResponseString(String responseString) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.setResponseString(responseString);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.setResponseManaType(manaTypePlayerId, responseManaType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResponseUUID(UUID responseUUID) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.setResponseUUID(responseUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResponseBoolean(Boolean responseBoolean) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.setResponseBoolean(responseBoolean);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResponseInteger(Integer responseInteger) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.setResponseInteger(responseInteger);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signalPlayerCheat() {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.signalPlayerCheat();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signalPlayerConcede(boolean stopCurrentChooseDialog) {
|
||||
if (this.lastControllingPlayer != null) {
|
||||
this.lastControllingPlayer.signalPlayerConcede(stopCurrentChooseDialog);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean priority(Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.priority(game);
|
||||
} else {
|
||||
Player player = getControllingPlayer(game);
|
||||
try {
|
||||
return player.priority(game);
|
||||
} finally {
|
||||
this.passed = player.isPassed(); // TODO: wtf, no needs?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseMulligan(Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseMulligan(game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseMulligan(game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseUse(outcome, message, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseUse(outcome, message, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int chooseReplacementEffect(Map<String, String> effectsMap, Map<String, MageObject> objectsMap, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseReplacementEffect(effectsMap, objectsMap, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseReplacementEffect(effectsMap, objectsMap, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean choose(Outcome outcome, Choice choice, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.choose(outcome, choice, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).choose(outcome, choice, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean choose(Outcome outcome, Target target, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.choose(outcome, target, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).choose(outcome, target, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.choose(outcome, target, source, game, options);
|
||||
} else {
|
||||
return getControllingPlayer(game).choose(outcome, target, source, game, options);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseTarget(outcome, target, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseTarget(outcome, target, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.choose(outcome, cards, target, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).choose(outcome, cards, target, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseTarget(outcome, cards, target, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseTarget(outcome, cards, target, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseTargetAmount(outcome, target, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseTargetAmount(outcome, target, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TriggeredAbility chooseTriggeredAbility(java.util.List<TriggeredAbility> abilities, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseTriggeredAbility(abilities, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseTriggeredAbility(abilities, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean playMana(Ability abilityToCast, ManaCost unpaid, String promptText, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.playMana(abilityToCast, unpaid, promptText, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).playMana(abilityToCast, unpaid, promptText, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.announceXMana(min, max, message, game, ability);
|
||||
} else {
|
||||
return getControllingPlayer(game).announceXMana(min, max, message, game, ability);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.announceXCost(min, max, message, game, ability, variableCost);
|
||||
} else {
|
||||
return getControllingPlayer(game).announceXCost(min, max, message, game, ability, variableCost);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectAttackers(Game game, UUID attackingPlayerId) {
|
||||
if (isUnderMe(game)) {
|
||||
super.selectAttackers(game, attackingPlayerId);
|
||||
} else {
|
||||
getControllingPlayer(game).selectAttackers(game, attackingPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
|
||||
if (isUnderMe(game)) {
|
||||
super.selectBlockers(source, game, defendingPlayerId);
|
||||
} else {
|
||||
getControllingPlayer(game).selectBlockers(source, game, defendingPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseAttackerOrder(attackers, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseAttackerOrder(attackers, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAmount(int min, int max, String message, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.getAmount(min, max, message, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).getAmount(min, max, message, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getMultiAmountWithIndividualConstraints(
|
||||
Outcome outcome,
|
||||
List<MultiAmountMessage> messages,
|
||||
int totalMin,
|
||||
int totalMax,
|
||||
MultiAmountType type,
|
||||
Game game
|
||||
) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sideboard(Match match, Deck deck) {
|
||||
super.sideboard(match, deck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void construct(Tournament tournament, Deck deck) {
|
||||
super.construct(tournament, deck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pickCard(java.util.List<Card> cards, Deck deck, Draft draft) {
|
||||
super.pickCard(cards, deck, draft);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean activateAbility(ActivatedAbility ability, Game game) {
|
||||
// TODO: need research, see HumanPlayer's code
|
||||
return super.activateAbility(ability, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseAbilityForCast(card, game, noMana);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseAbilityForCast(card, game, noMana);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivatedAbility chooseLandOrSpellAbility(Card card, Game game, boolean noMana) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseLandOrSpellAbility(card, game, noMana);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseLandOrSpellAbility(card, game, noMana);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode chooseMode(Modes modes, Ability source, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.chooseMode(modes, source, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).chooseMode(modes, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean choosePile(Outcome outcome, String message, java.util.List<? extends Card> pile1, java.util.List<? extends Card> pile2, Game game) {
|
||||
if (isUnderMe(game)) {
|
||||
return super.choosePile(outcome, message, pile1, pile2, game);
|
||||
} else {
|
||||
return getControllingPlayer(game).choosePile(outcome, message, pile1, pile2, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abort() {
|
||||
// TODO: need research, is it require real player call? Concede/leave/timeout works by default
|
||||
super.abort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip() {
|
||||
// TODO: see abort comments above
|
||||
super.skip();
|
||||
}
|
||||
}
|
||||
|
|
@ -311,37 +311,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
}
|
||||
|
||||
if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
|
||||
List<Permanent> targets;
|
||||
TargetCreatureOrPlayer origTarget = (TargetCreatureOrPlayer) target.getOriginalTarget();
|
||||
if (outcome.isGood()) {
|
||||
targets = threats(abilityControllerId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
|
||||
} else {
|
||||
targets = threats(randomOpponentId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
|
||||
}
|
||||
for (Permanent permanent : targets) {
|
||||
List<UUID> alreadyTargeted = target.getTargets();
|
||||
if (target.canTarget(abilityControllerId, permanent.getId(), null, game)) {
|
||||
if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
|
||||
target.add(permanent.getId(), game);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (outcome.isGood()) {
|
||||
if (target.canTarget(abilityControllerId, abilityControllerId, null, game)) {
|
||||
target.add(abilityControllerId, game);
|
||||
return true;
|
||||
}
|
||||
} else if (target.canTarget(abilityControllerId, randomOpponentId, null, game)) {
|
||||
target.add(randomOpponentId, game);
|
||||
return true;
|
||||
}
|
||||
if (!required) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
|
||||
List<Permanent> targets;
|
||||
TargetPermanentOrPlayer origTarget = (TargetPermanentOrPlayer) target.getOriginalTarget();
|
||||
|
|
@ -752,48 +721,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
return target.isChosen(game);
|
||||
}
|
||||
|
||||
if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
|
||||
List<Permanent> targets;
|
||||
TargetCreatureOrPlayer origTarget = ((TargetCreatureOrPlayer) target.getOriginalTarget());
|
||||
if (outcome.isGood()) {
|
||||
targets = threats(abilityControllerId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
|
||||
} else {
|
||||
targets = threats(randomOpponentId, source, ((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), game, target.getTargets());
|
||||
}
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
if (outcome.isGood()) {
|
||||
if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) {
|
||||
return tryAddTarget(target, abilityControllerId, source, game);
|
||||
}
|
||||
} else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) {
|
||||
return tryAddTarget(target, randomOpponentId, source, game);
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.isEmpty() && target.isRequired(source)) {
|
||||
targets = game.getBattlefield().getActivePermanents(((FilterCreatureOrPlayer) origTarget.getFilter()).getCreatureFilter(), playerId, game);
|
||||
}
|
||||
for (Permanent permanent : targets) {
|
||||
List<UUID> alreadyTargeted = target.getTargets();
|
||||
if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
|
||||
return tryAddTarget(target, permanent.getId(), source, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outcome.isGood()) {
|
||||
if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) {
|
||||
return tryAddTarget(target, abilityControllerId, source, game);
|
||||
}
|
||||
} else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) {
|
||||
return tryAddTarget(target, randomOpponentId, source, game);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.getOriginalTarget() instanceof TargetAnyTarget) {
|
||||
List<Permanent> targets;
|
||||
TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget());
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import mage.game.tournament.Tournament;
|
|||
import mage.players.Player;
|
||||
import mage.players.PlayerImpl;
|
||||
import mage.players.PlayerList;
|
||||
import mage.players.net.UserData;
|
||||
import mage.target.Target;
|
||||
import mage.target.TargetAmount;
|
||||
import mage.target.TargetCard;
|
||||
|
|
@ -93,10 +94,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
// * - GAME thread: open response for income command and wait (go to sleep by response.wait)
|
||||
// * - CALL thread: on closed response - waiting open status of player's response object (if it's too long then cancel the answer)
|
||||
// * - CALL thread: on opened response - save answer to player's response object and notify GAME thread about it by response.notifyAll
|
||||
// * - GAME thread: on nofify from response - check new answer value and process it (if it bad then repeat and wait the next one);
|
||||
// * - GAME thread: on notify from response - check new answer value and process it (if it bad then repeat and wait the next one);
|
||||
private transient Boolean responseOpenedForAnswer = false; // GAME thread waiting new answer
|
||||
private transient long responseLastWaitingThreadId = 0;
|
||||
private final transient PlayerResponse response = new PlayerResponse();
|
||||
private final transient PlayerResponse response; // data receiver from a client side (must be shared for one player between multiple clients)
|
||||
private final int RESPONSE_WAITING_TIME_SECS = 30; // waiting time before cancel current response
|
||||
private final int RESPONSE_WAITING_CHECK_MS = 100; // timeout for open status check
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
protected static FilterCreatureForCombat filterCreatureForCombat = new FilterCreatureForCombat();
|
||||
protected static FilterAttackingCreature filterAttack = new FilterAttackingCreature();
|
||||
protected static FilterBlockingCreature filterBlock = new FilterBlockingCreature();
|
||||
protected final Choice replacementEffectChoice;
|
||||
protected Choice replacementEffectChoice = null;
|
||||
private static final Logger logger = Logger.getLogger(HumanPlayer.class);
|
||||
|
||||
protected HashSet<String> autoSelectReplacementEffects = new LinkedHashSet<>(); // must be sorted
|
||||
|
|
@ -130,8 +131,12 @@ public class HumanPlayer extends PlayerImpl {
|
|||
|
||||
public HumanPlayer(String name, RangeOfInfluence range, int skill) {
|
||||
super(name, range);
|
||||
human = true;
|
||||
this.human = true;
|
||||
this.response = new PlayerResponse();
|
||||
initReplacementDialog();
|
||||
}
|
||||
|
||||
private void initReplacementDialog() {
|
||||
replacementEffectChoice = new ChoiceImpl(true);
|
||||
replacementEffectChoice.setMessage("Choose replacement effect to resolve first");
|
||||
replacementEffectChoice.setSpecial(
|
||||
|
|
@ -142,8 +147,20 @@ public class HumanPlayer extends PlayerImpl {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make fake player from any other
|
||||
*/
|
||||
public HumanPlayer(final PlayerImpl sourcePlayer, final PlayerResponse sourceResponse) {
|
||||
super(sourcePlayer);
|
||||
this.human = true;
|
||||
this.response = sourceResponse; // need for sync and wait user's response from a network
|
||||
initReplacementDialog();
|
||||
}
|
||||
|
||||
public HumanPlayer(final HumanPlayer player) {
|
||||
super(player);
|
||||
this.response = player.response;
|
||||
|
||||
this.replacementEffectChoice = player.replacementEffectChoice;
|
||||
this.autoSelectReplacementEffects.addAll(player.autoSelectReplacementEffects);
|
||||
this.currentlyUnpaidMana = player.currentlyUnpaidMana;
|
||||
|
|
@ -1159,25 +1176,27 @@ public class HumanPlayer extends PlayerImpl {
|
|||
// TODO: change pass and other states like passedUntilStackResolved for controlling player, not for "this"
|
||||
// TODO: check and change all "this" to controling player calls, many bugs with hand, mana, skips - https://github.com/magefree/mage/issues/2088
|
||||
// TODO: use controlling player in all choose dialogs (and canRespond too, what's with take control of player AI?!)
|
||||
UserData controllingUserData = this.userData;
|
||||
if (canRespond()) {
|
||||
HumanPlayer controllingPlayer = this;
|
||||
if (isGameUnderControl()) { // TODO: must be ! to get real controlling player
|
||||
if (!isGameUnderControl()) {
|
||||
Player player = game.getPlayer(getTurnControlledBy());
|
||||
if (player instanceof HumanPlayer) {
|
||||
controllingPlayer = (HumanPlayer) player;
|
||||
controllingUserData = player.getUserData();
|
||||
} else {
|
||||
// TODO: add computer opponent here?!
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check that all skips and stops used from real controlling player
|
||||
// like holdingPriority (is it a bug here?)
|
||||
if (getJustActivatedType() != null && !holdingPriority) {
|
||||
if (controllingPlayer.getUserData().isPassPriorityCast()
|
||||
if (controllingUserData.isPassPriorityCast()
|
||||
&& getJustActivatedType() == AbilityType.SPELL) {
|
||||
setJustActivatedType(null);
|
||||
pass(game);
|
||||
return false;
|
||||
}
|
||||
if (controllingPlayer.getUserData().isPassPriorityActivation()
|
||||
if (controllingUserData.isPassPriorityActivation()
|
||||
&& getJustActivatedType().isNonManaActivatedAbility()) {
|
||||
setJustActivatedType(null);
|
||||
pass(game);
|
||||
|
|
@ -1252,7 +1271,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
// it's main step
|
||||
if (!skippedAtLeastOnce
|
||||
|| (!playerId.equals(game.getActivePlayerId())
|
||||
&& !controllingPlayer.getUserData().getUserSkipPrioritySteps().isStopOnAllMainPhases())) {
|
||||
&& !controllingUserData.getUserSkipPrioritySteps().isStopOnAllMainPhases())) {
|
||||
skippedAtLeastOnce = true;
|
||||
if (passWithManaPoolCheck(game)) {
|
||||
return false;
|
||||
|
|
@ -1274,8 +1293,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
// it's end of turn step
|
||||
if (!skippedAtLeastOnce
|
||||
|| (playerId.equals(game.getActivePlayerId())
|
||||
&& !controllingPlayer
|
||||
.getUserData()
|
||||
&& !controllingUserData
|
||||
.getUserSkipPrioritySteps()
|
||||
.isStopOnAllEndPhases())) {
|
||||
skippedAtLeastOnce = true;
|
||||
|
|
@ -1295,7 +1313,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
if (!dontCheckPassStep
|
||||
&& checkPassStep(game, controllingPlayer)) {
|
||||
&& checkPassStep(game, controllingUserData)) {
|
||||
if (passWithManaPoolCheck(game)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1308,8 +1326,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
if (passedUntilStackResolved) {
|
||||
if (haveNewObjectsOnStack
|
||||
&& (playerId.equals(game.getActivePlayerId())
|
||||
&& controllingPlayer
|
||||
.getUserData()
|
||||
&& controllingUserData
|
||||
.getUserSkipPrioritySteps()
|
||||
.isStopOnStackNewObjects())) {
|
||||
// new objects on stack -- disable "pass until stack resolved"
|
||||
|
|
@ -1433,17 +1450,17 @@ public class HumanPlayer extends PlayerImpl {
|
|||
return response.getUUID();
|
||||
}
|
||||
|
||||
private boolean checkPassStep(Game game, HumanPlayer controllingPlayer) {
|
||||
private boolean checkPassStep(Game game, UserData controllingUserData) {
|
||||
try {
|
||||
|
||||
if (playerId.equals(game.getActivePlayerId())) {
|
||||
return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType());
|
||||
return !controllingUserData.getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType());
|
||||
} else {
|
||||
return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType());
|
||||
return !controllingUserData.getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType());
|
||||
}
|
||||
} catch (NullPointerException ex) {
|
||||
if (controllingPlayer.getUserData() != null) {
|
||||
if (controllingPlayer.getUserData().getUserSkipPrioritySteps() != null) {
|
||||
if (controllingUserData != null) {
|
||||
if (controllingUserData.getUserSkipPrioritySteps() != null) {
|
||||
if (game.getStep() != null) {
|
||||
if (game.getTurnStepType() == null) {
|
||||
logger.error("game.getTurnStepType() == null");
|
||||
|
|
@ -2928,20 +2945,9 @@ public class HumanPlayer extends PlayerImpl {
|
|||
|
||||
protected boolean passWithManaPoolCheck(Game game) {
|
||||
if (userData.confirmEmptyManaPool()
|
||||
&& game.getStack().isEmpty() && getManaPool().count() > 0) {
|
||||
String activePlayerText;
|
||||
if (game.isActivePlayer(playerId)) {
|
||||
activePlayerText = "Your turn";
|
||||
} else {
|
||||
activePlayerText = game.getPlayer(game.getActivePlayerId()).getName() + "'s turn";
|
||||
}
|
||||
String priorityPlayerText = "";
|
||||
if (!isGameUnderControl()) {
|
||||
priorityPlayerText = " / priority " + game.getPlayer(game.getPriorityPlayerId()).getName();
|
||||
}
|
||||
// TODO: chooseUse and other dialogs must be under controlling player
|
||||
if (!chooseUse(Outcome.Detriment, GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool. Pass regardless?")
|
||||
+ GameLog.getSmallSecondLineText(activePlayerText + " / " + game.getTurnStepType().toString() + priorityPlayerText), null, game)) {
|
||||
&& game.getStack().isEmpty() && getManaPool().count() > 0 && getManaPool().canLostManaOnEmpty()) {
|
||||
String message = GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lose. Pass anyway?");
|
||||
if (!chooseUse(Outcome.Detriment, message, null, game)) {
|
||||
sendPlayerAction(PlayerAction.PASS_PRIORITY_CANCEL_ALL_ACTIONS, game, null);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2950,11 +2956,6 @@ public class HumanPlayer extends PlayerImpl {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHistory() {
|
||||
return "no available";
|
||||
}
|
||||
|
||||
private boolean gameInCheckPlayableState(Game game) {
|
||||
return gameInCheckPlayableState(game, false);
|
||||
}
|
||||
|
|
@ -2972,4 +2973,14 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Player prepareControllableProxy(Player playerUnderControl) {
|
||||
// make fake player, e.g. transform computer player to human player for choose dialogs under control
|
||||
HumanPlayer fakePlayer = new HumanPlayer((PlayerImpl) playerUnderControl, this.response);
|
||||
if (!fakePlayer.getTurnControlledBy().equals(this.getId())) {
|
||||
throw new IllegalArgumentException("Wrong code usage: controllable proxy must be controlled by " + this.getName());
|
||||
}
|
||||
return fakePlayer;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
/>
|
||||
<playerTypes>
|
||||
<playerType name="Human" jar="mage-player-human.jar" className="mage.player.human.HumanPlayer"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
|
||||
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
|
||||
<playerType name="Computer - draftbot" jar="mage-player-ai-draft-bot.jar" className="mage.player.ai.ComputerDraftPlayer"/>
|
||||
</playerTypes>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
/>
|
||||
<playerTypes>
|
||||
<playerType name="Human" jar="mage-player-human-${project.version}.jar" className="mage.player.human.HumanPlayer"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma-${project.version}.jar" className="mage.player.ai.ComputerPlayer7"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma-${project.version}.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
|
||||
<playerType name="Computer - draftbot" jar="mage-player-ai-draftbot-${project.version}.jar" className="mage.player.ai.ComputerDraftPlayer"/>
|
||||
</playerTypes>
|
||||
<gameTypes>
|
||||
|
|
|
|||
|
|
@ -786,23 +786,23 @@ public class GameController implements GameCallback {
|
|||
}
|
||||
|
||||
public void sendPlayerUUID(UUID userId, final UUID data) {
|
||||
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerUUID(data));
|
||||
sendMessage(userId, playerId -> sendDirectPlayerUUID(playerId, data));
|
||||
}
|
||||
|
||||
public void sendPlayerString(UUID userId, final String data) {
|
||||
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerString(data));
|
||||
sendMessage(userId, playerId -> sendDirectPlayerString(playerId, data));
|
||||
}
|
||||
|
||||
public void sendPlayerManaType(UUID userId, final UUID manaTypePlayerId, final ManaType data) {
|
||||
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerManaType(data, manaTypePlayerId));
|
||||
sendMessage(userId, playerId -> sendDirectPlayerManaType(playerId, manaTypePlayerId, data));
|
||||
}
|
||||
|
||||
public void sendPlayerBoolean(UUID userId, final Boolean data) {
|
||||
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerBoolean(data));
|
||||
sendMessage(userId, playerId -> sendDirectPlayerBoolean(playerId, data));
|
||||
}
|
||||
|
||||
public void sendPlayerInteger(UUID userId, final Integer data) {
|
||||
sendMessage(userId, playerId -> getGameSession(playerId).sendPlayerInteger(data));
|
||||
sendMessage(userId, playerId -> sendDirectPlayerInteger(playerId, data));
|
||||
}
|
||||
|
||||
private void updatePriorityTimers() {
|
||||
|
|
@ -906,14 +906,14 @@ public class GameController implements GameCallback {
|
|||
perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options));
|
||||
}
|
||||
|
||||
private void informOthers(UUID playerId) {
|
||||
private void informOthers(UUID waitingPlayerId) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
if (game.getStep() != null) {
|
||||
message.append(game.getTurnStepType().toString()).append(" - ");
|
||||
}
|
||||
message.append("Waiting for ").append(game.getPlayer(playerId).getLogName());
|
||||
message.append("Waiting for ").append(game.getPlayer(waitingPlayerId).getLogName());
|
||||
for (final Entry<UUID, GameSessionPlayer> entry : getGameSessionsMap().entrySet()) {
|
||||
if (!entry.getKey().equals(playerId)) {
|
||||
if (!entry.getKey().equals(waitingPlayerId)) {
|
||||
entry.getValue().inform(message.toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -1030,7 +1030,7 @@ public class GameController implements GameCallback {
|
|||
// TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case
|
||||
// same for another player (can be fixed by super-duper connection)
|
||||
if (informOthers) {
|
||||
informOthers(playerId);
|
||||
informOthers(realPlayerController.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1055,7 +1055,8 @@ public class GameController implements GameCallback {
|
|||
} else {
|
||||
// otherwise execute the action under other player's control
|
||||
for (UUID controlled : player.getPlayersUnderYourControl()) {
|
||||
if (gameSessions.containsKey(controlled) && game.getPriorityPlayerId().equals(controlled)) {
|
||||
Player controlledPlayer = game.getPlayer(controlled);
|
||||
if ((gameSessions.containsKey(controlled) || controlledPlayer.isComputer()) && game.getPriorityPlayerId().equals(controlled)) {
|
||||
stopResponseIdleTimeout();
|
||||
command.execute(controlled);
|
||||
}
|
||||
|
|
@ -1098,7 +1099,6 @@ public class GameController implements GameCallback {
|
|||
|
||||
@FunctionalInterface
|
||||
interface Command {
|
||||
|
||||
void execute(UUID player);
|
||||
}
|
||||
|
||||
|
|
@ -1138,6 +1138,81 @@ public class GameController implements GameCallback {
|
|||
return newGameSessionWatchers;
|
||||
}
|
||||
|
||||
private void sendDirectPlayerUUID(UUID playerId, UUID data) {
|
||||
// real player
|
||||
GameSessionPlayer session = getGameSession(playerId);
|
||||
if (session != null) {
|
||||
session.sendPlayerUUID(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// computer under control
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.isComputer()) {
|
||||
player.setResponseUUID(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectPlayerString(UUID playerId, String data) {
|
||||
// real player
|
||||
GameSessionPlayer session = getGameSession(playerId);
|
||||
if (session != null) {
|
||||
session.sendPlayerString(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// computer under control
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.isComputer()) {
|
||||
player.setResponseString(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectPlayerManaType(UUID playerId, UUID manaTypePlayerId, ManaType manaType) {
|
||||
// real player
|
||||
GameSessionPlayer session = getGameSession(playerId);
|
||||
if (session != null) {
|
||||
session.sendPlayerManaType(manaTypePlayerId, manaType);
|
||||
return;
|
||||
}
|
||||
|
||||
// computer under control
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.isComputer()) {
|
||||
player.setResponseManaType(manaTypePlayerId, manaType);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectPlayerBoolean(UUID playerId, Boolean data) {
|
||||
// real player
|
||||
GameSessionPlayer session = getGameSession(playerId);
|
||||
if (session != null) {
|
||||
session.sendPlayerBoolean(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// computer under control
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.isComputer()) {
|
||||
player.setResponseBoolean(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectPlayerInteger(UUID playerId, Integer data) {
|
||||
// real player
|
||||
GameSessionPlayer session = getGameSession(playerId);
|
||||
if (session != null) {
|
||||
session.sendPlayerInteger(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// computer under control
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.isComputer()) {
|
||||
player.setResponseInteger(data);
|
||||
}
|
||||
}
|
||||
|
||||
private GameSessionPlayer getGameSession(UUID playerId) {
|
||||
// TODO: check parent callers - there are possible problems with sync, can be related to broken "fix" logs too
|
||||
// It modify players data, but:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
/>
|
||||
<playerTypes>
|
||||
<playerType name="Human" jar="mage-player-human.jar" className="mage.player.human.HumanPlayer"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
|
||||
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
|
||||
<playerType name="Computer - draftbot" jar="mage-player-ai-draft-bot.jar" className="mage.player.ai.ComputerDraftPlayer"/>
|
||||
</playerTypes>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
123
Mage.Sets/src/mage/cards/c/CaitCageBrawler.java
Normal file
123
Mage.Sets/src/mage/cards/c/CaitCageBrawler.java
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package mage.cards.c;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.AttacksTriggeredAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.condition.common.MyTurnCondition;
|
||||
import mage.abilities.decorator.ConditionalContinuousEffect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
|
||||
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
|
||||
import mage.abilities.keyword.IndestructibleAbility;
|
||||
import mage.cards.*;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetCard;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Grath
|
||||
*/
|
||||
public final class CaitCageBrawler extends CardImpl {
|
||||
|
||||
public CaitCageBrawler(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{G}");
|
||||
|
||||
this.supertype.add(SuperType.LEGENDARY);
|
||||
this.subtype.add(SubType.HUMAN);
|
||||
this.subtype.add(SubType.WARRIOR);
|
||||
this.power = new MageInt(1);
|
||||
this.toughness = new MageInt(1);
|
||||
|
||||
// During your turn, Cait, Cage Brawler has indestructible.
|
||||
this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(
|
||||
new GainAbilitySourceEffect(IndestructibleAbility.getInstance()),
|
||||
MyTurnCondition.instance, "during your turn, {this} has indestructible"
|
||||
)));
|
||||
|
||||
// Whenever Cait attacks, you and defending player each draw a card, then discard a card. Put two +1/+1 counters on Cait if you discarded the card with the highest mana value among those cards or tied for highest.
|
||||
this.addAbility(new AttacksTriggeredAbility(new CaitCageBrawlerEffect(), false, null, SetTargetPointer.PLAYER));
|
||||
}
|
||||
|
||||
private CaitCageBrawler(final CaitCageBrawler card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaitCageBrawler copy() {
|
||||
return new CaitCageBrawler(this);
|
||||
}
|
||||
}
|
||||
|
||||
class CaitCageBrawlerEffect extends OneShotEffect {
|
||||
|
||||
public CaitCageBrawlerEffect() {
|
||||
super(Outcome.Benefit);
|
||||
this.staticText = "you and defending player each draw a card, then discard a card. Put two +1/+1 counters on " +
|
||||
"{this} if you discarded the card with the highest mana value among those cards or tied for highest.";
|
||||
}
|
||||
|
||||
protected CaitCageBrawlerEffect(final CaitCageBrawlerEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaitCageBrawlerEffect copy() {
|
||||
return new CaitCageBrawlerEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
MageObject sourceObject = source.getSourceObject(game);
|
||||
if (controller == null
|
||||
|| sourceObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller.drawCards(1, source, game);
|
||||
|
||||
Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source));
|
||||
if (opponent != null) {
|
||||
opponent.drawCards(1, source, game);
|
||||
}
|
||||
int mvController = Integer.MIN_VALUE;
|
||||
Card cardController = null;
|
||||
int mvOpponent = Integer.MIN_VALUE;
|
||||
Card cardOpponent = null;
|
||||
|
||||
TargetCard controllerTarget = new TargetCard(Zone.HAND, new FilterCard());
|
||||
if (controller.choose(Outcome.Discard, controller.getHand(), controllerTarget, source, game)) {
|
||||
Card card = controller.getHand().get(controllerTarget.getFirstTarget(), game);
|
||||
if (card != null) {
|
||||
cardController = card;
|
||||
mvController = card.getManaValue();
|
||||
}
|
||||
}
|
||||
TargetCard opponentTarget = new TargetCard(Zone.HAND, new FilterCard());
|
||||
if (opponent != null && opponent.choose(Outcome.Discard, opponent.getHand(), opponentTarget, source, game)) {
|
||||
Card card = opponent.getHand().get(opponentTarget.getFirstTarget(), game);
|
||||
if (card != null) {
|
||||
cardOpponent = card;
|
||||
mvOpponent = card.getManaValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (cardOpponent != null) {
|
||||
opponent.discard(cardOpponent, false, source, game);
|
||||
}
|
||||
if (cardController != null) {
|
||||
controller.discard(cardController, false, source, game);
|
||||
if (mvController > mvOpponent) {
|
||||
new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)).apply(game, source);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
61
Mage.Sets/src/mage/cards/c/CryptidInspector.java
Normal file
61
Mage.Sets/src/mage/cards/c/CryptidInspector.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package mage.cards.c;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility;
|
||||
import mage.abilities.common.TurnedFaceUpAllTriggeredAbility;
|
||||
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
|
||||
import mage.abilities.keyword.VigilanceAbility;
|
||||
import mage.abilities.meta.OrTriggeredAbility;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.predicate.card.FaceDownPredicate;
|
||||
|
||||
/**
|
||||
* @author jackd149
|
||||
*/
|
||||
public final class CryptidInspector extends CardImpl {
|
||||
private static final FilterPermanent filter1 = new FilterPermanent("a face-down permanent");
|
||||
private static final FilterPermanent filter2 = new FilterControlledPermanent("Cryptid Inspector or another permanent you control");
|
||||
|
||||
static {
|
||||
filter1.add(FaceDownPredicate.instance);
|
||||
}
|
||||
|
||||
public CryptidInspector(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}");
|
||||
this.power = new MageInt(2);
|
||||
this.toughness = new MageInt(3);
|
||||
this.subtype.add(SubType.ELF);
|
||||
this.subtype.add(SubType.WARRIOR);
|
||||
this.addAbility(VigilanceAbility.getInstance());
|
||||
|
||||
// Whenever a face-down permanent you control enters and whenever Cryptid Inspector or another permanent you control is turned face up,
|
||||
// put a +1/+1 counter on Cryptid Inspector.
|
||||
this.addAbility(new OrTriggeredAbility(
|
||||
Zone.BATTLEFIELD,
|
||||
new AddCountersSourceEffect(CounterType.P1P1.createInstance()),
|
||||
false,
|
||||
"Whenever a face-down permanent you control enters and "
|
||||
+ "whenever Cryptid Inspector or another permanent you control is turned face up, ",
|
||||
new EntersBattlefieldControlledTriggeredAbility(null, filter1),
|
||||
new TurnedFaceUpAllTriggeredAbility(null, filter2)
|
||||
));
|
||||
}
|
||||
|
||||
private CryptidInspector(final CryptidInspector card){
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptidInspector copy() {
|
||||
return new CryptidInspector(this);
|
||||
}
|
||||
}
|
||||
161
Mage.Sets/src/mage/cards/c/CurieEmergentIntelligence.java
Normal file
161
Mage.Sets/src/mage/cards/c/CurieEmergentIntelligence.java
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package mage.cards.c;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
|
||||
import mage.abilities.common.SimpleActivatedAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.common.ExileTargetCost;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.decorator.ConditionalOneShotEffect;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.SuperType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.predicate.mageobject.AnotherPredicate;
|
||||
import mage.filter.predicate.permanent.TokenPredicate;
|
||||
import mage.filter.predicate.Predicates;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.PermanentCard;
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.util.functions.CopyApplier;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jam1garner
|
||||
*/
|
||||
public final class CurieEmergentIntelligence extends CardImpl {
|
||||
|
||||
private static final FilterControlledPermanent filter =
|
||||
new FilterControlledPermanent("another nontoken artifact creature you control");
|
||||
|
||||
static {
|
||||
filter.add(AnotherPredicate.instance);
|
||||
filter.add(Predicates.and(
|
||||
CardType.ARTIFACT.getPredicate(),
|
||||
CardType.CREATURE.getPredicate()
|
||||
));
|
||||
filter.add(TokenPredicate.FALSE);
|
||||
}
|
||||
|
||||
public CurieEmergentIntelligence(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{U}");
|
||||
|
||||
this.supertype.add(SuperType.LEGENDARY);
|
||||
this.subtype.add(SubType.ROBOT);
|
||||
this.power = new MageInt(1);
|
||||
this.toughness = new MageInt(3);
|
||||
|
||||
// Whenever Curie, Emergent Intelligence deals combat damage to a player, draw cards equal to its base power.
|
||||
this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(
|
||||
new DrawCardSourceControllerEffect(CurieEmergentIntelligenceValue.NON_NEGATIVE).setText("draw cards equal to its base power"), false
|
||||
));
|
||||
|
||||
// {1}{U}, Exile another nontoken artifact creature you control: Curie becomes a copy of the exiled creature, except it has
|
||||
// "Whenever this creature deals combat damage to a player, draw cards equal to its base power."
|
||||
Ability ability = new SimpleActivatedAbility(new CurieEmergentIntelligenceCopyEffect(), new ManaCostsImpl<>("{1}{U}"));
|
||||
ability.addCost(new ExileTargetCost(new TargetControlledPermanent(filter)));
|
||||
this.addAbility(ability);
|
||||
}
|
||||
|
||||
private CurieEmergentIntelligence(final CurieEmergentIntelligence card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurieEmergentIntelligence copy() {
|
||||
return new CurieEmergentIntelligence(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum CurieEmergentIntelligenceValue implements DynamicValue {
|
||||
NON_NEGATIVE;
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
Permanent sourcePermanent = sourceAbility.getSourcePermanentOrLKI(game);
|
||||
if (sourcePermanent == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Minimum of 0 needed to account for Spinal Parasite
|
||||
return Math.max(0, sourcePermanent.getPower().getModifiedBaseValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurieEmergentIntelligenceValue copy() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "X";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "{this}'s power";
|
||||
}
|
||||
}
|
||||
|
||||
class CurieEmergentIntelligenceCopyEffect extends OneShotEffect {
|
||||
|
||||
private static final CopyApplier applier = new CopyApplier() {
|
||||
@Override
|
||||
public boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId) {
|
||||
blueprint.getAbilities().add(new DealsCombatDamageToAPlayerTriggeredAbility(
|
||||
new DrawCardSourceControllerEffect(CurieEmergentIntelligenceValue.NON_NEGATIVE).setText("draw cards equal to its base power"), false
|
||||
));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
CurieEmergentIntelligenceCopyEffect() {
|
||||
super(Outcome.Benefit);
|
||||
this.setText("{this} becomes a copy of the exiled creature, except it has \"Whenever this creature deals combat damage to a player, draw cards equal to its base power.\"");
|
||||
}
|
||||
|
||||
private CurieEmergentIntelligenceCopyEffect(final CurieEmergentIntelligenceCopyEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurieEmergentIntelligenceCopyEffect copy() {
|
||||
return new CurieEmergentIntelligenceCopyEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
for (Cost c : source.getCosts()) {
|
||||
if (c.isPaid() && c instanceof ExileTargetCost) {
|
||||
for (Permanent exiled : ((ExileTargetCost) c).getPermanents()) {
|
||||
if (exiled != null) {
|
||||
game.copyPermanent(
|
||||
Duration.WhileOnBattlefield,
|
||||
exiled,
|
||||
source.getSourceId(), source, applier
|
||||
);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
132
Mage.Sets/src/mage/cards/k/KaitoBaneOfNightmares.java
Normal file
132
Mage.Sets/src/mage/cards/k/KaitoBaneOfNightmares.java
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package mage.cards.k;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.LoyaltyAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.condition.common.MyTurnCondition;
|
||||
import mage.abilities.decorator.ConditionalContinuousEffect;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
|
||||
import mage.abilities.effects.common.GetEmblemEffect;
|
||||
import mage.abilities.effects.common.TapTargetEffect;
|
||||
import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect;
|
||||
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
|
||||
import mage.abilities.effects.keyword.SurveilEffect;
|
||||
import mage.abilities.hint.common.MyTurnHint;
|
||||
import mage.abilities.keyword.HexproofAbility;
|
||||
import mage.abilities.keyword.NinjutsuAbility;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.command.emblems.KaitoBaneOfNightmaresEmblem;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.token.custom.CreatureToken;
|
||||
import mage.target.common.TargetCreaturePermanent;
|
||||
import mage.watchers.common.PlayerLostLifeWatcher;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jackd149
|
||||
*/
|
||||
public final class KaitoBaneOfNightmares extends CardImpl {
|
||||
|
||||
public KaitoBaneOfNightmares(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{U}{B}");
|
||||
|
||||
this.supertype.add(SuperType.LEGENDARY);
|
||||
this.subtype.add(SubType.KAITO);
|
||||
this.setStartingLoyalty(4);
|
||||
|
||||
// Ninjutsu {1}{U}{B}
|
||||
this.addAbility(new NinjutsuAbility("{1}{U}{B}"));
|
||||
|
||||
// During your turn, as long as Kaito has one or more loyalty counters on him, he's a 3/4 Ninja creature and has hexproof.
|
||||
this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect(
|
||||
new BecomesCreatureSourceEffect(
|
||||
new CreatureToken(3, 4, "3/4 Ninja creature")
|
||||
.withSubType(SubType.NINJA)
|
||||
.withAbility(HexproofAbility.getInstance()), null, Duration.WhileOnBattlefield
|
||||
), KaitoBaneOfNightmaresCondition.instance, "During your turn, as long as {this} has one or more loyalty counters on him, " +
|
||||
"he's a 3/4 Ninja creature and has hexproof."
|
||||
)).addHint(MyTurnHint.instance));
|
||||
|
||||
// +1: You get an emblem with "Ninjas you control get +1/+1."
|
||||
this.addAbility(new LoyaltyAbility(new GetEmblemEffect(new KaitoBaneOfNightmaresEmblem()), 1));
|
||||
|
||||
// 0: Surveil 2. Then draw a card for each opponent who lost life this turn.
|
||||
Ability ability = new LoyaltyAbility(new SurveilEffect(2), 0);
|
||||
ability.addEffect(new DrawCardSourceControllerEffect(KaitoBaneOfNightmaresCount.instance));
|
||||
this.addAbility(ability, new PlayerLostLifeWatcher());
|
||||
|
||||
// -2: Tap target creature. Put two stun counters on it.
|
||||
Ability minusTwoAbility = new LoyaltyAbility(new TapTargetEffect(), -2);
|
||||
minusTwoAbility.addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance(2))
|
||||
.setText("Put two stun counters on it"));
|
||||
minusTwoAbility.addTarget(new TargetCreaturePermanent());
|
||||
this.addAbility(minusTwoAbility);
|
||||
}
|
||||
|
||||
private KaitoBaneOfNightmares(final KaitoBaneOfNightmares card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KaitoBaneOfNightmares copy() {
|
||||
return new KaitoBaneOfNightmares(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum KaitoBaneOfNightmaresCondition implements Condition {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
if (!MyTurnCondition.instance.apply(game, source)){
|
||||
return false;
|
||||
}
|
||||
|
||||
Permanent permanent = game.getPermanent(source.getSourceId());
|
||||
|
||||
if (permanent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int loyaltyCount = permanent.getCounters(game).getCount(CounterType.LOYALTY);
|
||||
return loyaltyCount > 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum KaitoBaneOfNightmaresCount implements DynamicValue {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
PlayerLostLifeWatcher watcher = game.getState().getWatcher(PlayerLostLifeWatcher.class);
|
||||
if (watcher != null) {
|
||||
return watcher.getNumberOfOpponentsWhoLostLife(sourceAbility.getControllerId(), game);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KaitoBaneOfNightmaresCount copy() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "opponent who lost life this turn.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
128
Mage.Sets/src/mage/cards/n/NowhereToRun.java
Normal file
128
Mage.Sets/src/mage/cards/n/NowhereToRun.java
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package mage.cards.n;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.effects.AsThoughEffect;
|
||||
import mage.abilities.effects.AsThoughEffectImpl;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
|
||||
import mage.abilities.effects.common.continuous.BoostTargetEffect;
|
||||
import mage.abilities.keyword.FlashAbility;
|
||||
import mage.abilities.keyword.WardAbility;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.target.common.TargetOpponentsCreaturePermanent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author markort147
|
||||
*/
|
||||
public final class NowhereToRun extends CardImpl {
|
||||
|
||||
public NowhereToRun(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}");
|
||||
|
||||
// Flash
|
||||
this.addAbility(FlashAbility.getInstance());
|
||||
|
||||
// When Nowhere to Run enters, target creature an opponent controls gets -3/-3 until end of turn.
|
||||
Ability etbAbility = new EntersBattlefieldTriggeredAbility(new BoostTargetEffect(-3, -3, Duration.EndOfTurn));
|
||||
etbAbility.addTarget(new TargetOpponentsCreaturePermanent());
|
||||
this.addAbility(etbAbility);
|
||||
|
||||
// Creatures your opponents control can be the targets of spells and abilities as though they didn't have hexproof. Ward abilities of those creatures don't trigger.
|
||||
Ability staticAbility = new SimpleStaticAbility(new NowhereToRunHexproofEffect());
|
||||
staticAbility.addEffect(new NowhereToRunWardEffect());
|
||||
this.addAbility(staticAbility);
|
||||
}
|
||||
|
||||
private NowhereToRun(final NowhereToRun card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NowhereToRun copy() {
|
||||
return new NowhereToRun(this);
|
||||
}
|
||||
}
|
||||
|
||||
class NowhereToRunHexproofEffect extends AsThoughEffectImpl {
|
||||
|
||||
NowhereToRunHexproofEffect() {
|
||||
super(AsThoughEffectType.HEXPROOF, Duration.WhileOnBattlefield, Outcome.Benefit);
|
||||
staticText = "Creatures your opponents control "
|
||||
+ "can be the targets of spells and "
|
||||
+ "abilities as though they didn't "
|
||||
+ "have hexproof.";
|
||||
}
|
||||
|
||||
private NowhereToRunHexproofEffect(final NowhereToRunHexproofEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
|
||||
if (affectedControllerId.equals(source.getControllerId())) {
|
||||
Permanent creature = game.getPermanent(sourceId);
|
||||
return creature != null
|
||||
&& creature.isCreature(game)
|
||||
&& game.getOpponents(source.getControllerId()).contains(creature.getControllerId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsThoughEffect copy() {
|
||||
return new NowhereToRunHexproofEffect(this);
|
||||
}
|
||||
}
|
||||
|
||||
class NowhereToRunWardEffect extends ContinuousRuleModifyingEffectImpl {
|
||||
|
||||
|
||||
NowhereToRunWardEffect() {
|
||||
super(Duration.WhileOnBattlefield, Outcome.Benefit);
|
||||
staticText = "Ward abilities of those creatures don't trigger.";
|
||||
}
|
||||
|
||||
private NowhereToRunWardEffect(final NowhereToRunWardEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checksEventType(GameEvent event, Game game) {
|
||||
return event.getType().equals(GameEvent.EventType.NUMBER_OF_TRIGGERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
Permanent permanent = game.getPermanent(event.getSourceId());
|
||||
if (permanent == null || !permanent.isCreature(game)) {
|
||||
return false;
|
||||
}
|
||||
if (!game.getOpponents(source.getControllerId()).contains(permanent.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getValue("targetAbility") instanceof WardAbility;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContinuousEffect copy() {
|
||||
return new NowhereToRunWardEffect(this);
|
||||
}
|
||||
}
|
||||
120
Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java
Normal file
120
Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package mage.cards.p;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.AsEntersBattlefieldAbility;
|
||||
import mage.abilities.common.DiesCreatureTriggeredAbility;
|
||||
import mage.abilities.condition.common.ModeChoiceSourceCondition;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.CostImpl;
|
||||
import mage.abilities.decorator.ConditionalTriggeredAbility;
|
||||
import mage.abilities.effects.common.ChooseModeEffect;
|
||||
import mage.abilities.effects.common.CreateTokenEffect;
|
||||
import mage.abilities.effects.common.DoIfCostPaid;
|
||||
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
|
||||
import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility;
|
||||
import mage.constants.*;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterNonlandPermanent;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.token.HorrorEnchantmentCreatureToken;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetPermanent;
|
||||
|
||||
/**
|
||||
* @author Cguy7777
|
||||
*/
|
||||
public final class PhenomenonInvestigators extends CardImpl {
|
||||
|
||||
public PhenomenonInvestigators(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}");
|
||||
|
||||
this.subtype.add(SubType.HUMAN);
|
||||
this.subtype.add(SubType.DETECTIVE);
|
||||
this.power = new MageInt(3);
|
||||
this.toughness = new MageInt(4);
|
||||
|
||||
// As Phenomenon Investigators enters, choose Believe or Doubt.
|
||||
this.addAbility(new AsEntersBattlefieldAbility(
|
||||
new ChooseModeEffect("Believe or Doubt?", "Believe", "Doubt")));
|
||||
|
||||
// * Believe -- Whenever a nontoken creature you control dies, create a 2/2 black Horror enchantment creature token.
|
||||
this.addAbility(new ConditionalTriggeredAbility(
|
||||
new DiesCreatureTriggeredAbility(
|
||||
new CreateTokenEffect(new HorrorEnchantmentCreatureToken()),
|
||||
false,
|
||||
StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN),
|
||||
new ModeChoiceSourceCondition("Believe"),
|
||||
"&bull Believe — 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
|
||||
package mage.sets;
|
||||
|
||||
import mage.cards.ExpansionSet;
|
||||
import mage.collation.BoosterCollator;
|
||||
import mage.collation.BoosterStructure;
|
||||
import mage.collation.CardRun;
|
||||
import mage.collation.RarityConfiguration;
|
||||
import mage.constants.Rarity;
|
||||
import mage.constants.SetType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
|
@ -176,4 +182,61 @@ public final class AlaraReborn extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Zealous Persecution", 85, Rarity.UNCOMMON, mage.cards.z.ZealousPersecution.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoosterCollator createCollator() {
|
||||
return new AlaraRebornCollator();
|
||||
}
|
||||
}
|
||||
|
||||
// Booster collation info from https://vm1.substation33.com/tiera/t/lethe/arb.html
|
||||
// Using USA collation
|
||||
class AlaraRebornCollator implements BoosterCollator {
|
||||
private final CardRun commonA = new CardRun(true, "51", "74", "122", "45", "52", "14", "134", "29", "75", "138", "43", "13", "59", "80", "135", "27", "7", "143", "46", "72", "96", "22", "134", "4", "80", "144", "17", "46", "96", "19", "7", "138", "105", "132", "95", "75", "51", "22", "4", "122", "56", "45", "143", "74", "29", "13", "48", "139", "56", "72", "17", "5", "144", "27", "43", "14", "139", "105", "52", "48", "132", "19", "59", "5", "135", "95");
|
||||
private final CardRun commonB = new CardRun(true, "78", "131", "9", "55", "40", "107", "69", "18", "112", "61", "10", "40", "125", "79", "20", "107", "3", "55", "35", "116", "32", "79", "63", "112", "3", "66", "88", "142", "41", "32", "63", "141", "84", "54", "116", "66", "35", "20", "131", "38", "9", "54", "141", "78", "41", "84", "18", "142", "69", "61", "125", "10", "38", "88");
|
||||
private final CardRun uncommonA = new CardRun(false, "1", "11", "23", "25", "34", "39", "62", "65", "68", "85", "89", "93", "99", "100", "101", "111", "120", "133", "136", "137", "140", "145");
|
||||
private final CardRun uncommonB = new CardRun(false, "15", "16", "21", "26", "33", "44", "50", "57", "64", "76", "77", "83", "87", "94", "102", "108", "115", "127");
|
||||
private final CardRun rare = new CardRun(false, "2", "2", "6", "6", "8", "8", "12", "12", "24", "24", "28", "28", "30", "30", "31", "31", "36", "36", "42", "42", "47", "47", "49", "49", "58", "58", "60", "60", "67", "67", "70", "70", "71", "71", "73", "73", "81", "81", "82", "82", "86", "86", "90", "90", "92", "92", "97", "97", "98", "98", "103", "103", "104", "104", "106", "106", "114", "114", "118", "118", "119", "119", "121", "121", "123", "123", "126", "126", "129", "129", "37", "53", "91", "109", "110", "113", "117", "124", "128", "130");
|
||||
private final CardRun land = new CardRun(false, "ALA_230", "ALA_231", "ALA_232", "ALA_233", "ALA_234", "ALA_235", "ALA_236", "ALA_237", "ALA_238", "ALA_239", "ALA_240", "ALA_241", "ALA_242", "ALA_243", "ALA_244", "ALA_245", "ALA_246", "ALA_247", "ALA_248", "ALA_249");
|
||||
|
||||
private final BoosterStructure AAAAAABBBB = new BoosterStructure(
|
||||
commonA, commonA, commonA, commonA, commonA, commonA,
|
||||
commonB, commonB, commonB, commonB
|
||||
);
|
||||
private final BoosterStructure AAAAABBBBB = new BoosterStructure(
|
||||
commonA, commonA, commonA, commonA, commonA,
|
||||
commonB, commonB, commonB, commonB, commonB
|
||||
);
|
||||
private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB);
|
||||
private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB);
|
||||
private final BoosterStructure R1 = new BoosterStructure(rare);
|
||||
private final BoosterStructure L1 = new BoosterStructure(land);
|
||||
|
||||
// In order for equal numbers of each common to exist, the average booster must contain:
|
||||
// 5.5 A commons (11 / 2)
|
||||
// 4.5 B commons ( 9 / 2)
|
||||
private final RarityConfiguration commonRuns = new RarityConfiguration(
|
||||
AAAAAABBBB,
|
||||
AAAAABBBBB
|
||||
);
|
||||
// In order for equal numbers of each uncommon to exist, the average booster must contain:
|
||||
// 1.65 A uncommons (33 / 20)
|
||||
// 1.35 B uncommons (27 / 20)
|
||||
// These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs
|
||||
private final RarityConfiguration uncommonRuns = new RarityConfiguration(
|
||||
AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB,
|
||||
ABB, ABB, ABB, ABB, ABB, ABB, ABB
|
||||
);
|
||||
private final RarityConfiguration rareRuns = new RarityConfiguration(R1);
|
||||
private final RarityConfiguration landRuns = new RarityConfiguration(L1);
|
||||
|
||||
@Override
|
||||
public List<String> makeBooster() {
|
||||
List<String> booster = new ArrayList<>();
|
||||
booster.addAll(commonRuns.getNext().makeRun());
|
||||
booster.addAll(uncommonRuns.getNext().makeRun());
|
||||
booster.addAll(rareRuns.getNext().makeRun());
|
||||
booster.addAll(landRuns.getNext().makeRun());
|
||||
return booster;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
|
||||
package mage.sets;
|
||||
|
||||
import mage.cards.ExpansionSet;
|
||||
import mage.collation.BoosterCollator;
|
||||
import mage.collation.BoosterStructure;
|
||||
import mage.collation.CardRun;
|
||||
import mage.collation.RarityConfiguration;
|
||||
import mage.constants.Rarity;
|
||||
import mage.constants.SetType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
|
@ -175,4 +181,60 @@ public final class Conflux extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Zombie Outlander", 133, Rarity.COMMON, mage.cards.z.ZombieOutlander.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoosterCollator createCollator() {
|
||||
return new ConfluxCollator();
|
||||
}
|
||||
}
|
||||
|
||||
// Booster collation info from https://vm1.substation33.com/tiera/t/lethe/cfx.html
|
||||
// Using USA collation
|
||||
class ConfluxCollator implements BoosterCollator {
|
||||
private final CardRun commonA = new CardRun(true, "8", "97", "28", "63", "50", "130", "134", "42", "4", "72", "36", "78", "138", "133", "84", "7", "63", "56", "105", "32", "4", "137", "81", "72", "27", "135", "8", "54", "132", "97", "61", "28", "3", "56", "94", "60", "36", "54", "137", "16", "133", "78", "7", "37", "134", "67", "27", "40", "130", "94", "3", "50", "61", "32", "84", "135", "42", "60", "105", "37", "16", "132", "40", "81", "67", "138");
|
||||
private final CardRun commonB = new CardRun(true, "90", "47", "39", "70", "51", "2", "119", "29", "106", "9", "90", "57", "86", "76", "21", "128", "68", "51", "109", "6", "39", "85", "52", "21", "106", "68", "119", "86", "19", "29", "57", "131", "70", "96", "9", "144", "128", "22", "6", "69", "47", "122", "76", "85", "19", "52", "109", "22", "96", "2", "144", "131", "69", "122");
|
||||
private final CardRun uncommonA = new CardRun(false, "141", "5", "23", "41", "103", "24", "62", "82", "107", "83", "65", "66", "112", "114", "139", "13", "14", "143", "91", "129", "38", "55");
|
||||
private final CardRun uncommonB = new CardRun(false, "1", "43", "104", "25", "45", "46", "111", "15", "89", "123", "34", "124", "125", "126", "93", "145", "73", "74");
|
||||
private final CardRun rare = new CardRun(false, "58", "58", "59", "59", "99", "99", "100", "100", "79", "79", "80", "80", "142", "142", "44", "44", "136", "136", "108", "108", "64", "64", "110", "110", "30", "30", "48", "48", "113", "113", "116", "116", "10", "10", "11", "11", "31", "31", "118", "118", "87", "87", "49", "49", "140", "140", "88", "88", "71", "71", "17", "17", "53", "53", "33", "33", "18", "18", "92", "92", "127", "127", "35", "35", "75", "75", "20", "20", "77", "77", "98", "101", "102", "26", "115", "117", "12", "120", "121", "95");
|
||||
private final CardRun land = new CardRun(false, "ALA_230", "ALA_231", "ALA_232", "ALA_233", "ALA_234", "ALA_235", "ALA_236", "ALA_237", "ALA_238", "ALA_239", "ALA_240", "ALA_241", "ALA_242", "ALA_243", "ALA_244", "ALA_245", "ALA_246", "ALA_247", "ALA_248", "ALA_249");
|
||||
|
||||
private final BoosterStructure AAAAAABBBB = new BoosterStructure(
|
||||
commonA, commonA, commonA, commonA, commonA, commonA,
|
||||
commonB, commonB, commonB, commonB
|
||||
);
|
||||
private final BoosterStructure AAAAABBBBB = new BoosterStructure(
|
||||
commonA, commonA, commonA, commonA, commonA,
|
||||
commonB, commonB, commonB, commonB, commonB
|
||||
);
|
||||
private final BoosterStructure AAB = new BoosterStructure(uncommonA, uncommonA, uncommonB);
|
||||
private final BoosterStructure ABB = new BoosterStructure(uncommonA, uncommonB, uncommonB);
|
||||
private final BoosterStructure R1 = new BoosterStructure(rare);
|
||||
private final BoosterStructure L1 = new BoosterStructure(land);
|
||||
|
||||
// In order for equal numbers of each common to exist, the average booster must contain:
|
||||
// 5.5 A commons (11 / 2)
|
||||
// 4.5 B commons ( 9 / 2)
|
||||
private final RarityConfiguration commonRuns = new RarityConfiguration(
|
||||
AAAAAABBBB,
|
||||
AAAAABBBBB
|
||||
);
|
||||
// In order for equal numbers of each uncommon to exist, the average booster must contain:
|
||||
// 1.65 A uncommons (33 / 20)
|
||||
// 1.35 B uncommons (27 / 20)
|
||||
// These numbers are the same for all sets with 60 uncommons in asymmetrical A/B print runs
|
||||
private final RarityConfiguration uncommonRuns = new RarityConfiguration(
|
||||
AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB, AAB,
|
||||
ABB, ABB, ABB, ABB, ABB, ABB, ABB
|
||||
);
|
||||
private final RarityConfiguration rareRuns = new RarityConfiguration(R1);
|
||||
private final RarityConfiguration landRuns = new RarityConfiguration(L1);
|
||||
|
||||
@Override
|
||||
public List<String> makeBooster() {
|
||||
List<String> booster = new ArrayList<>();
|
||||
booster.addAll(commonRuns.getNext().makeRun());
|
||||
booster.addAll(uncommonRuns.getNext().makeRun());
|
||||
booster.addAll(rareRuns.getNext().makeRun());
|
||||
booster.addAll(landRuns.getNext().makeRun());
|
||||
return booster;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../Config.xsd">
|
||||
<server serverAddress="0.0.0.0" serverName="mage-server" port="17171" maxGameThreads="10" maxSecondsIdle="600"/>
|
||||
<playerTypes>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayer7"/>
|
||||
<playerType name="Computer - mad" jar="mage-player-ai-ma.jar" className="mage.player.ai.ComputerPlayerControllableProxy"/>
|
||||
<playerType name="Computer - monte carlo" jar="mage-player-aimcts.jar" className="mage.player.ai.ComputerPlayerMCTS"/>
|
||||
</playerTypes>
|
||||
<gameTypes>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import java.time.Instant;
|
|||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Intended to test Mage server under different load patterns.
|
||||
|
|
@ -214,7 +215,7 @@ public class LoadTest {
|
|||
}
|
||||
}
|
||||
|
||||
public void playTwoAIGame(String gameName, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) {
|
||||
public void playTwoAIGame(String gameName, Integer taskNumber, TasksProgress tasksProgress, long randomSeed, String deckColors, String deckAllowedSets, LoadTestGameResult gameResult) {
|
||||
Assert.assertFalse("need deck colors", deckColors.isEmpty());
|
||||
Assert.assertFalse("need allowed sets", deckAllowedSets.isEmpty());
|
||||
|
||||
|
|
@ -249,9 +250,13 @@ public class LoadTest {
|
|||
TableView checkGame = monitor.getTable(tableId).orElse(null);
|
||||
TableState state = (checkGame == null ? null : checkGame.getTableState());
|
||||
|
||||
tasksProgress.update(taskNumber, state == TableState.FINISHED, gameView == null ? 0 : gameView.getTurn());
|
||||
String globalProgress = tasksProgress.getInfo();
|
||||
|
||||
if (gameView != null && checkGame != null) {
|
||||
logger.info(checkGame.getTableName() + ": ---");
|
||||
logger.info(String.format("%s: turn %d, step %s, state %s",
|
||||
logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---");
|
||||
logger.info(String.format("%s, %s: turn %d, step %s, state %s",
|
||||
globalProgress,
|
||||
checkGame.getTableName(),
|
||||
gameView.getTurn(),
|
||||
gameView.getStep().toString(),
|
||||
|
|
@ -278,7 +283,8 @@ public class LoadTest {
|
|||
if (Objects.equals(gameView.getActivePlayerId(), p.getPlayerId())) {
|
||||
activeInfo = " (active)";
|
||||
}
|
||||
logger.info(String.format("%s, status: %s - Life=%d; Lib=%d;%s",
|
||||
logger.info(String.format("%s, %s, status: %s - Life=%d; Lib=%d;%s",
|
||||
globalProgress,
|
||||
checkGame.getTableName(),
|
||||
p.getName(),
|
||||
p.getLife(),
|
||||
|
|
@ -286,7 +292,7 @@ public class LoadTest {
|
|||
activeInfo
|
||||
));
|
||||
});
|
||||
logger.info(checkGame.getTableName() + ": ---");
|
||||
logger.info(globalProgress + ", " + checkGame.getTableName() + ": ---");
|
||||
}
|
||||
|
||||
// ping to keep active session
|
||||
|
|
@ -312,7 +318,9 @@ public class LoadTest {
|
|||
LoadTestGameResultsList gameResults = new LoadTestGameResultsList();
|
||||
long randomSeed = RandomUtil.nextInt();
|
||||
LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed);
|
||||
playTwoAIGame("Single AI game", randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
|
||||
TasksProgress tasksProgress = new TasksProgress();
|
||||
tasksProgress.update(1, true, 0);
|
||||
playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
|
||||
|
||||
printGameResults(gameResults);
|
||||
}
|
||||
|
|
@ -347,15 +355,16 @@ public class LoadTest {
|
|||
|
||||
LoadTestGameResultsList gameResults = new LoadTestGameResultsList();
|
||||
try {
|
||||
TasksProgress tasksProgress = new TasksProgress();
|
||||
for (int i = 0; i < seedsList.size(); i++) {
|
||||
int gameIndex = i;
|
||||
tasksProgress.update(gameIndex + 1, true, 0);
|
||||
long randomSeed = seedsList.get(i);
|
||||
logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed);
|
||||
|
||||
Future gameTask = executerService.submit(() -> {
|
||||
String gameName = "AI game #" + (gameIndex + 1);
|
||||
LoadTestGameResult gameResult = gameResults.createGame(gameIndex + 1, gameName, randomSeed);
|
||||
playTwoAIGame(gameName, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
|
||||
playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
|
||||
});
|
||||
|
||||
if (!isRunParallel) {
|
||||
|
|
@ -571,6 +580,40 @@ public class LoadTest {
|
|||
return createSimpleGameOptions(gameName, gameTypeView, session, PlayerType.COMPUTER_MAD);
|
||||
}
|
||||
|
||||
private static class TasksProgress {
|
||||
|
||||
private String info;
|
||||
private final Map<Integer, Boolean> finishes = new LinkedHashMap<>();
|
||||
private final Map<Integer, Integer> turns = new LinkedHashMap<>();
|
||||
|
||||
synchronized public void update(Integer taskNumber, boolean newFinish, Integer newTurn) {
|
||||
Boolean oldFinish = this.finishes.getOrDefault(taskNumber, false);
|
||||
Integer oldTurn = this.turns.getOrDefault(taskNumber, 0);
|
||||
if (!this.finishes.containsKey(taskNumber)
|
||||
|| !Objects.equals(oldFinish, newFinish)
|
||||
|| !Objects.equals(oldTurn, newTurn)) {
|
||||
this.finishes.put(taskNumber, newFinish);
|
||||
this.turns.put(taskNumber, newTurn);
|
||||
updateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateInfo() {
|
||||
// example: progress [=00, +01, +01, =12, =15, =01, +61]
|
||||
String res = this.finishes.keySet().stream()
|
||||
.map(taskNumber -> String.format("%s%02d",
|
||||
this.finishes.getOrDefault(taskNumber, false) ? "=" : "+",
|
||||
this.turns.getOrDefault(taskNumber, 0)
|
||||
))
|
||||
.collect(Collectors.joining(", "));
|
||||
this.info = String.format("progress [%s]", res);
|
||||
}
|
||||
|
||||
public String getInfo() {
|
||||
return this.info;
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadPlayer {
|
||||
|
||||
String userName;
|
||||
|
|
@ -798,23 +841,23 @@ public class LoadTest {
|
|||
}
|
||||
|
||||
public int getLife1() {
|
||||
return this.finalGameView.getPlayers().get(0).getLife();
|
||||
return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(0).getLife();
|
||||
}
|
||||
|
||||
public int getLife2() {
|
||||
return this.finalGameView.getPlayers().get(1).getLife();
|
||||
return finalGameView == null ? 0 : this.finalGameView.getPlayers().get(1).getLife();
|
||||
}
|
||||
|
||||
public int getTurn() {
|
||||
return this.finalGameView.getTurn();
|
||||
return finalGameView == null ? 0 : this.finalGameView.getTurn();
|
||||
}
|
||||
|
||||
public int getDurationMs() {
|
||||
return (int) ((this.timeEnded.getTime() - this.timeStarted.getTime()));
|
||||
return finalGameView == null ? 0 : ((int) ((this.timeEnded.getTime() - this.timeStarted.getTime())));
|
||||
}
|
||||
|
||||
public int getTotalErrorsCount() {
|
||||
return this.finalGameView.getTotalErrorsCount();
|
||||
return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -886,15 +929,15 @@ public class LoadTest {
|
|||
}
|
||||
|
||||
private int getAvgTurn() {
|
||||
return this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
|
||||
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
|
||||
}
|
||||
|
||||
private int getAvgLife1() {
|
||||
return this.values().stream().mapToInt(LoadTestGameResult::getLife1).sum() / this.size();
|
||||
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife1).sum() / this.size();
|
||||
}
|
||||
|
||||
private int getAvgLife2() {
|
||||
return this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
|
||||
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
|
||||
}
|
||||
|
||||
private int getTotalDurationMs() {
|
||||
|
|
@ -902,11 +945,12 @@ public class LoadTest {
|
|||
}
|
||||
|
||||
private int getAvgDurationMs() {
|
||||
return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size();
|
||||
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size();
|
||||
}
|
||||
|
||||
private int getAvgDurationPerTurnMs() {
|
||||
return getAvgDurationMs() / getAvgTurn();
|
||||
int turns = getAvgTurn();
|
||||
return turns == 0 ? 0 : getAvgDurationMs() / getAvgTurn();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -952,5 +996,9 @@ public class LoadTest {
|
|||
gameResults.printResultHeader();
|
||||
gameResults.printResultData();
|
||||
gameResults.printResultTotal();
|
||||
|
||||
if (gameResults.getAvgTurn() == 0) {
|
||||
Assert.fail("Games can't start, make sure you are run a localhost server before running current load test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ import mage.filter.common.*;
|
|||
import mage.filter.predicate.Predicates;
|
||||
import mage.filter.predicate.mageobject.NamePredicate;
|
||||
import mage.filter.predicate.permanent.SummoningSicknessPredicate;
|
||||
import mage.game.Game;
|
||||
import mage.game.GameImpl;
|
||||
import mage.game.Graveyard;
|
||||
import mage.game.Table;
|
||||
import mage.game.*;
|
||||
import mage.game.combat.CombatGroup;
|
||||
import mage.game.command.CommandObject;
|
||||
import mage.game.draft.Draft;
|
||||
|
|
@ -52,6 +49,7 @@ import mage.target.common.*;
|
|||
import mage.util.CardUtil;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.util.RandomUtil;
|
||||
import mage.watchers.common.AttackedOrBlockedThisCombatWatcher;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.junit.Assert;
|
||||
|
||||
|
|
@ -839,6 +837,20 @@ public class TestPlayer implements Player {
|
|||
wasProccessed = true;
|
||||
}
|
||||
|
||||
// check attacking: attackers list
|
||||
if (params[0].equals(CHECK_COMMAND_ATTACKERS) && params.length == 2) {
|
||||
assertAttackers(action, game, computerPlayer, params[1]);
|
||||
actions.remove(action);
|
||||
wasProccessed = true;
|
||||
}
|
||||
|
||||
// check attacking: attackers list
|
||||
if (params[0].equals(CHECK_COMMAND_BLOCKERS) && params.length == 2) {
|
||||
assertBlockers(action, game, computerPlayer, params[1]);
|
||||
actions.remove(action);
|
||||
wasProccessed = true;
|
||||
}
|
||||
|
||||
// check playable ability: ability text, must have
|
||||
if (params[0].equals(CHECK_COMMAND_PLAYABLE_ABILITY) && params.length == 3) {
|
||||
assertPlayableAbility(action, game, computerPlayer, params[1], Boolean.parseBoolean(params[2]));
|
||||
|
|
@ -1418,6 +1430,64 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
private void assertAttackers(PlayerAction action, Game game, Player player, String attackers) {
|
||||
AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class);
|
||||
Assert.assertNotNull(watcher);
|
||||
|
||||
List<String> actualAttackers = watcher.getAttackedThisTurnCreatures().stream()
|
||||
.map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki
|
||||
.filter(Objects::nonNull)
|
||||
.filter(o -> o instanceof ControllableOrOwnerable)
|
||||
.map(o -> (ControllableOrOwnerable) o)
|
||||
.filter(o -> o.getControllerOrOwnerId().equals(player.getId()))
|
||||
.map(o -> ((MageObject) o).getName())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
List<String> needAttackers = Arrays.stream(attackers.split("\\^"))
|
||||
.filter(s -> !s.equals(TestPlayer.ATTACK_SKIP))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!actualAttackers.equals(needAttackers)) {
|
||||
printStart(game, action.getActionName());
|
||||
System.out.println(String.format("Need attackers: %d", needAttackers.size()));
|
||||
needAttackers.forEach(s -> System.out.println(" - " + s));
|
||||
System.out.println(String.format("Actual attackers: %d", actualAttackers.size()));
|
||||
actualAttackers.forEach(s -> System.out.println(" - " + s));
|
||||
printEnd();
|
||||
Assert.fail("Found wrong attackers");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertBlockers(PlayerAction action, Game game, Player player, String blockers) {
|
||||
AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class);
|
||||
Assert.assertNotNull(watcher);
|
||||
|
||||
List<String> actualBlockers = watcher.getBlockedThisTurnCreatures().stream()
|
||||
.map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki
|
||||
.filter(Objects::nonNull)
|
||||
.filter(o -> o instanceof ControllableOrOwnerable)
|
||||
.map(o -> (ControllableOrOwnerable) o)
|
||||
.filter(o -> o.getControllerOrOwnerId().equals(player.getId()))
|
||||
.map(o -> ((MageObject) o).getName())
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
List<String> needBlockers = Arrays.stream(blockers.split("\\^"))
|
||||
.filter(s -> !s.equals(TestPlayer.BLOCK_SKIP))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!actualBlockers.equals(needBlockers)) {
|
||||
printStart(game, action.getActionName());
|
||||
System.out.println(String.format("Need blockers: %d", needBlockers.size()));
|
||||
needBlockers.forEach(s -> System.out.println(" - " + s));
|
||||
System.out.println(String.format("Actual blockers: %d", actualBlockers.size()));
|
||||
actualBlockers.forEach(s -> System.out.println(" - " + s));
|
||||
printEnd();
|
||||
Assert.fail("Found wrong blockers");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertMayAttackDefender(PlayerAction action, Game game, Player controller, String permanentName, Player defender, boolean expectedMayAttack) {
|
||||
Permanent attackingPermanent = findPermanentWithAssert(action, game, controller, permanentName);
|
||||
|
||||
|
|
@ -2216,13 +2286,10 @@ public class TestPlayer implements Player {
|
|||
|
||||
// TODO: Allow to choose a player with TargetPermanentOrPlayer
|
||||
if ((target.getOriginalTarget() instanceof TargetPermanent)
|
||||
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) // player target not implemented yet
|
||||
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)) { // player target not implemented yet
|
||||
FilterPermanent filterPermanent;
|
||||
if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
|
||||
filterPermanent = ((TargetPermanentOrPlayer) target.getOriginalTarget()).getFilterPermanent();
|
||||
} else if (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) {
|
||||
filterPermanent = ((TargetCreatureOrPlayer) target.getOriginalTarget()).getFilterCreature();
|
||||
} else {
|
||||
filterPermanent = ((TargetPermanent) target.getOriginalTarget()).getFilter();
|
||||
}
|
||||
|
|
@ -2446,8 +2513,6 @@ public class TestPlayer implements Player {
|
|||
|
||||
// player
|
||||
if (target.getOriginalTarget() instanceof TargetPlayer
|
||||
|| target.getOriginalTarget() instanceof TargetAnyTarget
|
||||
|| target.getOriginalTarget() instanceof TargetCreatureOrPlayer
|
||||
|| target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
|
||||
for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) {
|
||||
if (!targetDefinition.startsWith("targetPlayer=")) {
|
||||
|
|
@ -2469,8 +2534,6 @@ public class TestPlayer implements Player {
|
|||
// permanent in battlefield
|
||||
if ((target.getOriginalTarget() instanceof TargetPermanent)
|
||||
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)
|
||||
|| (target.getOriginalTarget() instanceof TargetAnyTarget)
|
||||
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer)
|
||||
|| (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard)) {
|
||||
for (String targetDefinition : targets.stream().limit(takeMaxTargetsPerChoose).collect(Collectors.toList())) {
|
||||
if (targetDefinition.startsWith("targetPlayer=")) {
|
||||
|
|
@ -2494,9 +2557,6 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
}
|
||||
Filter filter = target.getOriginalTarget().getFilter();
|
||||
if (filter instanceof FilterCreatureOrPlayer) {
|
||||
filter = ((FilterCreatureOrPlayer) filter).getCreatureFilter();
|
||||
}
|
||||
if (filter instanceof FilterPermanentOrPlayer) {
|
||||
filter = ((FilterPermanentOrPlayer) filter).getPermanentFilter();
|
||||
}
|
||||
|
|
@ -4484,10 +4544,6 @@ public class TestPlayer implements Player {
|
|||
return AIPlayer;
|
||||
}
|
||||
|
||||
public String getHistory() {
|
||||
return computerPlayer.getHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides) {
|
||||
return computerPlayer.rollPlanarDie(outcome, source, game, numberChaosSides, numberPlanarSides);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +1,22 @@
|
|||
package mage.filter.common;
|
||||
|
||||
import mage.MageItem;
|
||||
import mage.abilities.Ability;
|
||||
import mage.filter.FilterImpl;
|
||||
import mage.filter.FilterInPlay;
|
||||
import mage.filter.FilterPlayer;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class FilterCreatureOrPlayer extends FilterImpl<MageItem> implements FilterInPlay<MageItem> {
|
||||
|
||||
protected FilterCreaturePermanent creatureFilter;
|
||||
protected final FilterPlayer playerFilter;
|
||||
public class FilterCreatureOrPlayer extends FilterPermanentOrPlayer {
|
||||
|
||||
public FilterCreatureOrPlayer() {
|
||||
this("creature or player");
|
||||
}
|
||||
|
||||
public FilterCreatureOrPlayer(String name) {
|
||||
super(name);
|
||||
creatureFilter = new FilterCreaturePermanent();
|
||||
playerFilter = new FilterPlayer();
|
||||
super(name, new FilterCreaturePermanent(), new FilterPlayer());
|
||||
}
|
||||
|
||||
protected FilterCreatureOrPlayer(final FilterCreatureOrPlayer filter) {
|
||||
super(filter);
|
||||
this.creatureFilter = filter.creatureFilter.copy();
|
||||
this.playerFilter = filter.playerFilter.copy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkObjectClass(Object object) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(MageItem o, Game game) {
|
||||
if (super.match(o, game)) {
|
||||
if (o instanceof Player) {
|
||||
return playerFilter.match((Player) o, game);
|
||||
} else if (o instanceof Permanent) {
|
||||
return creatureFilter.match((Permanent) o, game);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(MageItem o, UUID playerId, Ability source, Game game) {
|
||||
if (super.match(o, game)) { // process predicates
|
||||
if (o instanceof Player) {
|
||||
return playerFilter.match((Player) o, playerId, source, game);
|
||||
} else if (o instanceof Permanent) {
|
||||
return creatureFilter.match((Permanent) o, playerId, source, game);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public FilterCreaturePermanent getCreatureFilter() {
|
||||
return this.creatureFilter;
|
||||
}
|
||||
|
||||
public FilterPlayer getPlayerFilter() {
|
||||
return this.playerFilter;
|
||||
}
|
||||
|
||||
public void setCreatureFilter(FilterCreaturePermanent creatureFilter) {
|
||||
this.creatureFilter = creatureFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -550,6 +550,12 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
@Deprecated // TODO: must research usage and remove it from all non engine code (example: Bestow ability, ProcessActions must be used instead)
|
||||
boolean checkStateAndTriggered();
|
||||
|
||||
/**
|
||||
* Play priority by all players
|
||||
*
|
||||
* @param activePlayerId starting priority player
|
||||
* @param resuming false to reset passed priority and ask it again
|
||||
*/
|
||||
void playPriority(UUID activePlayerId, boolean resuming);
|
||||
|
||||
void resetControlAfterSpellResolve(UUID topId);
|
||||
|
|
|
|||
|
|
@ -845,6 +845,10 @@ public abstract class GameImpl implements Game {
|
|||
// concede for itself
|
||||
// stop current player dialog and execute concede
|
||||
currentPriorityPlayer.signalPlayerConcede(true);
|
||||
} else if (currentPriorityPlayer.getTurnControlledBy().equals(playerId)) {
|
||||
// concede for itself while controlling another player
|
||||
// stop current player dialog and execute concede
|
||||
currentPriorityPlayer.signalPlayerConcede(true);
|
||||
} else {
|
||||
// concede for another player
|
||||
// allow current player to continue and check concede on any next priority
|
||||
|
|
@ -1423,6 +1427,7 @@ public abstract class GameImpl implements Game {
|
|||
newWatchers.add(new CreaturesDiedWatcher());
|
||||
newWatchers.add(new TemptedByTheRingWatcher());
|
||||
newWatchers.add(new SpellsCastWatcher());
|
||||
newWatchers.add(new AttackedOrBlockedThisCombatWatcher()); // required for tests
|
||||
|
||||
// runtime check - allows only GAME scope (one watcher per game)
|
||||
newWatchers.forEach(watcher -> {
|
||||
|
|
@ -2986,21 +2991,35 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
String message;
|
||||
if (this.canPlaySorcery(playerId)) {
|
||||
message = "Play spells and abilities.";
|
||||
message = "Play spells and abilities";
|
||||
} else {
|
||||
message = "Play instants and activated abilities.";
|
||||
message = "Play instants and activated abilities";
|
||||
}
|
||||
playerQueryEventSource.select(playerId, message);
|
||||
|
||||
message += getControllingPlayerHint(playerId);
|
||||
|
||||
Player player = this.getPlayer(playerId);
|
||||
playerQueryEventSource.select(player.getTurnControlledBy(), message);
|
||||
getState().clearLookedAt();
|
||||
getState().clearRevealed();
|
||||
}
|
||||
|
||||
private String getControllingPlayerHint(UUID playerId) {
|
||||
Player player = this.getPlayer(playerId);
|
||||
Player controllingPlayer = this.getPlayer(player.getTurnControlledBy());
|
||||
if (player != controllingPlayer) {
|
||||
return " (as " + player.getLogName() + ")";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void fireSelectEvent(UUID playerId, String message) {
|
||||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.select(playerId, message);
|
||||
playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3008,7 +3027,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.select(playerId, message, options);
|
||||
playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId), options);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3016,7 +3035,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.playMana(playerId, message, options);
|
||||
playerQueryEventSource.playMana(playerId, message + getControllingPlayerHint(playerId), options);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3024,7 +3043,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.playXMana(playerId, message);
|
||||
playerQueryEventSource.playXMana(playerId, message + getControllingPlayerHint(playerId));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3037,7 +3056,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.ask(playerId, message.getMessage(), source, addMessageToOptions(message, options));
|
||||
playerQueryEventSource.ask(playerId, message.getMessage() + getControllingPlayerHint(playerId), source, addMessageToOptions(message, options));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3049,7 +3068,7 @@ public abstract class GameImpl implements Game {
|
|||
if (object != null) {
|
||||
objectName = object.getName();
|
||||
}
|
||||
playerQueryEventSource.chooseAbility(playerId, message, objectName, choices);
|
||||
playerQueryEventSource.chooseAbility(playerId, message + getControllingPlayerHint(playerId), objectName, choices);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3057,7 +3076,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.chooseMode(playerId, message, modes);
|
||||
playerQueryEventSource.chooseMode(playerId, message + getControllingPlayerHint(playerId), modes);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3065,7 +3084,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.target(playerId, message.getMessage(), targets, required, addMessageToOptions(message, options));
|
||||
playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), targets, required, addMessageToOptions(message, options));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3073,7 +3092,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.target(playerId, message.getMessage(), cards, required, addMessageToOptions(message, options));
|
||||
playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), cards, required, addMessageToOptions(message, options));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3086,7 +3105,7 @@ public abstract class GameImpl implements Game {
|
|||
*/
|
||||
@Override
|
||||
public void fireSelectTargetTriggeredAbilityEvent(UUID playerId, String message, List<TriggeredAbility> abilities) {
|
||||
playerQueryEventSource.target(playerId, message, abilities);
|
||||
playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), abilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3094,7 +3113,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.target(playerId, message, perms, required);
|
||||
playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), perms, required);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3102,7 +3121,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.amount(playerId, message, min, max);
|
||||
playerQueryEventSource.amount(playerId, message + getControllingPlayerHint(playerId), min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3127,7 +3146,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
playerQueryEventSource.choosePile(playerId, message, pile1, pile2);
|
||||
playerQueryEventSource.choosePile(playerId, message + getControllingPlayerHint(playerId), pile1, pile2);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3635,7 +3654,7 @@ public abstract class GameImpl implements Game {
|
|||
|
||||
/**
|
||||
* Reset objects stored for Last Known Information. (Happens if all effects
|
||||
* are applied und stack is empty)
|
||||
* are applied and stack is empty)
|
||||
*/
|
||||
@Override
|
||||
public void resetLKI() {
|
||||
|
|
|
|||
|
|
@ -663,26 +663,47 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
if (defender == null) {
|
||||
continue;
|
||||
}
|
||||
boolean choose = true;
|
||||
if (blockController == null) {
|
||||
controller = defender;
|
||||
} else {
|
||||
controller = blockController;
|
||||
}
|
||||
while (choose) {
|
||||
|
||||
// choosing until good block configuration
|
||||
while (true) {
|
||||
// declare normal blockers
|
||||
// TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||
controller.selectBlockers(source, game, defenderId);
|
||||
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
|
||||
return;
|
||||
}
|
||||
if (!game.getCombat().checkBlockRestrictions(defender, game)) {
|
||||
if (controller.isHuman()) { // only human player can decide to do the block in another way
|
||||
continue;
|
||||
}
|
||||
|
||||
// check multiple restrictions by permanents and effects, reset on invalid blocking configuration, try to auto-fix
|
||||
// TODO: wtf, some checks contains AI related code inside -- it must be reworked and moved to computer classes?!
|
||||
|
||||
// check 1 of 3
|
||||
boolean isValidBlock = game.getCombat().checkBlockRestrictions(defender, game);
|
||||
if (!isValidBlock) {
|
||||
makeSureItsNotComputer(controller);
|
||||
continue;
|
||||
}
|
||||
choose = !game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
|
||||
if (!choose) {
|
||||
choose = !game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
|
||||
|
||||
// check 2 of 3
|
||||
isValidBlock = game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
|
||||
if (!isValidBlock) {
|
||||
makeSureItsNotComputer(controller);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check 3 of 3
|
||||
isValidBlock = game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
|
||||
if (!isValidBlock) {
|
||||
makeSureItsNotComputer(controller);
|
||||
continue;
|
||||
}
|
||||
|
||||
// all valid, can finish now
|
||||
break;
|
||||
}
|
||||
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defenderId, defenderId));
|
||||
|
||||
|
|
@ -695,6 +716,15 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
TraceUtil.traceCombatIfNeeded(game, game.getCombat());
|
||||
}
|
||||
|
||||
private void makeSureItsNotComputer(Player controller) {
|
||||
if (controller.isComputer() || !controller.isHuman()) {
|
||||
// TODO: wtf, AI will freeze forever here in games with attacker/blocker restrictions,
|
||||
// but it pass in some use cases due random choices. AI must deside blocker configuration
|
||||
// in one attempt
|
||||
//throw new IllegalStateException("AI can't find good blocker configuration, report it to github");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add info about attacker blocked by blocker to the game log
|
||||
*/
|
||||
|
|
@ -852,10 +882,7 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
* attacking creature fulfills both the restriction and the requirement, so
|
||||
* that's the only option.
|
||||
*
|
||||
* @param player
|
||||
* @param controller
|
||||
* @param game
|
||||
* @return
|
||||
* @return false on invalid block configuration e.g. player must choose new blockers
|
||||
*/
|
||||
public boolean checkBlockRequirementsAfter(Player player, Player controller, Game game) {
|
||||
// Get once a list of all opponents in range
|
||||
|
|
@ -866,7 +893,7 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
Map<UUID, Integer> minNumberOfBlockersMap = new HashMap<>();
|
||||
Map<UUID, Integer> minPossibleBlockersMap = new HashMap<>();
|
||||
|
||||
// check mustBlock requirements of creatures from opponents of attacking player
|
||||
// FIND attackers and potential blockers for "must be blocked" effects
|
||||
for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, player.getId(), game)) {
|
||||
// creature is controlled by an opponent of the attacker
|
||||
if (opponents.contains(creature.getControllerId())) {
|
||||
|
|
@ -985,7 +1012,7 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
if (toBeBlockedCreature != null) {
|
||||
CombatGroup toBeBlockedGroup = findGroup(toBeBlockedCreature);
|
||||
if (toBeBlockedGroup != null && toBeBlockedGroup.getDefendingPlayerId().equals(creature.getControllerId())) {
|
||||
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers());
|
||||
minNumberOfBlockersMap.put(toBeBlockedCreature, effect.getMinNumberOfBlockers()); // TODO: fail on multiple effects 1 + 2 min blockers?
|
||||
Permanent toBeBlockedCreaturePermanent = game.getPermanent(toBeBlockedCreature);
|
||||
if (toBeBlockedCreaturePermanent != null) {
|
||||
minPossibleBlockersMap.put(toBeBlockedCreature, toBeBlockedCreaturePermanent.getMinBlockedBy());
|
||||
|
|
@ -1069,12 +1096,10 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if for attacking creatures with mustBeBlockedByAtLeastX requirements are fulfilled
|
||||
// APPLY potential blockers to attackers with "must be blocked" effects
|
||||
for (UUID toBeBlockedCreatureId : mustBeBlockedByAtLeastX.keySet()) {
|
||||
for (CombatGroup combatGroup : game.getCombat().getGroups()) {
|
||||
if (combatGroup.getAttackers().contains(toBeBlockedCreatureId)) {
|
||||
|
|
@ -1097,6 +1122,8 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
if (!requirementFulfilled) {
|
||||
// creature is not blocked but has possible blockers
|
||||
if (controller.isHuman()) {
|
||||
// HUMAN logic - send warning about wrong blocker config and repeat declare
|
||||
// TODO: replace isHuman by !isComputer for working unit tests
|
||||
Permanent toBeBlockedCreature = game.getPermanent(toBeBlockedCreatureId);
|
||||
if (toBeBlockedCreature != null) {
|
||||
// check if all possible blocker block other creatures they are forced to block
|
||||
|
|
@ -1115,9 +1142,8 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// take the first potential blocker from the set to block for the AI
|
||||
// AI logic - auto-fix wrong blocker config (take the first potential blocker)
|
||||
for (UUID possibleBlockerId : mustBeBlockedByAtLeastX.get(toBeBlockedCreatureId)) {
|
||||
String blockRequiredMessage = isCreatureDoingARequiredBlock(
|
||||
possibleBlockerId, toBeBlockedCreatureId, mustBeBlockedByAtLeastX, game);
|
||||
|
|
@ -1140,8 +1166,8 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if creatures are forced to block but do not block at all or block creatures they are not forced to block
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<UUID, Set<UUID>> entry : creatureMustBlockAttackers.entrySet()) {
|
||||
|
|
@ -1274,10 +1300,7 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
* Checks the canBeBlockedCheckAfter RestrictionEffect Is the block still
|
||||
* valid after all block decisions are done
|
||||
*
|
||||
* @param player
|
||||
* @param controller
|
||||
* @param game
|
||||
* @return
|
||||
* @return false on invalid block configuration e.g. player must choose new blockers
|
||||
*/
|
||||
public boolean checkBlockRestrictionsAfter(Player player, Player controller, Game game) {
|
||||
// Restrictions applied to blocking creatures
|
||||
|
|
@ -1906,4 +1929,18 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
return new Combat(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
List<String> res = new ArrayList<>();
|
||||
for (int i = 0; i < this.groups.size(); i++) {
|
||||
res.add(String.format("group %d with %s",
|
||||
i + 1,
|
||||
this.groups.get(i)
|
||||
));
|
||||
}
|
||||
return String.format("%d groups%s",
|
||||
this.groups.size(),
|
||||
this.groups.size() > 0 ? ": " + String.join("; ", res) : ""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -976,4 +976,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) {
|
||||
return blocker.getLethalDamage(attacker.getId(), game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%d attackers, %d blockers",
|
||||
this.getAttackers().size(),
|
||||
this.getBlockers().size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
package mage.game.turn;
|
||||
|
||||
import mage.constants.TurnPhase;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ public abstract class Step implements Serializable, Copyable<Step> {
|
|||
stepPart = StepPart.PRE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play priority by all players
|
||||
*
|
||||
* @param activePlayerId starting priority player
|
||||
* @param resuming false to reset passed priority and ask it again
|
||||
*/
|
||||
public void priority(Game game, UUID activePlayerId, boolean resuming) {
|
||||
if (hasPriority) {
|
||||
stepPart = StepPart.PRIORITY;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,14 @@ public class TurnMods extends ArrayList<TurnMod> implements Serializable, Copyab
|
|||
}
|
||||
|
||||
public TurnMod useNextNewController(UUID playerId) {
|
||||
// 720.1a
|
||||
// Multiple player-controlling effects that affect the same player overwrite each other.
|
||||
// The last one to be created is the one that works.
|
||||
//
|
||||
// 720.1b
|
||||
// If a turn is skipped, any pending player-controlling effects wait until the player
|
||||
// who would be affected actually takes a turn.
|
||||
|
||||
TurnMod lastNewControllerMod = null;
|
||||
|
||||
// find last/actual mod
|
||||
|
|
|
|||
|
|
@ -38,9 +38,10 @@ public class ManaPool implements Serializable {
|
|||
private boolean forcedToPay; // for Word of Command
|
||||
private final List<ManaPoolItem> poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes
|
||||
|
||||
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>();
|
||||
private boolean manaBecomesBlack = false;
|
||||
private boolean manaBecomesColorless = false;
|
||||
// empty mana pool effects
|
||||
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>(); // keep some colors
|
||||
private boolean manaBecomesBlack = false; // replace all pool by black
|
||||
private boolean manaBecomesColorless = false; // replace all pool by colorless
|
||||
|
||||
private static final class ConditionalManaInfo {
|
||||
private final ManaType manaType;
|
||||
|
|
@ -147,10 +148,10 @@ public class ManaPool implements Serializable {
|
|||
if (ability.getSourceId().equals(mana.getSourceId())
|
||||
|| !(mana.getSourceObject() instanceof Spell)
|
||||
|| ((Spell) mana.getSourceObject())
|
||||
.getAbilities(game)
|
||||
.stream()
|
||||
.flatMap(a -> a.getAllEffects().stream())
|
||||
.anyMatch(ManaEffect.class::isInstance)) {
|
||||
.getAbilities(game)
|
||||
.stream()
|
||||
.flatMap(a -> a.getAllEffects().stream())
|
||||
.anyMatch(ManaEffect.class::isInstance)) {
|
||||
continue; // if any of the above cases, not an alt mana payment ability, thus excluded by filter
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +254,28 @@ public class ManaPool implements Serializable {
|
|||
manaItems.clear();
|
||||
}
|
||||
|
||||
public boolean canLostManaOnEmpty() {
|
||||
for (ManaPoolItem item : manaItems) {
|
||||
for (ManaType manaType : ManaType.values()) {
|
||||
if (item.get(manaType) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (doNotEmptyManaTypes.contains(manaType)) {
|
||||
continue;
|
||||
}
|
||||
if (manaBecomesBlack) {
|
||||
continue;
|
||||
}
|
||||
if (manaBecomesColorless) {
|
||||
continue;
|
||||
}
|
||||
// found real mana to empty
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int emptyPool(Game game) {
|
||||
int total = 0;
|
||||
Iterator<ManaPoolItem> it = manaItems.iterator();
|
||||
|
|
|
|||
|
|
@ -46,7 +46,15 @@ import java.util.UUID;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* Warning, if you add new choose dialogs then must implement it for:
|
||||
* - PlayerImpl (only if it use another default dialogs inside)
|
||||
* - HumanPlayer (support client-server in human games)
|
||||
* - ComputerPlayer (support AI in computer games)
|
||||
* - StubPlayer (temp)
|
||||
* - ComputerPlayerControllableProxy (support control of one player type over another player type)
|
||||
* - TestPlayer (support unit tests)
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public interface Player extends MageItem, Copyable<Player> {
|
||||
|
||||
|
|
@ -366,7 +374,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
boolean isGameUnderControl();
|
||||
|
||||
/**
|
||||
* Returns false in case you don't control the game.
|
||||
* False in case you don't control the game.
|
||||
* <p>
|
||||
* Note: For effects like "You control target player during that player's
|
||||
* next turn".
|
||||
|
|
@ -1212,8 +1220,6 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
*/
|
||||
boolean addTargets(Ability ability, Game game);
|
||||
|
||||
String getHistory();
|
||||
|
||||
boolean hasDesignation(DesignationType designationName);
|
||||
|
||||
void addDesignation(Designation designation);
|
||||
|
|
@ -1253,4 +1259,8 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
* so that's method helps to find real player that used by a game (in most use cases it's a PlayerImpl)
|
||||
*/
|
||||
Player getRealPlayer();
|
||||
|
||||
default Player prepareControllableProxy(Player playerUnderControl) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,11 @@
|
|||
package mage.target.common;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.Zone;
|
||||
import mage.filter.Filter;
|
||||
import mage.filter.common.FilterCreatureOrPlayer;
|
||||
import mage.filter.common.FilterCreaturePermanent;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetImpl;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class TargetCreatureOrPlayer extends TargetImpl {
|
||||
|
||||
protected FilterCreatureOrPlayer filter;
|
||||
public class TargetCreatureOrPlayer extends TargetPermanentOrPlayer {
|
||||
|
||||
public TargetCreatureOrPlayer() {
|
||||
this(1, 1, new FilterCreatureOrPlayer());
|
||||
|
|
@ -31,178 +16,11 @@ public class TargetCreatureOrPlayer extends TargetImpl {
|
|||
}
|
||||
|
||||
public TargetCreatureOrPlayer(int minNumTargets, int maxNumTargets, FilterCreatureOrPlayer filter) {
|
||||
this.minNumberOfTargets = minNumTargets;
|
||||
this.maxNumberOfTargets = maxNumTargets;
|
||||
this.zone = Zone.ALL;
|
||||
this.filter = filter;
|
||||
this.targetName = filter.getMessage();
|
||||
super(minNumTargets, maxNumTargets, filter, false);
|
||||
}
|
||||
|
||||
protected TargetCreatureOrPlayer(final TargetCreatureOrPlayer target) {
|
||||
super(target);
|
||||
this.filter = target.filter.copy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(UUID id, Game game) {
|
||||
Permanent permanent = game.getPermanent(id);
|
||||
if (permanent != null) {
|
||||
return filter.match(permanent, game);
|
||||
}
|
||||
Player player = game.getPlayer(id);
|
||||
return filter.match(player, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(UUID id, Ability source, Game game) {
|
||||
return canTarget(source.getControllerId(), id, source, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) {
|
||||
Permanent permanent = game.getPermanent(id);
|
||||
Player player = game.getPlayer(id);
|
||||
|
||||
if (source != null) {
|
||||
MageObject targetSource = game.getObject(source);
|
||||
if (permanent != null) {
|
||||
return permanent.canBeTargetedBy(targetSource, source.getControllerId(), source, game) && filter.match(permanent, source.getControllerId(), source, game);
|
||||
}
|
||||
if (player != null) {
|
||||
return player.canBeTargetedBy(targetSource, source.getControllerId(), source, game) && filter.match(player, game);
|
||||
}
|
||||
}
|
||||
|
||||
if (permanent != null) {
|
||||
return filter.match(permanent, game);
|
||||
}
|
||||
return filter.match(player, game);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are enough {@link Permanent} or {@link Player} that can
|
||||
* be chosen. Should only be used for Ability targets since this checks for
|
||||
* protection, shroud etc.
|
||||
*
|
||||
* @param sourceControllerId - controller of the target event source
|
||||
* @param source
|
||||
* @param game
|
||||
* @return - true if enough valid {@link Permanent} or {@link Player} exist
|
||||
*/
|
||||
@Override
|
||||
public boolean canChoose(UUID sourceControllerId, Ability source, Game game) {
|
||||
int count = 0;
|
||||
MageObject targetSource = game.getObject(source);
|
||||
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(player, game)) {
|
||||
count++;
|
||||
if (count >= this.minNumberOfTargets) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
|
||||
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game) && filter.match(permanent, sourceControllerId, source, game)) {
|
||||
count++;
|
||||
if (count >= this.minNumberOfTargets) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are enough {@link Permanent} or {@link Player} that can
|
||||
* be selected. Should not be used for Ability targets since this does not
|
||||
* check for protection, shroud etc.
|
||||
*
|
||||
* @param sourceControllerId - controller of the select event
|
||||
* @param game
|
||||
* @return - true if enough valid {@link Permanent} or {@link Player} exist
|
||||
*/
|
||||
@Override
|
||||
public boolean canChoose(UUID sourceControllerId, Game game) {
|
||||
int count = 0;
|
||||
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (filter.match(player, game)) {
|
||||
count++;
|
||||
if (count >= this.minNumberOfTargets) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
|
||||
if (filter.match(permanent, sourceControllerId, null, game)) {
|
||||
count++;
|
||||
if (count >= this.minNumberOfTargets) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
|
||||
Set<UUID> possibleTargets = new HashSet<>();
|
||||
MageObject targetSource = game.getObject(source);
|
||||
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null
|
||||
&& player.canBeTargetedBy(targetSource, sourceControllerId, source, game)
|
||||
&& filter.getPlayerFilter().match(player, sourceControllerId, source, game)) {
|
||||
possibleTargets.add(playerId);
|
||||
}
|
||||
}
|
||||
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
|
||||
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, source, game)
|
||||
&& filter.getCreatureFilter().match(permanent, sourceControllerId, source, game)) {
|
||||
possibleTargets.add(permanent.getId());
|
||||
}
|
||||
}
|
||||
return possibleTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
|
||||
Set<UUID> possibleTargets = new HashSet<>();
|
||||
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && filter.getPlayerFilter().match(player, game)) {
|
||||
possibleTargets.add(playerId);
|
||||
}
|
||||
}
|
||||
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getCreatureFilter(), sourceControllerId, game)) {
|
||||
if (filter.getCreatureFilter().match(permanent, sourceControllerId, null, game)) {
|
||||
possibleTargets.add(permanent.getId());
|
||||
}
|
||||
}
|
||||
return possibleTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTargetedName(Game game) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (UUID targetId : getTargets()) {
|
||||
Permanent permanent = game.getPermanent(targetId);
|
||||
if (permanent != null) {
|
||||
sb.append(permanent.getLogName()).append(' ');
|
||||
} else {
|
||||
Player player = game.getPlayer(targetId);
|
||||
if (player != null) {
|
||||
sb.append(player.getLogName()).append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -210,7 +28,4 @@ public class TargetCreatureOrPlayer extends TargetImpl {
|
|||
return new TargetCreatureOrPlayer(this);
|
||||
}
|
||||
|
||||
public FilterCreaturePermanent getFilterCreature() {
|
||||
return filter.getCreatureFilter().copy();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -160,10 +160,6 @@ public final class GameLog {
|
|||
return "<font color='" + LOG_COLOR_PLAYER_CONFIRM + "'>" + name + "</font>";
|
||||
}
|
||||
|
||||
public static String getSmallSecondLineText(String text) {
|
||||
return "<div style='font-size:11pt'>" + text + "</div>";
|
||||
}
|
||||
|
||||
private static String getColorName(ObjectColor objectColor) {
|
||||
if (objectColor.isMulticolored()) {
|
||||
return LOG_COLOR_MULTI;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue