Merge branch 'master' into rework/continuous-effects-layers

# Conflicts:
#	Mage.Sets/src/mage/cards/a/AettirAndPriwen.java
#	Mage.Sets/src/mage/cards/b/BlueDragon.java
#	Mage.Sets/src/mage/cards/k/KondasBanner.java
#	Mage.Sets/src/mage/cards/t/ThranWeaponry.java
#	Mage.Sets/src/mage/cards/t/TideShaper.java
This commit is contained in:
jmlundeen 2025-07-09 08:59:50 -05:00
commit e64a10bf10
2767 changed files with 25528 additions and 26335 deletions

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ syntax: glob
*.log.* *.log.*
*/gamelogs */gamelogs
*/gamelogsJson */gamelogsJson
*/gamesHistory
# Mage.Client # Mage.Client
Mage.Client/plugins/images Mage.Client/plugins/images

View file

@ -8,7 +8,7 @@ git:
before_install: before_install:
- echo "MAVEN_OPTS='-Xmx2g'" > ~/.mavenrc - echo "MAVEN_OPTS='-Xmx2g'" > ~/.mavenrc
script: script:
- mvn test -B -Dlog4j.configuration=file:${TRAVIS_BUILD_DIR}/.travis/log4j.properties - mvn test -B -Dxmage.dataCollectors.printGameLogs=false -Dlog4j.configuration=file:${TRAVIS_BUILD_DIR}/.travis/log4j.properties
cache: cache:
directories: directories:
- $HOME/.m2 - $HOME/.m2

View file

@ -87,14 +87,6 @@
<artifactId>jetlang</artifactId> <artifactId>jetlang</artifactId>
<version>0.2.23</version> <version>0.2.23</version>
</dependency> </dependency>
<dependency>
<!-- amazon s3 cloud lib to upload game logs from experimental client -->
<!-- TODO: feature must be removed as unused or implemented for all -->
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.78</version>
</dependency>
<dependency> <dependency>
<!-- GUI lib TODO: unused and can be deleted? --> <!-- GUI lib TODO: unused and can be deleted? -->
<groupId>com.jgoodies</groupId> <groupId>com.jgoodies</groupId>

View file

@ -63,6 +63,9 @@ public final class Constants {
public static final String RESOURCE_SYMBOL_FOLDER_SVG = "svg"; public static final String RESOURCE_SYMBOL_FOLDER_SVG = "svg";
public static final String RESOURCE_SYMBOL_FOLDER_PNG = "png"; public static final String RESOURCE_SYMBOL_FOLDER_PNG = "png";
// download rarity icons to large folder by default
public static final String RESOURCE_PATH_SYMBOLS_RARITY_DEFAULT_PATH = RESOURCE_PATH_SYMBOLS + File.separator + RESOURCE_SYMBOL_FOLDER_LARGE;
public enum ResourceSymbolSize { public enum ResourceSymbolSize {
SMALL, // TODO: delete SMALL, MEDIUM and LARGE as outdated (svg or generated png works fine) SMALL, // TODO: delete SMALL, MEDIUM and LARGE as outdated (svg or generated png works fine)
MEDIUM, MEDIUM,
@ -74,12 +77,12 @@ public final class Constants {
// resources - sets // resources - sets
public static final String RESOURCE_PATH_SETS = File.separator + "sets"; public static final String RESOURCE_PATH_SETS = File.separator + "sets";
public static final String RESOURCE_SET_FOLDER_SMALL = "small"; public static final String RESOURCE_SET_FOLDER_SMALL = "small";
public static final String RESOURCE_SET_FOLDER_MEDIUM = ""; // empty, medium images laydown in "sets" folder, TODO: delete that and auto gen, use png for html, not gif public static final String RESOURCE_SET_FOLDER_LARGE = "large";
public static final String RESOURCE_SET_FOLDER_SVG = "svg"; public static final String RESOURCE_SET_FOLDER_SVG = "svg";
public enum ResourceSetSize { public enum ResourceSetSize {
SMALL, SMALL,
MEDIUM, LARGE,
SVG SVG
} }

View file

@ -44,7 +44,7 @@ public class DeckGeneratorPool {
// List of cards so far in the deck // List of cards so far in the deck
private final List<Card> deckCards = new ArrayList<>(); private final List<Card> deckCards = new ArrayList<>();
// List of reserve cards found to fix up undersized decks // List of reserve cards found to fix up undersized decks
private final List<Card> reserveSpells = new ArrayList<>(); private final Map<String, Card> reserveSpells = new HashMap<>();
private final Deck deck; private final Deck deck;
/** /**
@ -170,12 +170,13 @@ public class DeckGeneratorPool {
* @param card the card to add. * @param card the card to add.
*/ */
public void addCard(Card card) { public void addCard(Card card) {
Object cnt = cardCounts.get((card.getName())); int count = cardCounts.getOrDefault(card.getName(), 0);
if (cnt == null) cardCounts.put(card.getName(), count + 1);
cardCounts.put(card.getName(), 0);
int existingCount = cardCounts.get((card.getName()));
cardCounts.put(card.getName(), existingCount + 1);
deckCards.add(card); deckCards.add(card);
if (deckCards.stream().distinct().collect(Collectors.toList()).size() != deckCards.size()) {
System.out.println("wtf " + card.getName());
}
} }
public void clearCards(boolean isClearReserve) { public void clearCards(boolean isClearReserve) {
@ -198,8 +199,8 @@ public class DeckGeneratorPool {
// Only cards with CMC < 7 and don't already exist in the deck // Only cards with CMC < 7 and don't already exist in the deck
// can be added to our reserve pool as not to overwhelm the curve // can be added to our reserve pool as not to overwhelm the curve
// with high CMC cards and duplicates. // with high CMC cards and duplicates.
if (cardCMC < 7 && getCardCount(card.getName()) == 0) { if (cardCMC < 7 && getCardCount(card.getName()) == 0 && !this.reserveSpells.containsKey(card.getName())) {
this.reserveSpells.add(card); this.reserveSpells.put(card.getName(), card);
return true; return true;
} }
return false; return false;
@ -416,48 +417,38 @@ public class DeckGeneratorPool {
* @return a fixed list of cards for this deck. * @return a fixed list of cards for this deck.
*/ */
private List<Card> getFixedSpells() { private List<Card> getFixedSpells() {
int spellSize = deckCards.size(); int spellsSize = deckCards.size();
int nonLandSize = (deckSize - landCount); int nonLandSize = (deckSize - landCount);
// Less spells than needed // fewer spells than needed - add
if (spellSize < nonLandSize) { if (spellsSize < nonLandSize) {
int needExtraSpells = nonLandSize - spellsSize;
int spellsNeeded = nonLandSize - spellSize; List<Card> possibleSpells = new ArrayList<>(reserveSpells.values());
while (needExtraSpells > 0) {
// If we haven't got enough spells in reserve to fulfil the amount we need, skip adding any. Card card = RandomUtil.randomFromCollection(possibleSpells);
if (reserveSpells.size() >= spellsNeeded) { if (card == null) {
break;
List<Card> spellsToAdd = new ArrayList<>(spellsNeeded);
// Initial reservoir
for (int i = 0; i < spellsNeeded; i++)
spellsToAdd.add(reserveSpells.get(i));
for (int i = spellsNeeded + 1; i < reserveSpells.size() - 1; i++) {
int j = RandomUtil.nextInt(i);
Card randomCard = reserveSpells.get(j);
if (isValidSpellCard(randomCard) && j < spellsToAdd.size()) {
spellsToAdd.set(j, randomCard);
}
} }
// Add randomly selected spells needed if (isValidSpellCard(card)) {
deckCards.addAll(spellsToAdd); needExtraSpells--;
deckCards.add(card);
}
possibleSpells.remove(card);
} }
} }
// More spells than needed // more spells than needed - remove
else if (spellSize > (deckSize - landCount)) { if (spellsSize > nonLandSize) {
int spellsRemoved = (spellSize) - (deckSize - landCount); int removeCount = spellsSize - nonLandSize;
for (int i = 0; i < spellsRemoved; ++i) { for (int i = 0; i < removeCount; ++i) {
deckCards.remove(RandomUtil.nextInt(deckCards.size())); deckCards.remove(RandomUtil.randomFromCollection(deckCards));
} }
} }
// Check we have exactly the right amount of cards for a deck.
if (deckCards.size() != nonLandSize) { if (deckCards.size() != nonLandSize) {
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors"); logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors (wrong non land cards amount)");
} }
// Return the fixed amount
return deckCards; return deckCards;
} }
@ -619,48 +610,75 @@ public class DeckGeneratorPool {
if (needCommandersCount > 0 && !genPool.cardCounts.isEmpty()) { if (needCommandersCount > 0 && !genPool.cardCounts.isEmpty()) {
throw new IllegalArgumentException("Wrong code usage: generateSpells with creatures and commanders must be called as first"); throw new IllegalArgumentException("Wrong code usage: generateSpells with creatures and commanders must be called as first");
} }
List<CardInfo> cardPool = CardRepository.instance.findCards(criteria); List<Card> cardsPool = CardRepository.instance.findCards(criteria).stream()
.map(CardInfo::createMockCard)
.filter(genPool::isValidSpellCard)
.collect(Collectors.toList());
List<Card> commandersPool = cardsPool.stream()
.filter(genPool::isValidCommander)
.collect(Collectors.toList());
List<DeckGeneratorCMC.CMC> deckCMCs = genPool.getCMCsForSpellCount(needCardsCount); List<DeckGeneratorCMC.CMC> deckCMCs = genPool.getCMCsForSpellCount(needCardsCount);
int count = 0; int usedCardsCount = 0;
int validCommanders = 0; int validCommanders = 0;
int reservesAdded = 0; int reservesAdded = 0;
if (cardPool.size() > 0 && cardPool.size() >= needCardsCount) { if (cardsPool.size() > 0 && cardsPool.size() >= needCardsCount) {
int tries = 0; int tries = 0;
List<Card> possibleCards = new ArrayList<>(cardsPool);
List<Card> possibleCommanders = new ArrayList<>(commandersPool);
while (true) { while (true) {
tries++; tries++;
// can't finish deck, stop and use reserved cards later // can't finish deck, stop and use reserved cards later
if (tries > DeckGenerator.MAX_TRIES) { if (tries > DeckGenerator.MAX_TRIES) {
logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors"); logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors (max tries exceeded)");
break; break;
} }
// can finish deck - but make sure it has commander // can finish deck - but make sure it has commander
if (count >= needCardsCount) { if (usedCardsCount >= needCardsCount) {
if (validCommanders < needCommandersCount) { if (validCommanders < needCommandersCount) {
// reset deck search from scratch (except reserved cards) // reset deck search from scratch (except reserved cards)
count = 0; usedCardsCount = 0;
validCommanders = 0; validCommanders = 0;
deckCMCs = genPool.getCMCsForSpellCount(needCardsCount); deckCMCs = genPool.getCMCsForSpellCount(needCardsCount);
genPool.clearCards(false); genPool.clearCards(true);
possibleCards = new ArrayList<>(cardsPool);
possibleCommanders = new ArrayList<>(commandersPool);
continue; continue;
} }
break; break;
} }
Card card = cardPool.get(RandomUtil.nextInt(cardPool.size())).createMockCard(); if (possibleCards.isEmpty()) {
throw new IllegalStateException("Not enough cards to generate deck (possible cards is empty)");
}
// choose commander first
Card card = null;
if (validCommanders < needCommandersCount && !possibleCommanders.isEmpty()) {
card = RandomUtil.randomFromCollection(possibleCommanders);
}
// choose other cards after commander
if (card == null) {
card = RandomUtil.randomFromCollection(possibleCards);
}
if (!genPool.isValidSpellCard(card)) { if (!genPool.isValidSpellCard(card)) {
possibleCards.remove(card);
possibleCommanders.remove(card);
continue; continue;
} }
int cardCMC = card.getManaValue(); int cardCMC = card.getManaValue();
for (DeckGeneratorCMC.CMC deckCMC : deckCMCs) { for (DeckGeneratorCMC.CMC deckCMC : deckCMCs) {
if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) { if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) {
int currentAmount = deckCMC.getAmount(); int needAmount = deckCMC.getAmount();
if (currentAmount > 0) { if (needAmount > 0) {
deckCMC.setAmount(currentAmount - 1); deckCMC.setAmount(needAmount - 1);
genPool.addCard(card.copy()); genPool.addCard(card.copy());
count++; usedCardsCount++;
// make sure it has compatible commanders // make sure it has compatible commanders
if (genPool.isValidCommander(card)) { if (genPool.isValidCommander(card)) {
validCommanders++; validCommanders++;
@ -674,7 +692,7 @@ public class DeckGeneratorPool {
} }
} }
} else { } else {
throw new IllegalStateException("Not enough cards to generate deck."); throw new IllegalStateException("Not enough cards to generate deck (cards pool too small)");
} }
} }

View file

@ -19,7 +19,6 @@ import mage.client.dialog.AddLandDialog;
import mage.client.dialog.PreferencesDialog; import mage.client.dialog.PreferencesDialog;
import mage.client.plugins.impl.Plugins; import mage.client.plugins.impl.Plugins;
import mage.client.util.Event; import mage.client.util.Event;
import mage.client.util.GUISizeHelper;
import mage.client.util.Listener; import mage.client.util.Listener;
import mage.client.util.audio.AudioManager; import mage.client.util.audio.AudioManager;
import mage.components.CardInfoPane; import mage.components.CardInfoPane;
@ -31,7 +30,6 @@ import mage.util.XmageThreadFactory;
import mage.view.CardView; import mage.view.CardView;
import mage.view.SimpleCardView; import mage.view.SimpleCardView;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.mage.card.arcane.ManaSymbols;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border; import javax.swing.border.Border;
@ -534,7 +532,8 @@ public class DeckEditorPanel extends javax.swing.JPanel {
} }
} }
}); });
refreshDeck(true);
refreshDeck(true, false);
// auto-import dropped files from OS // auto-import dropped files from OS
if (mode == DeckEditorMode.FREE_BUILDING) { if (mode == DeckEditorMode.FREE_BUILDING) {
@ -673,20 +672,28 @@ public class DeckEditorPanel extends javax.swing.JPanel {
private void refreshDeck() { private void refreshDeck() {
if (this.isVisible()) { // TODO: test auto-close deck with active lands dialog, e.g. on timeout if (this.isVisible()) { // TODO: test auto-close deck with active lands dialog, e.g. on timeout
refreshDeck(false); refreshDeck(false, false);
} }
} }
private void refreshDeck(boolean useLayout) { private void refreshDeck(boolean useLayout, boolean useDeckValidation) {
try { try {
setCursor(new Cursor(Cursor.WAIT_CURSOR)); MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR));
this.txtDeckName.setText(deck.getName()); this.txtDeckName.setText(deck.getName());
deckArea.loadDeck(deck, useLayout, bigCard); deckArea.loadDeck(deck, useLayout, bigCard);
if (useDeckValidation) {
validateDeck();
}
} finally { } finally {
setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); MageFrame.getDesktop().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
} }
} }
private void validateDeck() {
this.deckLegalityDisplay.setVisible(true);
this.deckLegalityDisplay.validateDeck(deck);
}
private void setTimeout(int s) { private void setTimeout(int s) {
int minute = s / 60; int minute = s / 60;
int second = s - (minute * 60); int second = s - (minute * 60);
@ -762,7 +769,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) { if (newDeck != null) {
deck = newDeck; deck = newDeck;
refreshDeck(); refreshDeck(false, true);
} }
// save last deck import folder // save last deck import folder
@ -818,7 +825,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
Deck deckToAppend = Deck.load(DeckImporter.importDeckFromFile(tempDeckPath, errorMessages, false), true, true); Deck deckToAppend = Deck.load(DeckImporter.importDeckFromFile(tempDeckPath, errorMessages, false), true, true);
processAndShowImportErrors(errorMessages); processAndShowImportErrors(errorMessages);
this.deck = Deck.append(deckToAppend, this.deck); this.deck = Deck.append(deckToAppend, this.deck);
refreshDeck(); refreshDeck(false, true);
} catch (GameException e1) { } catch (GameException e1) {
JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE);
} finally { } finally {
@ -922,7 +929,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) { if (newDeck != null) {
deck = newDeck; deck = newDeck;
refreshDeck(); refreshDeck(false, true);
return true; return true;
} }
@ -1441,7 +1448,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (newDeck != null) { if (newDeck != null) {
deck = newDeck; deck = newDeck;
refreshDeck(true); refreshDeck(true, true);
} }
// save last deck history // save last deck history
@ -1474,7 +1481,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
// in deck editor mode - clear all cards // in deck editor mode - clear all cards
deck = new Deck(); deck = new Deck();
} }
refreshDeck(); refreshDeck(false, true);
}//GEN-LAST:event_btnNewActionPerformed }//GEN-LAST:event_btnNewActionPerformed
private void btnExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnExitActionPerformed private void btnExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnExitActionPerformed
@ -1483,7 +1490,9 @@ public class DeckEditorPanel extends javax.swing.JPanel {
private void btnAddLandActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnAddLandActionPerformed private void btnAddLandActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnAddLandActionPerformed
AddLandDialog dialog = new AddLandDialog(); AddLandDialog dialog = new AddLandDialog();
dialog.showDialog(deck, mode, this::refreshDeck); dialog.showDialog(deck, mode, () -> {
this.refreshDeck(false, true);
});
}//GEN-LAST:event_btnAddLandActionPerformed }//GEN-LAST:event_btnAddLandActionPerformed
private void btnGenDeckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGenDeckActionPerformed private void btnGenDeckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGenDeckActionPerformed
@ -1501,7 +1510,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
} finally { } finally {
MageFrame.getDesktop().setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); MageFrame.getDesktop().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
} }
refreshDeck(); refreshDeck(false, true);
}//GEN-LAST:event_btnGenDeckActionPerformed }//GEN-LAST:event_btnGenDeckActionPerformed
private void btnSubmitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitActionPerformed private void btnSubmitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitActionPerformed
@ -1548,8 +1557,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
}//GEN-LAST:event_btnExportActionPerformed }//GEN-LAST:event_btnExportActionPerformed
private void btnLegalityActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnLegalityActionPerformed private void btnLegalityActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnLegalityActionPerformed
this.deckLegalityDisplay.setVisible(true); validateDeck();
this.deckLegalityDisplay.validateDeck(deck);
}//GEN-LAST:event_btnLegalityActionPerformed }//GEN-LAST:event_btnLegalityActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables

View file

@ -75,7 +75,7 @@ public class DraftPickLogger {
private void appendToDraftLog(String data) { private void appendToDraftLog(String data) {
if (logging) { if (logging) {
try { try {
Files.write(logPath, data.getBytes(), StandardOpenOption.APPEND); Files.write(logPath, data.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.error(null, ex); LOGGER.error(null, ex);
} }

View file

@ -257,10 +257,7 @@ public class CallbackClientImpl implements CallbackClient {
if (panel != null) { if (panel != null) {
Session session = SessionHandler.getSession(); Session session = SessionHandler.getSession();
if (session.isJsonLogActive()) { if (session.isJsonLogActive()) {
UUID gameId = callback.getObjectId();
appendJsonEvent("GAME_OVER", callback.getObjectId(), message); appendJsonEvent("GAME_OVER", callback.getObjectId(), message);
String logFileName = "game-" + gameId + ".json";
S3Uploader.upload(logFileName, gameId.toString());
} }
panel.endMessage(callback.getMessageId(), message.getGameView(), message.getOptions(), message.getMessage()); panel.endMessage(callback.getMessageId(), message.getGameView(), message.getOptions(), message.getMessage());
} }

View file

@ -1,46 +0,0 @@
package mage.client.remote;
import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import org.apache.log4j.Logger;
import java.io.File;
public class S3Uploader {
private static final Logger logger = Logger.getLogger(S3Uploader.class);
public static Boolean upload(String filePath, String keyName) throws Exception {
String existingBucketName = System.getenv("S3_BUCKET") != null ? System.getenv("S3_BUCKET")
: "xmage-game-logs-dev";
String accessKeyId = System.getenv("AWS_ACCESS_ID");
String secretKeyId = System.getenv("AWS_SECRET_KEY");
if (accessKeyId == null || accessKeyId.isEmpty()
|| secretKeyId == null || secretKeyId.isEmpty()
|| existingBucketName.isEmpty()) {
logger.info("Aborting json log sync.");
return false;
}
String path = new File("./" + filePath).getCanonicalPath();
logger.info("Syncing " + path + " to bucket: " + existingBucketName + " with AWS Access Id: " + accessKeyId);
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKeyId, secretKeyId);
TransferManager tm = new TransferManager(awsCreds);
Upload upload = tm.upload(existingBucketName, "/game/" + keyName + ".json", new File(path));
try {
upload.waitForUploadResult();
logger.info("Sync Complete For " + path + " to bucket: " + existingBucketName + " with AWS Access Id: " + accessKeyId);
new File(path);
return true;
} catch (AmazonClientException amazonClientException) {
logger.fatal("Unable to upload file, upload was aborted.", amazonClientException);
return false;
}
}
}

View file

@ -896,7 +896,7 @@ public class TablesPanel extends javax.swing.JPanel {
formatFilterList.add(RowFilter.regexFilter("^Limited", TablesTableModel.COLUMN_DECK_TYPE)); formatFilterList.add(RowFilter.regexFilter("^Limited", TablesTableModel.COLUMN_DECK_TYPE));
} }
if (btnFormatOther.isSelected()) { if (btnFormatOther.isSelected()) {
formatFilterList.add(RowFilter.regexFilter("^Momir Basic|^Constructed - Pauper|^Constructed - Frontier|^Constructed - Extended|^Constructed - Eternal|^Constructed - Historical|^Constructed - Super|^Constructed - Freeform|^Australian Highlander|^European Highlander|^Canadian Highlander|^Constructed - Old|^Constructed - Historic", TablesTableModel.COLUMN_DECK_TYPE)); formatFilterList.add(RowFilter.regexFilter("^Momir Basic|^Constructed - Pauper|^Constructed - Frontier|^Constructed - Extended|^Constructed - Eternal|^Constructed - Historical|^Constructed - Super|^Constructed - Freeform|^Constructed - Freeform Unlimited|^Australian Highlander|^European Highlander|^Canadian Highlander|^Constructed - Old|^Constructed - Historic", TablesTableModel.COLUMN_DECK_TYPE));
} }
// skill // skill

View file

@ -89,11 +89,6 @@ public final class ManaSymbols {
public static void loadImages() { public static void loadImages() {
logger.info("Symbols: loading..."); logger.info("Symbols: loading...");
// TODO: delete files rename jpg->gif (it was for backward compatibility for one of the old version?)
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.SMALL));
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.MEDIUM));
renameSymbols(getResourceSymbolsPath(ResourceSymbolSize.LARGE));
//renameSymbols(getSymbolsPath(ResourceSymbolSize.SVG)); // not need
// TODO: remove medium sets files to "medium" folder like symbols above? // TODO: remove medium sets files to "medium" folder like symbols above?
// prepare svg's css settings // prepare svg's css settings
@ -145,9 +140,9 @@ public final class ManaSymbols {
Map<Rarity, Image> rarityImages = new EnumMap<>(Rarity.class); Map<Rarity, Image> rarityImages = new EnumMap<>(Rarity.class);
setImages.put(set, rarityImages); setImages.put(set, rarityImages);
// load medium size // load large size
for (Rarity rarityCode : codes) { for (Rarity rarityCode : codes) {
File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + rarityCode.getCode() + ".jpg"); File file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + rarityCode.getCode() + ".png");
try { try {
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
int width = image.getWidth(null); int width = image.getWidth(null);
@ -167,7 +162,7 @@ public final class ManaSymbols {
// generate small size // generate small size
try { try {
File file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM)); File file = new File(getResourceSetsPath(ResourceSetSize.LARGE));
if (!file.exists()) { if (!file.exists()) {
file.mkdirs(); file.mkdirs();
} }
@ -175,11 +170,11 @@ public final class ManaSymbols {
for (Rarity code : codes) { for (Rarity code : codes) {
File newFile = new File(pathRoot + '-' + code + ".png"); File newFile = new File(pathRoot + '-' + code + ".png");
if (!(MageFrame.isSkipSmallSymbolGenerationForExisting() && newFile.exists())) {// skip if option enabled and file already exists if (!(MageFrame.isSkipSmallSymbolGenerationForExisting() && newFile.exists())) {// skip if option enabled and file already exists
file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".png"); file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + code + ".png");
if (file.exists()) { if (file.exists()) {
continue; continue;
} }
file = new File(getResourceSetsPath(ResourceSetSize.MEDIUM) + set + '-' + code + ".jpg"); file = new File(getResourceSetsPath(ResourceSetSize.LARGE) + set + '-' + code + ".png");
Image image = UI.getImageIcon(file.getAbsolutePath()).getImage(); Image image = UI.getImageIcon(file.getAbsolutePath()).getImage();
try { try {
int width = image.getWidth(null); int width = image.getWidth(null);
@ -239,7 +234,7 @@ public final class ManaSymbols {
} }
} }
private static File getSymbolFileNameAsGIF(String symbol, int size) { private static File getSymbolFileNameAsPNG(String symbol, int size) {
ResourceSymbolSize needSize = null; ResourceSymbolSize needSize = null;
if (size <= 15) { if (size <= 15) {
@ -250,15 +245,15 @@ public final class ManaSymbols {
needSize = ResourceSymbolSize.LARGE; needSize = ResourceSymbolSize.LARGE;
} }
return new File(getResourceSymbolsPath(needSize) + symbol + ".gif"); return new File(getResourceSymbolsPath(needSize) + symbol + ".png");
} }
private static BufferedImage loadSymbolAsGIF(String symbol, int resizeToWidth, int resizeToHeight) { private static BufferedImage loadSymbolAsPNG(String symbol, int resizeToWidth, int resizeToHeight) {
File file = getSymbolFileNameAsGIF(symbol, resizeToWidth); File file = getSymbolFileNameAsPNG(symbol, resizeToWidth);
return loadSymbolAsGIF(file, resizeToWidth, resizeToHeight); return loadSymbolAsPNG(file, resizeToWidth, resizeToHeight);
} }
private static BufferedImage loadSymbolAsGIF(File sourceFile, int resizeToWidth, int resizeToHeight) { private static BufferedImage loadSymbolAsPNG(File sourceFile, int resizeToWidth, int resizeToHeight) {
BufferedImage image = null; BufferedImage image = null;
@ -315,9 +310,9 @@ public final class ManaSymbols {
// gif (if svg fails) // gif (if svg fails)
if (image == null) { if (image == null) {
file = getSymbolFileNameAsGIF(symbol, size); file = getSymbolFileNameAsPNG(symbol, size);
if (file.exists()) { if (file.exists()) {
image = loadSymbolAsGIF(file, size, size); image = loadSymbolAsPNG(file, size, size);
} }
} }
if (image == null) { if (image == null) {
@ -352,29 +347,6 @@ public final class ManaSymbols {
return errorInfo.isEmpty(); return errorInfo.isEmpty();
} }
private static void renameSymbols(String path) {
File file = new File(path);
if (!file.exists()) {
return;
}
final PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.jpg");
try {
Files.walkFileTree(Paths.get(path), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (matcher.matches(file)) {
Path gifPath = file.resolveSibling(file.getFileName().toString().replaceAll("\\.jpg$", ".gif"));
Files.move(file, gifPath, StandardCopyOption.REPLACE_EXISTING);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
logger.error("Couldn't rename mana symbols on " + path, e);
}
}
private static String getResourceSymbolsPath(ResourceSymbolSize needSize) { private static String getResourceSymbolsPath(ResourceSymbolSize needSize) {
// return real path to symbols (default or user defined) // return real path to symbols (default or user defined)
@ -420,8 +392,8 @@ public final class ManaSymbols {
case SMALL: case SMALL:
path = path + Constants.RESOURCE_SET_FOLDER_SMALL; path = path + Constants.RESOURCE_SET_FOLDER_SMALL;
break; break;
case MEDIUM: case LARGE:
path = path + Constants.RESOURCE_SET_FOLDER_MEDIUM; path = path + Constants.RESOURCE_SET_FOLDER_LARGE;
break; break;
case SVG: case SVG:
path = path + Constants.RESOURCE_SET_FOLDER_SVG; path = path + Constants.RESOURCE_SET_FOLDER_SVG;

View file

@ -663,7 +663,9 @@ public class CardPluginImpl implements CardPlugin {
// mana symbols (low quality) // mana symbols (low quality)
jobs = new GathererSymbols(); jobs = new GathererSymbols();
for (DownloadJob job : jobs) { for (DownloadJob job : jobs) {
downloader.add(job); // TODO: gatherer removed mana symbols icons after 2025, see https://github.com/magefree/mage/issues/13797
// remove GathererSymbols code after few releases as unused (2025.06.28)
// downloader.add(job);
} }
// set code symbols (low quality) // set code symbols (low quality)

View file

@ -46,8 +46,10 @@ public class Downloader extends AbstractLaternaBean {
public Downloader() { public Downloader() {
// prepare 10 threads and start to waiting new download jobs from queue // prepare 10 threads and start to waiting new download jobs from queue
// TODO: gatherer website has download rate limits, so limit max threads as temporary solution
int maxThreads = 3;
PoolFiberFactory f = new PoolFiberFactory(pool); PoolFiberFactory f = new PoolFiberFactory(pool);
for (int i = 0, numThreads = 10; i < numThreads; i++) { for (int i = 0, numThreads = maxThreads; i < numThreads; i++) {
Fiber fiber = f.create(); Fiber fiber = f.create();
fiber.start(); fiber.start();
fibers.add(fiber); fibers.add(fiber);

View file

@ -16,7 +16,9 @@ import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
/** /**
* Download: set code symbols download from wizards web size * Download: set code symbols download from wizards web size
* <p> * <p>
* Warning, it's outdated source with low quality images. TODO: must migrate to scryfall like mana icons * Warning, it's outdated source with low quality images.
* TODO: must migrate to scryfall like mana icons,
* see https://github.com/magefree/mage/issues/13261
*/ */
public class GathererSets implements Iterable<DownloadJob> { public class GathererSets implements Iterable<DownloadJob> {
@ -41,9 +43,10 @@ public class GathererSets implements Iterable<DownloadJob> {
private static File outDir; private static File outDir;
private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic eralies 14 days before release date private static final int DAYS_BEFORE_RELEASE_TO_DOWNLOAD = +14; // Try to load the symbolsBasic 14 days before release date
private static final Logger logger = Logger.getLogger(GathererSets.class); private static final Logger logger = Logger.getLogger(GathererSets.class);
// TODO: find all possible sets from ExpansionRepository instead custom
private static final String[] symbolsBasic = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA", private static final String[] symbolsBasic = {"10E", "9ED", "8ED", "7ED", "6ED", "5ED", "4ED", "3ED", "2ED", "LEB", "LEA",
"HOP", "HOP",
"ARN", "ATQ", "LEG", "DRK", "FEM", "HML", "ARN", "ATQ", "LEG", "DRK", "FEM", "HML",
@ -61,14 +64,16 @@ public class GathererSets implements Iterable<DownloadJob> {
"TSP", "TSB", "PLC", "FUT", "TSP", "TSB", "PLC", "FUT",
"LRW", "MOR", "LRW", "MOR",
"SHM", "EVE", "SHM", "EVE",
"MED", "ME2", "ME3", "ME4", "ME2", "ME3", "ME4",
"POR", "P02", "PTK", "POR", "P02", "PTK",
"ARC", "DD3EVG", "ARC", "DD3EVG",
"W16", "W17", "W16", "W17",
// "PALP" -- Gatherer does not have the set Asia Pacific Land Program // "PALP" -- Gatherer does not have the set Asia Pacific Land Program
// "ATH" -- has cards from many sets, symbol does not exist on gatherer // "ATH" -- has cards from many sets, symbol does not exist on gatherer
// "CP", "DPA", "PELP", "PGPX", "PGRU", "H17", "JR", "SWS", // need to fix // "CP", "DPA", "PELP", "PGPX", "PGRU", "H17", "JR", "SWS", // need to fix
"H09", "PD2", "PD3", "UNH", "CM1", "V11", "A25", "UST", "IMA", "DD2", "EVG", "DDC", "DDE", "DDD", "CHR", "G18", "GVL", "S00", "S99", "UGL" // ok "H09", "PD2", "PD3", "UNH", "CM1", "V11", "A25", "UST", "IMA", "DD2",
"EVG", "DDC", "DDE", "DDD", "CHR", "G18", "GVL", "S00", "S99", "UGL",
"BTD" // ok
// current testing // current testing
}; };
@ -98,21 +103,25 @@ public class GathererSets implements Iterable<DownloadJob> {
"GNT", "UMA", "GRN", "GNT", "UMA", "GRN",
"RNA", "WAR", "MH1", "RNA", "WAR", "MH1",
"M20", "M20",
"C19", "ELD", "MB1", "GN2", "J20", "THB", "UND", "C20", "IKO", "M21", "C19", "ELD", "MB1", "GN2", "THB", "UND", "C20", "IKO", "M21",
"JMP", "2XM", "ZNR", "KLR", "CMR", "KHC", "KHM", "TSR", "STX", "STA", "JMP", "2XM", "ZNR", "KLR", "CMR", "KHC", "KHM", "TSR", "STX", "STA",
"C21", "MH2", "AFR", "AFC", "J21", "MID", "MIC", "VOW", "VOC", "YMID", "C21", "MH2", "AFR", "AFC", "J21", "MID", "MIC", "VOW", "VOC", "YMID",
"NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "YDMU", "40K", "GN3", "NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "40K", "GN3",
"UNF", "BRO", "BRC", "BOT", "30A", "J22", "SCD", "DMR", "ONE", "ONC", "UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC",
"MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT", "MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT",
"WOC", "SPG", "LCI", "LCC", "REX", "PIP", "MKM", "MKC", "CLU", "OTJ", "WOC", "SPG", "LCI", "LCC", "REX", "PIP", "MKM", "MKC", "CLU", "OTJ",
"OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB" "OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB", "BLC", "DSK", "DSC",
"MB2", "FDN", "INR", "J25", "DRC", "DFT", "TDC", "TDM", "FCA", "FIC",
"FIN", "SIS", "SIR", "SLD", "AKR", "MD1", "ANB", "LTC", "BRR", "HA1",
"HA2", "HA3", "HA4", "HA5", "ZNC", "EOE", "EOC", "SPE", "TLA"
// "HHO", "ANA" -- do not exist on gatherer // "HHO", "ANA" -- do not exist on gatherer
}; };
private static final String[] symbolsOnlyMyth = { private static final String[] symbolsOnlyMyth = {
"DRB", "V09", "V10", "V12", "V13", "V14", "V15", "V16", "V17", "EXP", "MED" "DRB", "V09", "V10", "V12", "V13", "V14", "V15", "V16", "V17", "EXP", "MED", "ZNE"
// "HTR16" does not exist // "HTR16" does not exist
}; };
private static final String[] symbolsOnlySpecial = { private static final String[] symbolsOnlySpecial = {
"MPS", "MP2" "MPS", "MP2"
}; };
@ -130,6 +139,7 @@ public class GathererSets implements Iterable<DownloadJob> {
codeReplacements.put("APC", "AP"); codeReplacements.put("APC", "AP");
codeReplacements.put("ARN", "AN"); codeReplacements.put("ARN", "AN");
codeReplacements.put("ATQ", "AQ"); codeReplacements.put("ATQ", "AQ");
codeReplacements.put("BTD", "BD");
codeReplacements.put("CMA", "CM1"); codeReplacements.put("CMA", "CM1");
codeReplacements.put("CHR", "CH"); codeReplacements.put("CHR", "CH");
codeReplacements.put("DVD", "DD3_DVD"); codeReplacements.put("DVD", "DD3_DVD");
@ -165,14 +175,16 @@ public class GathererSets implements Iterable<DownloadJob> {
codeReplacements.put("UGIN", "FRF_UGIN"); codeReplacements.put("UGIN", "FRF_UGIN");
codeReplacements.put("UGL", "UG"); codeReplacements.put("UGL", "UG");
codeReplacements.put("ULG", "GU"); codeReplacements.put("ULG", "GU");
codeReplacements.put("UNF", "UNFS");
codeReplacements.put("USG", "UZ"); codeReplacements.put("USG", "UZ");
codeReplacements.put("VIS", "VI"); codeReplacements.put("VIS", "VI");
codeReplacements.put("WTH", "WL"); codeReplacements.put("WTH", "WL");
codeReplacements.put("YMID", "Y22");
codeReplacements.put("YNEO", "Y22NEO");
} }
public GathererSets() { public GathererSets() {
outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS_RARITY_DEFAULT_PATH);
outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS);
if (!outDir.exists()) { if (!outDir.exists()) {
outDir.mkdirs(); outDir.mkdirs();
@ -287,9 +299,9 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false; canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) { if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true; canDownload = true;
jobs.add(generateDownloadJob(symbol, "C", "C")); jobs.add(generateDownloadJob(symbol, "C", "common"));
jobs.add(generateDownloadJob(symbol, "U", "U")); jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
jobs.add(generateDownloadJob(symbol, "R", "R")); jobs.add(generateDownloadJob(symbol, "R", "rare"));
} }
CheckSearchResult(symbol, exp, canDownload, true, true, true, false); CheckSearchResult(symbol, exp, canDownload, true, true, true, false);
} }
@ -299,10 +311,10 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false; canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) { if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true; canDownload = true;
jobs.add(generateDownloadJob(symbol, "C", "C")); jobs.add(generateDownloadJob(symbol, "C", "common"));
jobs.add(generateDownloadJob(symbol, "U", "U")); jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
jobs.add(generateDownloadJob(symbol, "R", "R")); jobs.add(generateDownloadJob(symbol, "R", "rare"));
jobs.add(generateDownloadJob(symbol, "M", "M")); jobs.add(generateDownloadJob(symbol, "M", "mythic"));
} }
CheckSearchResult(symbol, exp, canDownload, true, true, true, true); CheckSearchResult(symbol, exp, canDownload, true, true, true, true);
} }
@ -312,7 +324,7 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false; canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) { if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true; canDownload = true;
jobs.add(generateDownloadJob(symbol, "M", "M")); jobs.add(generateDownloadJob(symbol, "M", "mythic"));
} }
CheckSearchResult(symbol, exp, canDownload, false, false, false, true); CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
} }
@ -322,7 +334,7 @@ public class GathererSets implements Iterable<DownloadJob> {
canDownload = false; canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) { if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true; canDownload = true;
jobs.add(generateDownloadJob(symbol, "M", "S")); jobs.add(generateDownloadJob(symbol, "M", "special"));
} }
CheckSearchResult(symbol, exp, canDownload, false, false, false, true); CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
} }
@ -334,11 +346,22 @@ public class GathererSets implements Iterable<DownloadJob> {
} }
private DownloadJob generateDownloadJob(String set, String rarity, String urlRarity) { private DownloadJob generateDownloadJob(String set, String rarity, String urlRarity) {
File dst = new File(outDir, set + '-' + rarity + ".jpg"); File dst = new File(outDir, set + '-' + rarity + ".png");
if (codeReplacements.containsKey(set)) { if (codeReplacements.containsKey(set)) {
set = codeReplacements.get(set); set = codeReplacements.get(set);
} }
String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; // example:
// - small: https://gatherer-static.wizards.com/set_symbols/FIN/small-common-FIN.png
// - big: https://gatherer-static.wizards.com/set_symbols/FIN/large-rare-FIN.png
String useSet = set.toUpperCase(Locale.ENGLISH);
String useSize = "large"; // allow: small, large
String url = String.format("https://gatherer-static.wizards.com/set_symbols/%s/%s-%s-%s.png",
useSet,
useSize,
urlRarity,
useSet
);
return new DownloadJob(set + '-' + rarity, url, toFile(dst), false); return new DownloadJob(set + '-' + rarity, url, toFile(dst), false);
} }
} }

View file

@ -75,7 +75,7 @@ public class GathererSymbols implements Iterable<DownloadJob> {
continue; continue;
} }
String symbol = sym.replaceAll("/", ""); String symbol = sym.replaceAll("/", "");
File dst = new File(dir, symbol + ".gif"); File dst = new File(dir, symbol + ".png");
// workaround for miss icons on Gatherer (no cards with it, so no icons) // workaround for miss icons on Gatherer (no cards with it, so no icons)
// TODO: comment and try download without workaround, keep fix for symbols with "Resource not found" error // TODO: comment and try download without workaround, keep fix for symbols with "Resource not found" error

View file

@ -588,7 +588,10 @@ public class ScryfallImageSupportCards {
add("FIN"); // Final Fantasy add("FIN"); // Final Fantasy
add("FIC"); // Final Fantasy Commander add("FIC"); // Final Fantasy Commander
add("FCA"); // Final Fantasy: Through the Ages add("FCA"); // Final Fantasy: Through the Ages
add("EOE"); // Edge of Eternities
add("EOC"); // Edge of Eternities Commander
add("SPE"); // Marvel's Spider-Man Eternal add("SPE"); // Marvel's Spider-Man Eternal
add("TLA"); // Avatar: The Last Airbender
// Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks // Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks
add("CALC"); // Custom Alchemized versions of existing cards add("CALC"); // Custom Alchemized versions of existing cards

View file

@ -352,6 +352,9 @@ public class ScryfallImageSupportTokens {
put("ELD/Rat", "https://api.scryfall.com/cards/teld/6/en?format=image"); put("ELD/Rat", "https://api.scryfall.com/cards/teld/6/en?format=image");
put("ELD/Wolf", "https://api.scryfall.com/cards/teld/14/en?format=image"); put("ELD/Wolf", "https://api.scryfall.com/cards/teld/14/en?format=image");
// UND
put("UND/Goblin", "https://api.scryfall.com/cards/tund/2?format=image");
// THB // THB
put("THB/Elemental", "https://api.scryfall.com/cards/tthb/8/en?format=image"); put("THB/Elemental", "https://api.scryfall.com/cards/tthb/8/en?format=image");
put("THB/Goat", "https://api.scryfall.com/cards/tthb/1/en?format=image"); put("THB/Goat", "https://api.scryfall.com/cards/tthb/1/en?format=image");
@ -829,12 +832,18 @@ public class ScryfallImageSupportTokens {
put("SLD/Food/3", "https://api.scryfall.com/cards/sld/2011?format=image"); put("SLD/Food/3", "https://api.scryfall.com/cards/sld/2011?format=image");
put("SLD/Food/4", "https://api.scryfall.com/cards/sld/2012?format=image"); put("SLD/Food/4", "https://api.scryfall.com/cards/sld/2012?format=image");
put("SLD/Food/5", "https://api.scryfall.com/cards/sld/2013?format=image"); put("SLD/Food/5", "https://api.scryfall.com/cards/sld/2013?format=image");
put("SLD/Food/6", "https://api.scryfall.com/cards/sld/2064?format=image");
put("SLD/Goblin", "https://api.scryfall.com/cards/sld/219?format=image"); put("SLD/Goblin", "https://api.scryfall.com/cards/sld/219?format=image");
put("SLD/Hydra", "https://api.scryfall.com/cards/sld/1334?format=image"); put("SLD/Hydra", "https://api.scryfall.com/cards/sld/1334?format=image");
put("SLD/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/sld/1018?format=image"); put("SLD/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/sld/1018?format=image");
put("SLD/Marit Lage", "https://api.scryfall.com/cards/sld/1681?format=image"); put("SLD/Marit Lage", "https://api.scryfall.com/cards/sld/1681?format=image");
put("SLD/Mechtitan", "https://api.scryfall.com/cards/sld/1969?format=image"); put("SLD/Mechtitan", "https://api.scryfall.com/cards/sld/1969?format=image");
put("SLD/Myr", "https://api.scryfall.com/cards/sld/2101?format=image");
put("SLD/Saproling", "https://api.scryfall.com/cards/sld/1139?format=image"); put("SLD/Saproling", "https://api.scryfall.com/cards/sld/1139?format=image");
put("SLD/Shapeshifter/1", "https://api.scryfall.com/cards/sld/1906?format=image");
put("SLD/Shapeshifter/2", "https://api.scryfall.com/cards/sld/1907?format=image");
put("SLD/Shapeshifter/3", "https://api.scryfall.com/cards/sld/1908?format=image");
put("SLD/Shapeshifter/4", "https://api.scryfall.com/cards/sld/1909?format=image");
put("SLD/Shrine", "https://api.scryfall.com/cards/sld/1835?format=image"); put("SLD/Shrine", "https://api.scryfall.com/cards/sld/1835?format=image");
put("SLD/Spirit/1", "https://api.scryfall.com/cards/sld/1341?format=image"); put("SLD/Spirit/1", "https://api.scryfall.com/cards/sld/1341?format=image");
put("SLD/Spirit/2", "https://api.scryfall.com/cards/sld/1852?format=image"); put("SLD/Spirit/2", "https://api.scryfall.com/cards/sld/1852?format=image");
@ -843,6 +852,8 @@ public class ScryfallImageSupportTokens {
put("SLD/Treasure/2", "https://api.scryfall.com/cards/sld/1736/en?format=image"); put("SLD/Treasure/2", "https://api.scryfall.com/cards/sld/1736/en?format=image");
put("SLD/Treasure/3", "https://api.scryfall.com/cards/sld/1507/en?format=image"); put("SLD/Treasure/3", "https://api.scryfall.com/cards/sld/1507/en?format=image");
put("SLD/Treasure/4", "https://api.scryfall.com/cards/sld/153/en?format=image"); put("SLD/Treasure/4", "https://api.scryfall.com/cards/sld/153/en?format=image");
put("SLD/Treasure/5", "https://api.scryfall.com/cards/sld/2065/en?format=image");
put("SLD/Treasure/6", "https://api.scryfall.com/cards/sld/2094/en?format=image");
put("SLD/Walker/1", "https://api.scryfall.com/cards/sld/148/en?format=image"); put("SLD/Walker/1", "https://api.scryfall.com/cards/sld/148/en?format=image");
put("SLD/Walker/2", "https://api.scryfall.com/cards/sld/149/en?format=image"); put("SLD/Walker/2", "https://api.scryfall.com/cards/sld/149/en?format=image");
put("SLD/Walker/3", "https://api.scryfall.com/cards/sld/150/en?format=image"); put("SLD/Walker/3", "https://api.scryfall.com/cards/sld/150/en?format=image");
@ -1843,6 +1854,12 @@ public class ScryfallImageSupportTokens {
put("40K/Tyranid Gargoyle", "https://api.scryfall.com/cards/t40k/9/en?format=image"); put("40K/Tyranid Gargoyle", "https://api.scryfall.com/cards/t40k/9/en?format=image");
put("40K/Tyranid Warrior", "https://api.scryfall.com/cards/t40k/19/en?format=image"); put("40K/Tyranid Warrior", "https://api.scryfall.com/cards/t40k/19/en?format=image");
// UNF
put("UNF/Clown Robot/1", "https://api.scryfall.com/cards/tunf/2?format=image");
put("UNF/Clown Robot/2", "https://api.scryfall.com/cards/tunf/3?format=image");
put("UNF/Storm Crow", "https://api.scryfall.com/cards/tunf/5?format=image");
put("UNF/Squirrel", "https://api.scryfall.com/cards/tunf/8?format=image");
// BRO // BRO
put("BRO/Bear", "https://api.scryfall.com/cards/tbro/2/en?format=image"); put("BRO/Bear", "https://api.scryfall.com/cards/tbro/2/en?format=image");
put("BRO/Construct/1", "https://api.scryfall.com/cards/tbro/5/en?format=image"); put("BRO/Construct/1", "https://api.scryfall.com/cards/tbro/5/en?format=image");
@ -2026,6 +2043,7 @@ public class ScryfallImageSupportTokens {
// DIS // DIS
put("DIS/Emblem Momir", "https://api.scryfall.com/cards/pmoa/61/en?format=image"); put("DIS/Emblem Momir", "https://api.scryfall.com/cards/pmoa/61/en?format=image");
put("DIS/Elemental", "https://api.scryfall.com/cards/togw/9?format=image");
// MUL // MUL
put("MUL/Elemental", "https://api.scryfall.com/cards/tmul/2/en?format=image"); put("MUL/Elemental", "https://api.scryfall.com/cards/tmul/2/en?format=image");
@ -2162,26 +2180,20 @@ public class ScryfallImageSupportTokens {
put("WOE/Young Hero", "https://api.scryfall.com/cards/twoe/16/en?format=image"); put("WOE/Young Hero", "https://api.scryfall.com/cards/twoe/16/en?format=image");
// WOC // WOC
put("WOC/Cat/1", "https://api.scryfall.com/cards/twoc/6/en?format=image");
put("WOC/Cat/2", "https://api.scryfall.com/cards/twoc/5/en?format=image");
put("WOC/Elephant", "https://api.scryfall.com/cards/twoc/13/en?format=image");
put("WOC/Faerie", "https://api.scryfall.com/cards/twoc/10/en?format=image"); put("WOC/Faerie", "https://api.scryfall.com/cards/twoc/10/en?format=image");
put("WOC/Faerie Rogue/1", "https://api.scryfall.com/cards/twoc/11/en?format=image"); put("WOC/Faerie Rogue/1", "https://api.scryfall.com/cards/twoc/11/en?format=image");
put("WOC/Faerie Rogue/2", "https://api.scryfall.com/cards/twoc/16/en?format=image"); put("WOC/Faerie Rogue/2", "https://api.scryfall.com/cards/twoc/16/en?format=image");
put("WOC/Human Monk", "https://api.scryfall.com/cards/twoc/14/en?format=image");
put("WOC/Human Soldier", "https://api.scryfall.com/cards/twoc/7/en?format=image"); put("WOC/Human Soldier", "https://api.scryfall.com/cards/twoc/7/en?format=image");
put("WOC/Monster", "https://api.scryfall.com/cards/twoc/1/en?format=image"); put("WOC/Monster", "https://api.scryfall.com/cards/twoc/1/en?format=image");
put("WOC/Ox", "https://api.scryfall.com/cards/twoc/8/en?format=image");
put("WOC/Pegasus", "https://api.scryfall.com/cards/twoc/9/en?format=image"); put("WOC/Pegasus", "https://api.scryfall.com/cards/twoc/9/en?format=image");
put("WOC/Pirate", "https://api.scryfall.com/cards/twoc/12/en?format=image"); put("WOC/Pirate", "https://api.scryfall.com/cards/twoc/12/en?format=image");
put("WOC/Royal", "https://api.scryfall.com/cards/twoc/2/en?format=image"); put("WOC/Royal", "https://api.scryfall.com/cards/twoc/2/en?format=image");
put("WOC/Saproling", "https://api.scryfall.com/cards/twoc/15/en?format=image"); put("WOC/Saproling", "https://api.scryfall.com/cards/twoc/15/en?format=image");
put("WOC/Sorcerer", "https://api.scryfall.com/cards/twoc/3/en?format=image"); put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/2/en?format=image");
put("WOC/Spirit", "https://api.scryfall.com/cards/twoc/17/en?format=image");
put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/3/en?format=image");
// WHO // WHO
put("WHO/Alien", "https://api.scryfall.com/cards/twho/2?format=image"); put("WHO/Alien", "https://api.scryfall.com/cards/twho/2?format=image");
put("WHO/Alien Angel", "https://api.scryfall.com/cards/twho/11?format=image");
put("WHO/Alien Insect", "https://api.scryfall.com/cards/twho/19/en?format=image"); put("WHO/Alien Insect", "https://api.scryfall.com/cards/twho/19/en?format=image");
put("WHO/Alien Rhino", "https://api.scryfall.com/cards/twho/3/en?format=image"); put("WHO/Alien Rhino", "https://api.scryfall.com/cards/twho/3/en?format=image");
put("WHO/Alien Salamander", "https://api.scryfall.com/cards/twho/16?format=image"); put("WHO/Alien Salamander", "https://api.scryfall.com/cards/twho/16?format=image");
@ -2536,12 +2548,14 @@ public class ScryfallImageSupportTokens {
put("DSK/Everywhere", "https://api.scryfall.com/cards/tdsk/16?format=image"); put("DSK/Everywhere", "https://api.scryfall.com/cards/tdsk/16?format=image");
put("DSK/Glimmer", "https://api.scryfall.com/cards/tdsk/4?format=image"); put("DSK/Glimmer", "https://api.scryfall.com/cards/tdsk/4?format=image");
put("DSK/Gremlin", "https://api.scryfall.com/cards/tdsk/11?format=image"); put("DSK/Gremlin", "https://api.scryfall.com/cards/tdsk/11?format=image");
put("DSK/Horror", "https://api.scryfall.com/cards/tdsk/10?format=image");
put("DSK/Insect/1", "https://api.scryfall.com/cards/tdsk/13?format=image"); put("DSK/Insect/1", "https://api.scryfall.com/cards/tdsk/13?format=image");
put("DSK/Insect/2", "https://api.scryfall.com/cards/tdsk/5?format=image"); put("DSK/Insect/2", "https://api.scryfall.com/cards/tdsk/5?format=image");
put("DSK/Primo, the Indivisible", "https://api.scryfall.com/cards/tdsk/14?format=image"); put("DSK/Primo, the Indivisible", "https://api.scryfall.com/cards/tdsk/14?format=image");
put("DSK/Shard", "https://api.scryfall.com/cards/tdsk/2?format=image"); put("DSK/Shard", "https://api.scryfall.com/cards/tdsk/2?format=image");
put("DSK/Spider", "https://api.scryfall.com/cards/tdsk/12?format=image"); put("DSK/Spider", "https://api.scryfall.com/cards/tdsk/12?format=image");
put("DSK/Spirit", "https://api.scryfall.com/cards/tdsk/8?format=image"); put("DSK/Spirit/1", "https://api.scryfall.com/cards/tdsk/6?format=image");
put("DSK/Spirit/2", "https://api.scryfall.com/cards/tdsk/8?format=image");
put("DSK/Treasure", "https://api.scryfall.com/cards/tdsk/15?format=image"); put("DSK/Treasure", "https://api.scryfall.com/cards/tdsk/15?format=image");
// DSC // DSC
@ -2698,7 +2712,8 @@ public class ScryfallImageSupportTokens {
put("TDC/Gold", "https://api.scryfall.com/cards/ttdc/29/en?format=image"); put("TDC/Gold", "https://api.scryfall.com/cards/ttdc/29/en?format=image");
put("TDC/Human", "https://api.scryfall.com/cards/ttdc/5/en?format=image"); put("TDC/Human", "https://api.scryfall.com/cards/ttdc/5/en?format=image");
put("TDC/Inkling", "https://api.scryfall.com/cards/ttdc/28?format=image"); put("TDC/Inkling", "https://api.scryfall.com/cards/ttdc/28?format=image");
put("TDC/Insect", "https://api.scryfall.com/cards/ttdc/22/en?format=image"); put("TDC/Insect/1", "https://api.scryfall.com/cards/ttdc/22/en?format=image");
put("TDC/Insect/2", "https://api.scryfall.com/cards/ttdc/23/en?format=image");
put("TDC/Karox Bladewing", "https://api.scryfall.com/cards/ttdc/19?format=image"); put("TDC/Karox Bladewing", "https://api.scryfall.com/cards/ttdc/19?format=image");
put("TDC/Myr", "https://api.scryfall.com/cards/ttdc/30/en?format=image"); put("TDC/Myr", "https://api.scryfall.com/cards/ttdc/30/en?format=image");
put("TDC/Plant", "https://api.scryfall.com/cards/ttdc/24/en?format=image"); put("TDC/Plant", "https://api.scryfall.com/cards/ttdc/24/en?format=image");
@ -2710,6 +2725,7 @@ public class ScryfallImageSupportTokens {
put("TDC/Spider", "https://api.scryfall.com/cards/ttdc/25?format=image"); put("TDC/Spider", "https://api.scryfall.com/cards/ttdc/25?format=image");
put("TDC/Spirit", "https://api.scryfall.com/cards/ttdc/6/en?format=image"); put("TDC/Spirit", "https://api.scryfall.com/cards/ttdc/6/en?format=image");
put("TDC/Thopter", "https://api.scryfall.com/cards/ttdc/33/en?format=image"); put("TDC/Thopter", "https://api.scryfall.com/cards/ttdc/33/en?format=image");
put("TDC/Wall", "https://api.scryfall.com/cards/ttdc/7/en?format=image");
// ACR // ACR
put("ACR/Assassin", "https://api.scryfall.com/cards/tacr/4?format=image"); put("ACR/Assassin", "https://api.scryfall.com/cards/tacr/4?format=image");
@ -2723,7 +2739,51 @@ public class ScryfallImageSupportTokens {
put("DD2/Elemental Shaman", "https://api.scryfall.com/cards/tdd2/1?format=image"); put("DD2/Elemental Shaman", "https://api.scryfall.com/cards/tdd2/1?format=image");
// FIN // FIN
put("FIN/Hero/1", "https://api.scryfall.com/cards/tfin/2/en?format=image");
put("FIN/Hero/2", "https://api.scryfall.com/cards/tfin/3/en?format=image");
put("FIN/Hero/3", "https://api.scryfall.com/cards/tfin/4/en?format=image");
put("FIN/Hero/4", "https://api.scryfall.com/cards/tfin/5/en?format=image");
put("FIN/Hero/5", "https://api.scryfall.com/cards/tfin/6/en?format=image");
put("FIN/Hero/6", "https://api.scryfall.com/cards/tfin/7/en?format=image");
put("FIN/Hero/7", "https://api.scryfall.com/cards/tfin/8/en?format=image");
put("FIN/Hero/8", "https://api.scryfall.com/cards/tfin/9/en?format=image");
put("FIN/Hero/9", "https://api.scryfall.com/cards/tfin/26/en?format=image");
put("FIN/Hero/10", "https://api.scryfall.com/cards/tfin/27/en?format=image");
put("FIN/Hero/11", "https://api.scryfall.com/cards/tfin/28/en?format=image");
put("FIN/Hero/12", "https://api.scryfall.com/cards/tfin/29/en?format=image");
put("FIN/Hero/13", "https://api.scryfall.com/cards/tfin/30/en?format=image");
put("FIN/Hero/14", "https://api.scryfall.com/cards/tfin/31/en?format=image");
put("FIN/Hero/15", "https://api.scryfall.com/cards/tfin/32/en?format=image");
put("FIN/Hero/16", "https://api.scryfall.com/cards/tfin/33/en?format=image");
put("FIN/Knight", "https://api.scryfall.com/cards/tfin/10/en?format=image");
put("FIN/Moogle/1", "https://api.scryfall.com/cards/tfin/11/en?format=image");
put("FIN/Moogle/2", "https://api.scryfall.com/cards/tfin/34/en?format=image");
put("FIN/Robot", "https://api.scryfall.com/cards/tfin/12/en?format=image");
put("FIN/Horror", "https://api.scryfall.com/cards/tfin/13/en?format=image");
put("FIN/Wizard/1", "https://api.scryfall.com/cards/tfin/14/en?format=image");
put("FIN/Wizard/2", "https://api.scryfall.com/cards/tfin/15/en?format=image");
put("FIN/Wizard/3", "https://api.scryfall.com/cards/tfin/35/en?format=image");
put("FIN/Bird/1", "https://api.scryfall.com/cards/tfin/16/en?format=image");
put("FIN/Bird/2", "https://api.scryfall.com/cards/tfin/17/en?format=image");
put("FIN/Frog", "https://api.scryfall.com/cards/tfin/18/en?format=image");
put("FIN/Angelo", "https://api.scryfall.com/cards/tfin/19/en?format=image");
put("FIN/Darkstar", "https://api.scryfall.com/cards/tfin/20/en?format=image");
put("FIN/Elemental", "https://api.scryfall.com/cards/tfin/21/en?format=image");
put("FIN/Food", "https://api.scryfall.com/cards/tfin/22?format=image"); put("FIN/Food", "https://api.scryfall.com/cards/tfin/22?format=image");
put("FIN/Treasure/1", "https://api.scryfall.com/cards/tfin/23/en?format=image");
put("FIN/Treasure/2", "https://api.scryfall.com/cards/tfin/36/en?format=image");
put("FIN/Emblem Sephiroth", "https://api.scryfall.com/cards/tfin/24/en?format=image");
// FIC
put("FIC/Human Soldier", "https://api.scryfall.com/cards/tfic/1/en?format=image");
put("FIC/Soldier", "https://api.scryfall.com/cards/tfic/2/en?format=image");
put("FIC/Spirit", "https://api.scryfall.com/cards/tfic/3/en?format=image");
put("FIC/Bird", "https://api.scryfall.com/cards/tfic/4/en?format=image");
put("FIC/Squid", "https://api.scryfall.com/cards/tfic/5/en?format=image");
put("FIC/Zombie", "https://api.scryfall.com/cards/tfic/6/en?format=image");
put("FIC/Rebel", "https://api.scryfall.com/cards/tfic/7/en?format=image");
put("FIC/The Blackjack", "https://api.scryfall.com/cards/tfic/8/en?format=image");
put("FIC/Clue", "https://api.scryfall.com/cards/tfic/9/en?format=image");
// JVC // JVC
put("JVC/Elemental Shaman", "https://api.scryfall.com/cards/tjvc/4?format=image"); put("JVC/Elemental Shaman", "https://api.scryfall.com/cards/tjvc/4?format=image");
@ -2756,6 +2816,7 @@ public class ScryfallImageSupportTokens {
// UGL // UGL
put("UGL/Goblin", "https://api.scryfall.com/cards/tugl/4?format=image"); put("UGL/Goblin", "https://api.scryfall.com/cards/tugl/4?format=image");
put("UGL/Pegasus", "https://api.scryfall.com/cards/tugl/1?format=image"); put("UGL/Pegasus", "https://api.scryfall.com/cards/tugl/1?format=image");
put("UGL/Rabid Sheep", "https://api.scryfall.com/cards/tugl/5?format=image");
put("UGL/Soldier", "https://api.scryfall.com/cards/tugl/2?format=image"); put("UGL/Soldier", "https://api.scryfall.com/cards/tugl/2?format=image");
put("UGL/Squirrel", "https://api.scryfall.com/cards/tugl/6?format=image"); put("UGL/Squirrel", "https://api.scryfall.com/cards/tugl/6?format=image");
put("UGL/Zombie", "https://api.scryfall.com/cards/tugl/3?format=image"); put("UGL/Zombie", "https://api.scryfall.com/cards/tugl/3?format=image");

View file

@ -157,7 +157,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
@Override @Override
public boolean isNeedCancel() { public boolean isNeedCancel() {
return this.needCancel || (this.errorCount > MAX_ERRORS_COUNT_BEFORE_CANCEL) || Thread.interrupted(); return this.needCancel || (this.errorCount > MAX_ERRORS_COUNT_BEFORE_CANCEL) || Thread.currentThread().isInterrupted();
} }
private void setNeedCancel(boolean needCancel) { private void setNeedCancel(boolean needCancel) {

View file

@ -66,7 +66,6 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View file

@ -1,63 +0,0 @@
package mage.cards.action.impl;
import mage.cards.action.ActionCallback;
import mage.cards.action.TransferData;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
/**
* Callback that does nothing on any action
*
* @author nantuko84
*/
public class EmptyCallback implements ActionCallback {
@Override
public void mouseMoved(MouseEvent e, TransferData data) {
}
@Override
public void mouseDragged(MouseEvent e, TransferData data) {
}
@Override
public void mouseEntered(MouseEvent e, TransferData data) {
}
@Override
public void mouseExited(MouseEvent e, TransferData data) {
}
@Override
public void mouseWheelMoved(int mouseWheelRotation, TransferData data) {
}
@Override
public void hideOpenComponents() {
}
@Override
public void mouseClicked(MouseEvent e, TransferData data, boolean doubleClick) {
}
@Override
public void mousePressed(MouseEvent e, TransferData data) {
}
@Override
public void mouseReleased(MouseEvent e, TransferData data) {
}
@Override
public void popupMenuCard(MouseEvent e, TransferData data) {
}
@Override
public void popupMenuPanel(MouseEvent e, Component sourceComponent) {
}
}

View file

@ -1,72 +0,0 @@
package mage.filters;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
/**
* Mage abstract class that implements single-input/single-output
* operations performed on {@link java.awt.image.BufferedImage}.
*
* @author nantuko
*/
public abstract class MageBufferedImageOp implements BufferedImageOp {
/**
* Creates compatible image for @param src image.
*/
@Override
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dest) {
if (dest == null) {
dest = src.getColorModel();
}
return new BufferedImage(dest, dest.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dest.isAlphaPremultiplied(), null);
}
@Override
public RenderingHints getRenderingHints() {
return null;
}
@Override
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle(0, 0, src.getWidth(), src.getHeight());
}
@Override
public Point2D getPoint2D(Point2D srcPt, Point2D destPt) {
if (destPt == null) {
destPt = new Point2D.Double();
}
destPt.setLocation(srcPt.getX(), srcPt.getY());
return destPt;
}
/**
* Gets ARGB pixels from image. Solves the performance
* issue of BufferedImage.getRGB method.
*/
public int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
int type = image.getType();
if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
}
return image.getRGB(x, y, width, height, pixels, 0, width);
}
/**
* Sets ARGB pixels in image. Solves the performance
* issue of BufferedImage.setRGB method.
*/
public void setRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
int type = image.getType();
if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
image.getRaster().setDataElements(x, y, width, height, pixels);
} else {
image.setRGB(x, y, width, height, pixels, 0, width);
}
}
}

View file

@ -1,11 +0,0 @@
package mage.utils;
import mage.view.TableView;
/**
* Used to write less code for ActionWithResult anonymous classes with UUID return type.
*
* @author noxx
*/
public abstract class ActionWithUUIDResult extends ActionWithNullNegativeResult<TableView> {
}

View file

@ -0,0 +1,59 @@
package mage.utils.testers;
import java.util.List;
/**
* Part of testable game dialogs
*
* @author JayDi85
*/
public class AmountTestableResult extends BaseTestableResult {
Integer amount = null;
boolean aiAssertEnabled = false;
int aiAssertMinAmount = 0;
int aiAssertMaxAmount = 0;
public void onFinish(String resDebugSource, boolean status, List<String> info, int amount) {
this.onFinish(resDebugSource, status, info);
this.amount = amount;
}
@Override
public String getResAssert() {
if (!this.aiAssertEnabled) {
return null;
}
// not finished
if (this.amount == null) {
return null;
}
if (!this.getResStatus()) {
return String.format("Wrong status: need %s, but get %s",
true, // res must be true all the time
this.getResStatus()
);
}
// wrong amount
if (this.amount < this.aiAssertMinAmount || this.amount > this.aiAssertMaxAmount) {
return String.format("Wrong amount: need [%d, %d], but get %d",
this.aiAssertMinAmount,
this.aiAssertMaxAmount,
this.amount
);
}
// all fine
return "";
}
@Override
public void onClear() {
super.onClear();
this.amount = null;
}
}

View file

@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -25,22 +26,33 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
public AnnounceXTestableDialog(boolean isYou, boolean isMana, int min, int max) { public AnnounceXTestableDialog(boolean isYou, boolean isMana, int min, int max) {
super(String.format("player.announceX(%s)", isYou ? "you" : "AI"), super(String.format("player.announceX(%s)", isYou ? "you" : "AI"),
String.format("%s from %d to %d", isMana ? "mana" : "cost", min, max), ""); String.format("%s from %d to %d", isMana ? "mana" : "cost", min, max), "",
new AmountTestableResult());
this.isYou = isYou; this.isYou = isYou;
this.isMana = isMana; this.isMana = isMana;
this.min = min; this.min = min;
this.max = max; this.max = max;
} }
private AnnounceXTestableDialog aiMustChoose(int minAmount, int maxAmount) {
// require min/max cause AI logic uses random choices
AmountTestableResult res = ((AmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertMinAmount = minAmount;
res.aiAssertMaxAmount = maxAmount;
return this;
}
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
String message = "<font color=green>message</font> with html"; String message = "<font color=green>message</font> with html";
int chooseRes; String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana); int chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana);
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(getGroup() + " - " + this.getName() + " selected " + chooseRes); res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
return result;
((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {
@ -48,17 +60,17 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
List<Boolean> isManas = Arrays.asList(false, true); List<Boolean> isManas = Arrays.asList(false, true);
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
for (boolean isMana : isManas) { for (boolean isMana : isManas) {
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0).aiMustChoose(0, 0));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1).aiMustChoose(0, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3).aiMustChoose(0, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50).aiMustChoose(0, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500).aiMustChoose(0, 500));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1).aiMustChoose(1, 1));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3).aiMustChoose(1, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50).aiMustChoose(1, 50));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3).aiMustChoose(3, 3));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10).aiMustChoose(3, 10));
runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10)); runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10).aiMustChoose(10, 10));
} }
} }
} }

View file

@ -1,11 +1,12 @@
package mage.utils.testers; package mage.utils.testers;
import mage.constants.SubType; import mage.constants.ComparisonType;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.FilterPermanent;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.common.TargetCreaturePermanent; import mage.target.TargetPermanent;
import mage.target.common.TargetPermanentOrPlayer; import mage.target.common.TargetPermanentOrPlayer;
/** /**
@ -15,14 +16,27 @@ import mage.target.common.TargetPermanentOrPlayer;
*/ */
abstract class BaseTestableDialog implements TestableDialog { abstract class BaseTestableDialog implements TestableDialog {
private Integer regNumber; // dialog number in runner (use it to find results and debugging)
private final String group; private final String group;
private final String name; private final String name;
private final String description; private final String description;
private final TestableResult result;
public BaseTestableDialog(String group, String name, String description) { public BaseTestableDialog(String group, String name, String description, TestableResult result) {
this.group = group; this.group = group;
this.name = name; this.name = name;
this.description = description; this.description = description;
this.result = result;
}
@Override
public void setRegNumber(Integer regNumber) {
this.regNumber = regNumber;
}
@Override
public Integer getRegNumber() {
return this.regNumber;
} }
@Override @Override
@ -41,13 +55,23 @@ abstract class BaseTestableDialog implements TestableDialog {
} }
@Override @Override
final public void showResult(Player player, Game game, String result) { public void prepare() {
this.result.onClear();
}
@Override
final public void showResult(Player player, Game game) {
// show message with result // show message with result
game.informPlayer(player, result); game.informPlayer(player, String.join("<br>", getResult().getResDetails()));
// reset game and gui (in most use cases it must return to player's priority) // reset game and gui (in most use cases it must return to player's priority)
game.firePriorityEvent(player.getId()); game.firePriorityEvent(player.getId());
} }
@Override
public TestableResult getResult() {
return this.result;
}
static Target createAnyTarget(int min, int max) { static Target createAnyTarget(int min, int max) {
return createAnyTarget(min, max, false); return createAnyTarget(min, max, false);
} }
@ -56,19 +80,18 @@ abstract class BaseTestableDialog implements TestableDialog {
return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget); return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget);
} }
static Target createCreatureTarget(int min, int max) { private static final FilterPermanent impossibleFilter = new FilterPermanent();
return createCreatureTarget(min, max, false);
}
private static Target createCreatureTarget(int min, int max, boolean notTarget) { static {
return new TargetCreaturePermanent(min, max).withNotTarget(notTarget); impossibleFilter.add(new ManaValuePredicate(ComparisonType.OR_LESS, -1));
} }
static Target createImpossibleTarget(int min, int max) { static Target createImpossibleTarget(int min, int max) {
return createImpossibleTarget(min, max, false); return new TargetPermanent(min, max, impossibleFilter);
} }
private static Target createImpossibleTarget(int min, int max, boolean notTarget) { @Override
return new TargetCreaturePermanent(min, max, new FilterCreaturePermanent(SubType.TROOPER, "rare type"), notTarget); public String toString() {
return this.getGroup() + " - " + this.getName() + " - " + this.getDescription();
} }
} }

View file

@ -0,0 +1,57 @@
package mage.utils.testers;
import java.util.ArrayList;
import java.util.List;
/**
* Part of testable game dialogs
*
* @author JayDi85
*/
public class BaseTestableResult implements TestableResult {
boolean isFinished = false;
String resDebugSource = ""; // source code line to find starting place to debug
boolean resStatus = false;
List<String> resInfo = new ArrayList<>();
@Override
public String getResDebugSource() {
return this.resDebugSource;
}
@Override
public boolean getResStatus() {
return this.resStatus;
}
@Override
public List<String> getResDetails() {
return this.resInfo;
}
@Override
public String getResAssert() {
return null; // TODO: implement
}
@Override
public void onFinish(String resDebugSource, boolean resStatus, List<String> resDetails) {
this.isFinished = true;
this.resDebugSource = resDebugSource;
this.resStatus = resStatus;
this.resInfo = resDetails;
}
@Override
public boolean isFinished() {
return this.isFinished;
}
@Override
public void onClear() {
this.isFinished = false;
this.resStatus = false;
this.resInfo.clear();
}
}

View file

@ -0,0 +1,29 @@
package mage.utils.testers;
import java.util.List;
/**
* Part of testable game dialogs
*
* @author JayDi85
*/
public class ChoiceTestableResult extends BaseTestableResult {
String choice = null;
public void onFinish(String resDebugSource, boolean status, List<String> info, String choice) {
this.onFinish(resDebugSource, status, info);
this.choice = choice;
}
@Override
public String getResAssert() {
return null; // TODO: implement
}
@Override
public void onClear() {
super.onClear();
this.choice = null;
}
}

View file

@ -7,6 +7,7 @@ import mage.players.Player;
import mage.target.TargetAmount; import mage.target.TargetAmount;
import mage.target.Targets; import mage.target.Targets;
import mage.target.common.TargetAnyTargetAmount; import mage.target.common.TargetAnyTargetAmount;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -32,27 +33,39 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
public ChooseAmountTestableDialog(boolean isYou, String name, int distributeAmount, int targetsMin, int targetsMax) { public ChooseAmountTestableDialog(boolean isYou, String name, int distributeAmount, int targetsMin, int targetsMax) {
super(String.format("player.chooseTarget(%s, amount)", isYou ? "you" : "AI"), super(String.format("player.chooseTarget(%s, amount)", isYou ? "you" : "AI"),
name, name,
String.format("%d between %d-%d targets", distributeAmount, targetsMin, targetsMax)); String.format("%d between %d-%d targets", distributeAmount, targetsMin, targetsMax),
new TargetTestableResult());
this.isYou = isYou; this.isYou = isYou;
this.distributeAmount = distributeAmount; this.distributeAmount = distributeAmount;
this.targetsMin = targetsMin; this.targetsMin = targetsMin;
this.targetsMax = targetsMax; this.targetsMax = targetsMax;
} }
private ChooseAmountTestableDialog aiMustChoose(boolean resStatus, int targetsCount) {
// TODO: AI use default distribution, improve someday
TargetTestableResult res = ((TargetTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertResStatus = resStatus;
res.aiAssertTargetsCount = targetsCount;
return this;
}
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
TargetAmount choosingTarget = new TargetAnyTargetAmount(this.distributeAmount, this.targetsMin, this.targetsMax); TargetAmount choosingTarget = new TargetAnyTargetAmount(this.distributeAmount, this.targetsMin, this.targetsMax);
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
// TODO: add "damage" word in ability text, so chooseTargetAmount an show diff dialog (due inner logic - distribute damage or 1/1) // TODO: add "damage" word in ability text, so chooseTargetAmount an show diff dialog (due inner logic - distribute damage or 1/1)
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseTargetAmount(Outcome.Benefit, choosingTarget, source, game); boolean chooseRes = choosingPlayer.chooseTargetAmount(Outcome.Benefit, choosingTarget, source, game);
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
if (chooseRes) { if (chooseRes) {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, res);
} else { } else {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
} }
return result;
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {
@ -61,53 +74,55 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
List<Boolean> isYous = Arrays.asList(false, true); List<Boolean> isYous = Arrays.asList(false, true);
// current AI will choose 1 target and assign all values to it (except with outcome.Damage)
// TODO: add use cases for damage effects?
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
// up to // up to
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 0)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 0).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 1).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 3).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 5).aiMustChoose(false, 0));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 1, 0, 0)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 1, 0, 0).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 2, 0, 0)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 2, 0, 0).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 3, 0, 0)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 3, 0, 0).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 5, 0, 0)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 5, 0, 0).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 5).aiMustChoose(true, 1));
// need target // need target
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 1).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 3).aiMustChoose(false, 0));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 5).aiMustChoose(false, 0));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 5).aiMustChoose(true, 1));
// //
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 1)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 1).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 3)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 3).aiMustChoose(true, 1));
runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 5)); runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 5).aiMustChoose(true, 1));
} }
} }
} }

View file

@ -13,6 +13,7 @@ import mage.players.Player;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.Targets; import mage.target.Targets;
import mage.target.common.TargetCardInHand; import mage.target.common.TargetCardInHand;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -36,16 +37,17 @@ class ChooseCardsTestableDialog extends BaseTestableDialog {
public ChooseCardsTestableDialog(boolean isTargetChoice, boolean notTarget, boolean isYou, String name, TargetCard target) { public ChooseCardsTestableDialog(boolean isTargetChoice, boolean notTarget, boolean isYou, String name, TargetCard target) {
super(String.format("%s(%s, %s, cards)", super(String.format("%s(%s, %s, cards)",
isTargetChoice ? "player.chooseTarget" : "player.choose", isTargetChoice ? "player.chooseTarget" : "player.choose",
isYou ? "you" : "AI", isYou ? "you" : "AI",
notTarget ? "not target" : "target"), name, target.toString()); notTarget ? "not target" : "target"), name, target.toString(),
new TargetTestableResult());
this.isTargetChoice = isTargetChoice; this.isTargetChoice = isTargetChoice;
this.target = target.withNotTarget(notTarget); this.target = target.withNotTarget(notTarget);
this.isYou = isYou; this.isYou = isYou;
} }
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
TargetCard choosingTarget = this.target.copy(); TargetCard choosingTarget = this.target.copy();
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
@ -56,19 +58,23 @@ class ChooseCardsTestableDialog extends BaseTestableDialog {
Cards choosingCards = new CardsImpl(all.stream().limit(100).collect(Collectors.toList())); Cards choosingCards = new CardsImpl(all.stream().limit(100).collect(Collectors.toList()));
boolean chooseRes; boolean chooseRes;
String chooseDebugSource;
if (this.isTargetChoice) { if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingCards, choosingTarget, source, game); chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingCards, choosingTarget, source, game);
} else { } else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingCards, choosingTarget, source, game); chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingCards, choosingTarget, source, game);
} }
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
if (chooseRes) { if (chooseRes) {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, res);
} else { } else {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
} }
return result;
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {

View file

@ -5,6 +5,7 @@ import mage.choices.*;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -24,28 +25,36 @@ class ChooseChoiceTestableDialog extends BaseTestableDialog {
Choice choice; Choice choice;
public ChooseChoiceTestableDialog(boolean isYou, String name, Choice choice) { public ChooseChoiceTestableDialog(boolean isYou, String name, Choice choice) {
super(String.format("player.choose(%s, choice)", isYou ? "you" : "AI"), name, choice.getClass().getSimpleName()); super(String.format("player.choose(%s, choice)", isYou ? "you" : "AI"),
name,
choice.getClass().getSimpleName(),
new ChoiceTestableResult()
);
this.isYou = isYou; this.isYou = isYou;
this.choice = choice; this.choice = choice;
} }
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
Choice dialog = this.choice.copy(); Choice dialog = this.choice.copy();
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choose(Outcome.Benefit, dialog, game); boolean chooseRes = choosingPlayer.choose(Outcome.Benefit, dialog, game);
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE")); res.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
result.add(""); res.add("");
String choice;
if (dialog.isKeyChoice()) { if (dialog.isKeyChoice()) {
String key = dialog.getChoiceKey(); String key = dialog.getChoiceKey();
result.add(String.format("* selected key: %s (%s)", key, dialog.getKeyChoices().getOrDefault(key, null))); choice = dialog.getKeyChoices().getOrDefault(key, null);
res.add(String.format("* selected key: %s (%s)", key, choice));
} else { } else {
result.add(String.format("* selected value: %s", dialog.getChoice())); choice = dialog.getChoice();
res.add(String.format("* selected value: %s", choice));
} }
return result; ((ChoiceTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choice);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {

View file

@ -6,6 +6,7 @@ import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -28,14 +29,18 @@ class ChoosePileTestableDialog extends BaseTestableDialog {
int pileSize2; int pileSize2;
public ChoosePileTestableDialog(boolean isYou, int pileSize1, int pileSize2) { public ChoosePileTestableDialog(boolean isYou, int pileSize1, int pileSize2) {
super(String.format("player.choosePile(%s)", isYou ? "you" : "AI"), "pile sizes: " + pileSize1 + " and " + pileSize2, ""); super(String.format("player.choosePile(%s)", isYou ? "you" : "AI"),
"pile sizes: " + pileSize1 + " and " + pileSize2,
"",
new BaseTestableResult()
);
this.isYou = isYou; this.isYou = isYou;
this.pileSize1 = pileSize1; this.pileSize1 = pileSize1;
this.pileSize2 = pileSize2; this.pileSize2 = pileSize2;
} }
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
// TODO: it's ok to show broken title - must add html support in windows's title someday // TODO: it's ok to show broken title - must add html support in windows's title someday
String mainMessage = "main <font color=green>message</font> with html" + CardUtil.getSourceLogName(game, source); String mainMessage = "main <font color=green>message</font> with html" + CardUtil.getSourceLogName(game, source);
@ -47,11 +52,13 @@ class ChoosePileTestableDialog extends BaseTestableDialog {
List<Card> pile2 = all.stream().limit(this.pileSize2).collect(Collectors.toList()); List<Card> pile2 = all.stream().limit(this.pileSize2).collect(Collectors.toList());
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choosePile(Outcome.Benefit, mainMessage, pile1, pile2, game); boolean chooseRes = choosingPlayer.choosePile(Outcome.Benefit, mainMessage, pile1, pile2, game);
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE")); res.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
result.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2")); res.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2"));
return result;
this.getResult().onFinish(chooseDebugSource, chooseRes, res);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {

View file

@ -6,6 +6,7 @@ import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.Targets; import mage.target.Targets;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -31,10 +32,14 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
public ChooseTargetTestableDialog(boolean isPlayerChoice, boolean isTargetChoice, boolean notTarget, boolean isYou, String name, Target target) { public ChooseTargetTestableDialog(boolean isPlayerChoice, boolean isTargetChoice, boolean notTarget, boolean isYou, String name, Target target) {
super(String.format("%s%s(%s, %s)", super(String.format("%s%s(%s, %s)",
isPlayerChoice ? "player.choose" : "target.choose", isPlayerChoice ? "player.choose" : "target.choose",
isTargetChoice ? "Target" : "", // chooseTarget or choose isTargetChoice ? "Target" : "", // chooseTarget or choose
isYou ? "you" : "AI", isYou ? "you" : "AI",
notTarget ? "not target" : "target"), name, target.toString()); notTarget ? "not target" : "target"),
name,
target.toString(),
new TargetTestableResult()
);
this.isPlayerChoice = isPlayerChoice; this.isPlayerChoice = isPlayerChoice;
this.isTargetChoice = isTargetChoice; this.isTargetChoice = isTargetChoice;
this.target = target.withNotTarget(notTarget); this.target = target.withNotTarget(notTarget);
@ -42,34 +47,48 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
} }
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Target choosingTarget = this.target.copy(); Target choosingTarget = this.target.copy();
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
boolean chooseRes; boolean chooseRes;
String chooseDebugSource;
if (this.isPlayerChoice) { if (this.isPlayerChoice) {
// player.chooseXXX // player.chooseXXX
if (this.isTargetChoice) { if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingTarget, source, game); chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingTarget, source, game);
} else { } else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingTarget, source, game); chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingTarget, source, game);
} }
} else { } else {
// target.chooseXXX // target.chooseXXX
if (this.isTargetChoice) { if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.chooseTarget(Outcome.Benefit, choosingPlayer.getId(), source, game); chooseRes = choosingTarget.chooseTarget(Outcome.Benefit, choosingPlayer.getId(), source, game);
} else { } else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.choose(Outcome.Benefit, choosingPlayer.getId(), source, game); chooseRes = choosingTarget.choose(Outcome.Benefit, choosingPlayer.getId(), source, game);
} }
} }
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
if (chooseRes) { if (chooseRes) {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "TRUE", new Targets(choosingTarget), source, game, res);
} else { } else {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, result); Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
} }
return result;
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
}
private ChooseTargetTestableDialog aiMustChoose(boolean resStatus, int targetsCount) {
TargetTestableResult res = ((TargetTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertResStatus = resStatus;
res.aiAssertTargetsCount = targetsCount;
return this;
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {
@ -84,37 +103,29 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
for (boolean isTargetChoice : isTargetChoices) { for (boolean isTargetChoice : isTargetChoices) {
for (boolean isPlayerChoice : isPlayerChoices) { for (boolean isPlayerChoice : isPlayerChoices) {
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0))); // simulate X=0 runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0 e.g. X=0", createAnyTarget(0, 0)).aiMustChoose(false, 0)); // simulate X=0
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1", createAnyTarget(1, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any max", createAnyTarget(0, Integer.MAX_VALUE)).aiMustChoose(true, 6 + 1)); // 6 own cards + 1 own player
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-1", createAnyTarget(0, 1)).aiMustChoose(true, 1));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-3", createAnyTarget(0, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 0-5", createAnyTarget(0, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-3", createAnyTarget(1, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-3", createAnyTarget(1, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-3", createAnyTarget(2, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-3", createAnyTarget(2, 3)).aiMustChoose(true, 3));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 5)).aiMustChoose(true, 5));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5))); // impossible on 3 targets runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 4-5", createAnyTarget(4, 5)).aiMustChoose(true, 5));
// //
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0, e.g. X=0", createImpossibleTarget(0, 0)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1", createImpossibleTarget(1, 1)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 3", createImpossibleTarget(3, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 3", createImpossibleTarget(3, 3)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-1", createImpossibleTarget(0, 1))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-1", createImpossibleTarget(0, 1)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-3", createImpossibleTarget(0, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-3", createImpossibleTarget(0, 3)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3)).aiMustChoose(false, 0));
runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE))); runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE)).aiMustChoose(false, 0));
//
/*
runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 0, e.g. X=0", createCreatureTarget(0, 0))); // simulate X=0
runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 1", createCreatureTarget(1, 1)));
runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 3", createCreatureTarget(3, 3)));
runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures 5", createCreatureTarget(5, 5)));
runner.registerDialog(new PlayerChooseTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "creatures max", createCreatureTarget(0, Integer.MAX_VALUE)));
*/
} }
} }
} }

View file

@ -5,6 +5,7 @@ import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -27,7 +28,11 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
String messageAdditional; String messageAdditional;
public ChooseUseTestableDialog(boolean isYou, String name, String trueText, String falseText, String messageMain, String messageAdditional) { public ChooseUseTestableDialog(boolean isYou, String name, String trueText, String falseText, String messageMain, String messageAdditional) {
super(String.format("player.chooseUse(%s)", isYou ? "you" : "AI"), name + buildName(trueText, falseText, messageMain, messageAdditional), ""); super(String.format("player.chooseUse(%s)", isYou ? "you" : "AI"),
name + buildName(trueText, falseText, messageMain, messageAdditional),
"",
new BaseTestableResult()
);
this.isYou = isYou; this.isYou = isYou;
this.trueText = trueText; this.trueText = trueText;
this.falseText = falseText; this.falseText = falseText;
@ -42,8 +47,9 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
} }
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseUse( boolean chooseRes = choosingPlayer.chooseUse(
Outcome.Benefit, Outcome.Benefit,
messageMain, messageMain,
@ -53,9 +59,10 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
source, source,
game game
); );
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(chooseRes ? "TRUE" : "FALSE"); res.add(chooseRes ? "TRUE" : "FALSE");
return result;
this.getResult().onFinish(chooseDebugSource, chooseRes, res);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {

View file

@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -26,37 +27,53 @@ class GetAmountTestableDialog extends BaseTestableDialog {
public GetAmountTestableDialog(boolean isYou, int min, int max) { public GetAmountTestableDialog(boolean isYou, int min, int max) {
super(String.format("player.getAmount(%s)", isYou ? "you" : "AI"), super(String.format("player.getAmount(%s)", isYou ? "you" : "AI"),
String.format("from %d to %d", min, max), ""); String.format("from %d to %d", min, max),
"",
new AmountTestableResult()
);
this.isYou = isYou; this.isYou = isYou;
this.min = min; this.min = min;
this.max = max; this.max = max;
} }
private GetAmountTestableDialog aiMustChoose(int minAmount, int maxAmount) {
// require min/max cause AI logic uses random choices
AmountTestableResult res = ((AmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertMinAmount = minAmount;
res.aiAssertMaxAmount = maxAmount;
return this;
}
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
String message = "<font color=green>message</font> with html"; String message = "<font color=green>message</font> with html";
int chooseRes; String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game); int chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game);
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(getGroup() + " - " + this.getName() + " selected " + chooseRes); res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
return result;
((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {
List<Boolean> isYous = Arrays.asList(false, true); List<Boolean> isYous = Arrays.asList(false, true);
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0)); // TODO: add good and bad effects:
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1)); // - on good: choose random big value
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3)); // - on bad: choose lower value
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50)); runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0).aiMustChoose(0, 0));
runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500)); runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1).aiMustChoose(0, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1)); runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3).aiMustChoose(0, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3)); runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50).aiMustChoose(0, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 50)); runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500).aiMustChoose(0, 500));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 3)); runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1).aiMustChoose(1, 1));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 10)); runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3).aiMustChoose(1, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 10, 10)); runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 50).aiMustChoose(1, 50));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 3).aiMustChoose(3, 3));
runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 10).aiMustChoose(3, 10));
runner.registerDialog(new GetAmountTestableDialog(isYou, 10, 10).aiMustChoose(10, 10));
} }
} }
} }

View file

@ -5,12 +5,14 @@ import mage.constants.MultiAmountType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.util.DebugUtil;
import mage.util.MultiAmountMessage; import mage.util.MultiAmountMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
/** /**
* Part of testable game dialogs * Part of testable game dialogs
@ -36,7 +38,9 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
public GetMultiAmountTestableDialog(boolean isYou, String info, int totalMin, int totalMax, List<List<Integer>> options) { public GetMultiAmountTestableDialog(boolean isYou, String info, int totalMin, int totalMax, List<List<Integer>> options) {
super(String.format("player.getMultiAmount(%s)", isYou ? "you" : "AI"), super(String.format("player.getMultiAmount(%s)", isYou ? "you" : "AI"),
String.format("%s, %d options from [%d-%d]", info, options.size(), totalMin, totalMax), String.format("%s, %d options from [%d-%d]", info, options.size(), totalMin, totalMax),
""); "",
new MultiAmountTestableResult()
);
this.isYou = isYou; this.isYou = isYou;
this.totalMin = totalMin; this.totalMin = totalMin;
this.totalMax = totalMax; this.totalMax = totalMax;
@ -48,13 +52,35 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
} }
} }
private GetMultiAmountTestableDialog aiMustChoose(Integer... needValues) {
// TODO: AI use default distribution:
// - bad effect: min possible values
// - good effect: max possible and distributed values
MultiAmountTestableResult res = ((MultiAmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertValues = Arrays.stream(needValues).collect(Collectors.toList());
return this;
}
private GetMultiAmountTestableDialog aiMustChooseMany(Integer options, Integer perOption) {
List<Integer> need = new ArrayList<>();
IntStream.rangeClosed(1, options).forEach(x -> {
need.add(perOption);
});
MultiAmountTestableResult res = ((MultiAmountTestableResult) this.getResult());
res.aiAssertEnabled = true;
res.aiAssertValues = need;
return this;
}
@Override @Override
public List<String> showDialog(Player player, Ability source, Game game, Player opponent) { public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent; Player choosingPlayer = this.isYou ? player : opponent;
//String message = "<font color=green>message</font> with html"; //String message = "<font color=green>message</font> with html";
List<Integer> chooseRes;
List<MultiAmountMessage> options = this.amountOptions.stream().map(MultiAmountMessage::copy).collect(Collectors.toList()); List<MultiAmountMessage> options = this.amountOptions.stream().map(MultiAmountMessage::copy).collect(Collectors.toList());
chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints( String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
List<Integer> chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints(
Outcome.Benefit, Outcome.Benefit,
options, options,
this.totalMin, this.totalMin,
@ -63,24 +89,24 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
game game
); );
List<String> result = new ArrayList<>(); List<String> res = new ArrayList<>();
result.add(getGroup() + " - " + this.getName()); res.add(getGroup() + " - " + this.getName());
int selectedIndex = -1; int selectedIndex = -1;
int selectedTotal = 0; int selectedTotal = 0;
for (Integer selectedValue : chooseRes) { for (Integer selectedValue : chooseRes) {
selectedIndex++; selectedIndex++;
selectedTotal += selectedValue; selectedTotal += selectedValue;
MultiAmountMessage option = this.amountOptions.get(selectedIndex); MultiAmountMessage option = this.amountOptions.get(selectedIndex);
result.add(String.format("%d from [%d-%d, def %d]", res.add(String.format("%d from [%d-%d, def %d]",
selectedValue, selectedValue,
option.min, option.min,
option.max, option.max,
option.defaultValue option.defaultValue
)); ));
} }
result.add("total selected: " + selectedTotal); res.add("total selected: " + selectedTotal);
return result; ((MultiAmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
} }
static public void register(TestableDialogsRunner runner) { static public void register(TestableDialogsRunner runner) {
@ -88,28 +114,29 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) { for (boolean isYou : isYous) {
// make sure default values are valid due min/max settings // make sure default values are valid due min/max settings
// TODO: add bad effect for AI (must test default distribution)
// single target // single target
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 0 def", 0, 1, genSameOptions(1, 0, 1, 0))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 0 def", 0, 1, genSameOptions(1, 0, 1, 0)).aiMustChoose(1));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 0 def", 0, 3, genSameOptions(1, 0, 3, 0))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 0 def", 0, 3, genSameOptions(1, 0, 3, 0)).aiMustChoose(3));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 1, genSameOptions(1, 1, 1, 1))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 1, genSameOptions(1, 1, 1, 1)).aiMustChoose(1));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 3, genSameOptions(1, 1, 3, 1))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 3, genSameOptions(1, 1, 3, 1)).aiMustChoose(3));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 5 def", 0, 10, genSameOptions(1, 0, 10, 5))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 5 def", 0, 10, genSameOptions(1, 0, 10, 5)).aiMustChoose(10));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 10 def", 10, 10, genSameOptions(1, 0, 10, 10))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 10 def", 10, 10, genSameOptions(1, 0, 10, 10)).aiMustChoose(10));
// multiple targets // multiple targets
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 0 def", 0, 5, genSameOptions(3, 0, 3, 0))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 0 def", 0, 5, genSameOptions(3, 0, 3, 0)).aiMustChoose(2, 2, 1));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 0 def", 0, 5, genSameOptions(3, 0, 3, 0))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 1 def", 1, 5, genSameOptions(3, 1, 3, 1)).aiMustChoose(2, 2, 1));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 1 def", 1, 5, genSameOptions(3, 1, 3, 1))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 20 def", 0, 60, genSameOptions(3, 0, 60, 20)).aiMustChoose(20, 20, 20));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 1 def", 1, 5, genSameOptions(3, 1, 3, 1))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 20 def", 60, 60, genSameOptions(3, 0, 60, 20)).aiMustChoose(20, 20, 20));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 20 def", 0, 60, genSameOptions(3, 0, 60, 20)));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 20 def", 60, 60, genSameOptions(3, 0, 60, 20)));
// big lists // big lists
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "big list", 0, 100, genSameOptions(20, 0, 100, 0))); runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "big list", 0, 100, genSameOptions(20, 0, 100, 0)).aiMustChooseMany(20, 5));
runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "big list", 0, 100, genSameOptions(100, 0, 100, 0)).aiMustChooseMany(100, 1));
} }
} }
private static List<List<Integer>> genSameOptions(int amount, int min, int max, int def) { private static List<List<Integer>> genSameOptions(int options, int min, int max, int def) {
List<List<Integer>> res = new ArrayList<>(); List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < amount; i++) { for (int i = 0; i < options; i++) {
// min, max, default // min, max, default
res.add(Arrays.asList(min, max, def)); res.add(Arrays.asList(min, max, def));
} }

View file

@ -0,0 +1,54 @@
package mage.utils.testers;
import java.util.ArrayList;
import java.util.List;
/**
* Part of testable game dialogs
*
* @author JayDi85
*/
public class MultiAmountTestableResult extends BaseTestableResult {
List<Integer> selectedValues;
boolean aiAssertEnabled = false;
List<Integer> aiAssertValues = new ArrayList<>();
public void onFinish(String resDebugSource, boolean status, List<String> info, List<Integer> selectedValues) {
this.onFinish(resDebugSource, status, info);
this.selectedValues = selectedValues;
}
@Override
public String getResAssert() {
if (!this.aiAssertEnabled) {
return null;
}
// not finished
if (this.selectedValues == null) {
return null;
}
// wrong selection
String selected = this.selectedValues.toString();
String need = this.aiAssertValues.toString();
if (!selected.equals(need)) {
return String.format("Wrong selection: need %s, but get %s",
need,
selected
);
}
// all fine
return "";
}
@Override
public void onClear() {
super.onClear();
this.selectedValues = null;
}
}

View file

@ -0,0 +1,61 @@
package mage.utils.testers;
import mage.target.Target;
import java.util.List;
/**
* Part of testable game dialogs
*
* @author JayDi85
*/
public class TargetTestableResult extends BaseTestableResult {
Target target = null;
boolean aiAssertEnabled = false;
boolean aiAssertResStatus = false;
int aiAssertTargetsCount = 0;
public void onFinish(String resDebugSource, boolean status, List<String> info, Target target) {
this.onFinish(resDebugSource, status, info);
this.target = target;
}
@Override
public String getResAssert() {
if (!this.aiAssertEnabled) {
return null;
}
// not finished
if (this.target == null) {
return null;
}
// wrong choose
if (this.getResStatus() != this.aiAssertResStatus) {
return String.format("Wrong status: need %s, but get %s",
this.aiAssertResStatus,
this.getResStatus()
);
}
// wrong targets
if (this.target.getTargets().size() != this.aiAssertTargetsCount) {
return String.format("Wrong targets count: need %d, but get %d",
this.aiAssertTargetsCount,
this.target.getTargets().size()
);
}
// all fine
return "";
}
@Override
public void onClear() {
super.onClear();
this.target = null;
}
}

View file

@ -4,8 +4,6 @@ import mage.abilities.Ability;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import java.util.List;
/** /**
* Part of testable game dialogs * Part of testable game dialogs
* <p> * <p>
@ -17,7 +15,11 @@ import java.util.List;
* *
* @author JayDi85 * @author JayDi85
*/ */
interface TestableDialog { public interface TestableDialog {
void setRegNumber(Integer regNumber);
Integer getRegNumber();
String getGroup(); String getGroup();
@ -25,7 +27,20 @@ interface TestableDialog {
String getDescription(); String getDescription();
List<String> showDialog(Player player, Ability source, Game game, Player opponent); TestableResult getResult();
void showResult(Player player, Game game, String result); /**
* Prepare dialog before show, e.g. clear prev results
*/
void prepare();
/**
* Show game dialog to the user and save result
*/
void showDialog(Player player, Ability source, Game game, Player opponent);
/**
* Show result dialog to the user
*/
void showResult(Player player, Game game);
} }

View file

@ -8,8 +8,10 @@ import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import java.util.ArrayList; import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -55,7 +57,7 @@ import java.util.stream.Collectors;
*/ */
public class TestableDialogsRunner { public class TestableDialogsRunner {
private final List<TestableDialog> dialogs = new ArrayList<>(); private final Map<Integer, TestableDialog> dialogs = new LinkedHashMap<>();
static final int LAST_SELECTED_GROUP_ID = 997; static final int LAST_SELECTED_GROUP_ID = 997;
static final int LAST_SELECTED_DIALOG_ID = 998; static final int LAST_SELECTED_DIALOG_ID = 998;
@ -79,12 +81,14 @@ public class TestableDialogsRunner {
} }
void registerDialog(TestableDialog dialog) { void registerDialog(TestableDialog dialog) {
this.dialogs.add(dialog); Integer regNumber = this.dialogs.size() + 1;
dialog.setRegNumber(regNumber);
this.dialogs.put(regNumber, dialog);
} }
public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) { public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) {
// select group or fast links // select group or fast links
List<String> groups = this.dialogs.stream() List<String> groups = this.dialogs.values().stream()
.map(TestableDialog::getGroup) .map(TestableDialog::getGroup)
.distinct() .distinct()
.sorted() .sorted()
@ -129,8 +133,9 @@ public class TestableDialogsRunner {
// all fine, can show it and finish // all fine, can show it and finish
lastSelectedGroup = needGroup; lastSelectedGroup = needGroup;
lastSelectedDialog = needDialog; lastSelectedDialog = needDialog;
List<String> resInfo = needDialog.showDialog(player, source, game, opponent); needDialog.prepare();
needDialog.showResult(player, game, String.join("<br>", resInfo)); needDialog.showDialog(player, source, game, opponent);
needDialog.showResult(player, game);
} }
private Choice prepareSelectGroupChoice(List<String> groups) { private Choice prepareSelectGroupChoice(List<String> groups) {
@ -199,5 +204,9 @@ public class TestableDialogsRunner {
} }
return choice; return choice;
} }
public Collection<TestableDialog> getDialogs() {
return this.dialogs.values();
}
} }

View file

@ -0,0 +1,43 @@
package mage.utils.testers;
import java.util.List;
/**
* Part of testable game dialogs, must contain dialogs result
*
* @author JayDi85
*/
public interface TestableResult {
/**
* Get source code line with called dialog, use it as starting debug point
*/
String getResDebugSource();
/**
* Dialog's result
*/
boolean getResStatus();
/**
* Dialog's detail result
*/
List<String> getResDetails();
/**
* Save new result after show dialog
*/
void onFinish(String chooseDebugSource, boolean resStatus, List<String> resDetails);
boolean isFinished();
void onClear();
/**
* Assert dialog result
* - null - not ready (dev must setup wanted result)
* - empty - good
* - not empty - fail (return error message)
*/
String getResAssert();
}

View file

@ -25,7 +25,12 @@ public class ChatMessage implements Serializable {
} }
public enum MessageType { public enum MessageType {
USER_INFO, STATUS, GAME, TALK, WHISPER_FROM, WHISPER_TO USER_INFO, // system messages
STATUS, // system messages
GAME, // game logs
TALK, // public chat
WHISPER_FROM, // private chat income
WHISPER_TO // private chat outcome
} }
public enum SoundToPlay { public enum SoundToPlay {

View file

@ -67,6 +67,7 @@ public class GameView implements Serializable {
// TODO: implement and support in admin tools // TODO: implement and support in admin tools
private int totalErrorsCount; private int totalErrorsCount;
private int totalEffectsCount; private int totalEffectsCount;
private int gameCycle;
public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) { public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) {
Player createdForPlayer = null; Player createdForPlayer = null;
@ -214,6 +215,7 @@ public class GameView implements Serializable {
this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed; this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed;
this.totalErrorsCount = game.getTotalErrorsCount(); this.totalErrorsCount = game.getTotalErrorsCount();
this.totalEffectsCount = game.getTotalEffectsCount(); this.totalEffectsCount = game.getTotalEffectsCount();
this.gameCycle = game.getState().getApplyEffectsCounter();
} }
private void checkPaid(UUID uuid, StackAbility stackAbility) { private void checkPaid(UUID uuid, StackAbility stackAbility) {
@ -358,4 +360,8 @@ public class GameView implements Serializable {
public int getTotalEffectsCount() { public int getTotalEffectsCount() {
return this.totalEffectsCount; return this.totalEffectsCount;
} }
public int getGameCycle() {
return this.gameCycle;
}
} }

View file

@ -5,11 +5,13 @@ import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.common.CommanderChooseColorAbility; import mage.abilities.common.CommanderChooseColorAbility;
import mage.abilities.keyword.CompanionAbility; import mage.abilities.keyword.CompanionAbility;
import mage.abilities.keyword.StationLevelAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.decks.Constructed; import mage.cards.decks.Constructed;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidatorErrorType; import mage.cards.decks.DeckValidatorErrorType;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.ManaUtil; import mage.util.ManaUtil;
@ -51,9 +53,19 @@ public abstract class AbstractCommander extends Constructed {
protected abstract boolean checkBanned(Map<String, Integer> counts); protected abstract boolean checkBanned(Map<String, Integer> counts);
protected boolean checkCommander(Card commander, Set<Card> commanders) { protected boolean checkCommander(Card commander, Set<Card> commanders) {
return commander.hasCardTypeForDeckbuilding(CardType.CREATURE) && commander.isLegendary() if (commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance())) {
|| commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance()) return true;
|| (validators.stream().anyMatch(validator -> validator.specialCheck(commander)) && commanders.size() == 2); }
if (commander.isLegendary()
&& (commander.hasCardTypeForDeckbuilding(CardType.CREATURE)
|| commander.hasSubTypeForDeckbuilding(SubType.VEHICLE)
|| commander.hasSubTypeForDeckbuilding(SubType.SPACECRAFT)
&& CardUtil
.castStream(commander.getAbilities(), StationLevelAbility.class)
.anyMatch(StationLevelAbility::hasPT))) {
return true;
}
return commanders.size() == 2 && validators.stream().anyMatch(validator -> validator.specialCheck(commander));
} }
protected boolean checkPartners(Set<Card> commanders) { protected boolean checkPartners(Set<Card> commanders) {

View file

@ -0,0 +1,34 @@
package mage.deck;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
import mage.cards.decks.DeckValidatorErrorType;
/**
* @author resech
*/
public class FreeformUnlimited extends DeckValidator {
public FreeformUnlimited() {
this("Constructed - Freeform Unlimited", null);
}
public FreeformUnlimited(String name, String shortName) {
super(name, shortName);
}
@Override
public int getDeckMinSize() {
return 0;
}
@Override
public int getSideboardMinSize() {
return 0;
}
@Override
public boolean validate(Deck deck) {
return true;
}
}

View file

@ -32,7 +32,6 @@ public class Historic extends Constructed {
banned.add("Agent of Treachery"); banned.add("Agent of Treachery");
banned.add("Brainstorm"); banned.add("Brainstorm");
banned.add("Channel"); banned.add("Channel");
banned.add("Counterspell");
banned.add("Dark Ritual"); banned.add("Dark Ritual");
banned.add("Demonic Tutor"); banned.add("Demonic Tutor");
banned.add("Fires of Invention"); banned.add("Fires of Invention");

View file

@ -19,10 +19,13 @@ public class Standard extends Constructed {
setCodes.addAll(makeLegalSets()); setCodes.addAll(makeLegalSets());
banned.add("The Meathook Massacre"); banned.add("Abuelo's Awakening");
banned.add("Fable of the Mirror-Breaker"); banned.add("Cori-Steel Cutter");
banned.add("Reckoner Bankbuster"); banned.add("Heartfire Hero");
banned.add("Invoke Despair"); banned.add("Hopeless Nightmare");
banned.add("Monstrous Rage");
banned.add("This Town Ain't Big Enough");
banned.add("Up the Beanstalk");
} }
static List<String> makeLegalSets() { static List<String> makeLegalSets() {

View file

@ -23,6 +23,7 @@ import mage.game.stack.StackAbility;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.player.ai.ma.optimizers.TreeOptimizer; import mage.player.ai.ma.optimizers.TreeOptimizer;
import mage.player.ai.ma.optimizers.impl.*; import mage.player.ai.ma.optimizers.impl.*;
import mage.player.ai.score.GameStateEvaluator2;
import mage.player.ai.util.CombatInfo; import mage.player.ai.util.CombatInfo;
import mage.player.ai.util.CombatUtil; import mage.player.ai.util.CombatUtil;
import mage.players.Player; import mage.players.Player;
@ -113,6 +114,13 @@ public class ComputerPlayer6 extends ComputerPlayer {
this.actionCache = player.actionCache; this.actionCache = player.actionCache;
} }
/**
* Change simulation timeout - used for AI stability tests only
*/
public void setMaxThinkTimeSecs(int maxThinkTimeSecs) {
this.maxThinkTimeSecs = maxThinkTimeSecs;
}
@Override @Override
public ComputerPlayer6 copy() { public ComputerPlayer6 copy() {
return new ComputerPlayer6(this); return new ComputerPlayer6(this);
@ -205,10 +213,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
logger.trace("Add Action [" + depth + "] " + node.getAbilities().toString() + " a: " + alpha + " b: " + beta); logger.trace("Add Action [" + depth + "] " + node.getAbilities().toString() + " a: " + alpha + " b: " + beta);
} }
Game game = node.getGame(); Game game = node.getGame();
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
&& Thread.interrupted()) { logger.debug("AI game sim interrupted by timeout");
Thread.currentThread().interrupt();
logger.debug("interrupted");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
} }
// Condition to stop deeper simulation // Condition to stop deeper simulation
@ -399,7 +405,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (effect != null if (effect != null
&& stackObject.getControllerId().equals(playerId)) { && stackObject.getControllerId().equals(playerId)) {
Target target = effect.getTarget(); Target target = effect.getTarget();
if (!target.isChoiceCompleted(game)) { if (!target.isChoiceCompleted(getId(), (StackAbility) stackObject, game)) {
for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) { for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) {
Game sim = game.createSimulationForAI(); Game sim = game.createSimulationForAI();
StackAbility newAbility = (StackAbility) stackObject.copy(); StackAbility newAbility = (StackAbility) stackObject.copy();
@ -430,6 +436,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
* @return * @return
*/ */
protected Integer addActionsTimed() { protected Integer addActionsTimed() {
// TODO: all actions added and calculated one by one,
// multithreading do not supported here
// run new game simulation in parallel thread // run new game simulation in parallel thread
FutureTask<Integer> task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE)); FutureTask<Integer> task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE));
threadPoolSimulations.execute(task); threadPoolSimulations.execute(task);
@ -445,15 +453,23 @@ public class ComputerPlayer6 extends ComputerPlayer {
} }
} catch (TimeoutException | InterruptedException e) { } catch (TimeoutException | InterruptedException e) {
// AI thinks too long // AI thinks too long
logger.info("ai simulating - timed out"); // how-to fix: look at stack info - it can contain bad ability with infinite choose dialog
logger.warn("");
logger.warn("AI player thinks too long (report it to github):");
logger.warn(" - player: " + getName());
logger.warn(" - battlefield size: " + root.game.getBattlefield().getAllPermanents().size());
logger.warn(" - stack: " + root.game.getStack());
logger.warn(" - game: " + root.game);
printFreezeNode(root);
logger.warn("");
task.cancel(true); task.cancel(true);
} catch (ExecutionException e) { } catch (ExecutionException e) {
// game error // game error
logger.error("AI simulation catch game error: " + e, e); logger.error("AI player catch game error in simulation - " + getName() + " - " + root.game + ": " + e, e);
task.cancel(true); task.cancel(true);
// real games: must catch and log // real games: must catch and log
// unit tests: must raise again for fast fail // unit tests: must raise again for fast fail
if (this.isTestsMode()) { if (this.isTestMode() && this.isFastFailInTestMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e, e); throw new IllegalStateException("One of the simulated games raise the error: " + e, e);
} }
} catch (Throwable e) { } catch (Throwable e) {
@ -465,11 +481,33 @@ public class ComputerPlayer6 extends ComputerPlayer {
return 0; return 0;
} }
private void printFreezeNode(SimulationNode2 root) {
// print simple tree - there are possible multiple child nodes, but ignore it - same for abilities
List<String> chain = new ArrayList<>();
SimulationNode2 node = root;
while (node != null) {
if (node.abilities != null && !node.abilities.isEmpty()) {
Ability ability = node.abilities.get(0);
String sourceInfo = CardUtil.getSourceIdName(node.game, ability);
chain.add(String.format("%s: %s",
(sourceInfo.isEmpty() ? "unknown" : sourceInfo),
ability
));
}
node = node.children == null || node.children.isEmpty() ? null : node.children.get(0);
}
logger.warn("Possible freeze chain:");
if (root != null && chain.isEmpty()) {
logger.warn(" - unknown use case"); // maybe can't finish any calc, maybe related to target options, I don't know
}
chain.forEach(s -> {
logger.warn(" - " + s);
});
}
protected int simulatePriority(SimulationNode2 node, Game game, int depth, int alpha, int beta) { protected int simulatePriority(SimulationNode2 node, Game game, int depth, int alpha, int beta) {
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
&& Thread.interrupted()) { logger.debug("AI game sim interrupted by timeout");
Thread.currentThread().interrupt();
logger.info("interrupted");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
} }
node.setGameValue(game.getState().getValue(true).hashCode()); node.setGameValue(game.getState().getValue(true).hashCode());
@ -497,9 +535,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
int bestValSubNodes = Integer.MIN_VALUE; int bestValSubNodes = Integer.MIN_VALUE;
for (Ability action : allActions) { for (Ability action : allActions) {
actionNumber++; actionNumber++;
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
&& Thread.interrupted()) {
Thread.currentThread().interrupt();
logger.info("Sim Prio [" + depth + "] -- interrupted"); logger.info("Sim Prio [" + depth + "] -- interrupted");
break; break;
} }
@ -848,10 +884,12 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (targets.isEmpty()) { if (targets.isEmpty()) {
return super.chooseTarget(outcome, cards, target, source, game); return super.chooseTarget(outcome, cards, target, source, game);
} }
if (!target.isChoiceCompleted(game)) {
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
if (!target.isChoiceCompleted(abilityControllerId, source, game)) {
for (UUID targetId : targets) { for (UUID targetId : targets) {
target.addTarget(targetId, source, game); target.addTarget(targetId, source, game);
if (target.isChoiceCompleted(game)) { if (target.isChoiceCompleted(abilityControllerId, source, game)) {
targets.clear(); targets.clear();
return true; return true;
} }
@ -866,10 +904,12 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (targets.isEmpty()) { if (targets.isEmpty()) {
return super.choose(outcome, cards, target, source, game); return super.choose(outcome, cards, target, source, game);
} }
if (!target.isChoiceCompleted(game)) {
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
if (!target.isChoiceCompleted(abilityControllerId, source, game)) {
for (UUID targetId : targets) { for (UUID targetId : targets) {
target.add(targetId, game); target.add(targetId, game);
if (target.isChoiceCompleted(game)) { if (target.isChoiceCompleted(abilityControllerId, source, game)) {
targets.clear(); targets.clear();
return true; return true;
} }

View file

@ -3,6 +3,7 @@ package mage.player.ai;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.constants.RangeOfInfluence; import mage.constants.RangeOfInfluence;
import mage.game.Game; import mage.game.Game;
import mage.player.ai.score.GameStateEvaluator2;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.Date; import java.util.Date;
@ -111,8 +112,6 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
protected void calculateActions(Game game) { protected void calculateActions(Game game) {
if (!getNextAction(game)) { if (!getNextAction(game)) {
//logger.info("--- calculating possible actions for " + this.getName() + " on " + game.toString());
Date startTime = new Date();
currentScore = GameStateEvaluator2.evaluate(playerId, game).getTotalScore(); currentScore = GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
Game sim = createSimulation(game); Game sim = createSimulation(game);
SimulationNode2.resetCount(); SimulationNode2.resetCount();
@ -143,17 +142,9 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
} }
} }
} else { } else {
logger.info('[' + game.getPlayer(playerId).getName() + "][pre] Action: skip"); // nothing to choose or freeze/infinite game
logger.info("AI player can't find next action: " + getName());
} }
Date endTime = new Date();
this.setLastThinkTime((endTime.getTime() - startTime.getTime()));
/*
logger.warn("Last think time: " + this.getLastThinkTime()
+ "; actions: " + actions.size()
+ "; hand: " + this.getHand().size()
+ "; permanents: " + game.getBattlefield().getAllPermanents().size());
*/
} else { } else {
logger.debug("Next Action exists!"); logger.debug("Next Action exists!");
} }

View file

@ -13,7 +13,7 @@ import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep; import mage.game.turn.CombatDamageStep;
import mage.game.turn.EndOfCombatStep; import mage.game.turn.EndOfCombatStep;
import mage.game.turn.Step; import mage.game.turn.Step;
import mage.player.ai.GameStateEvaluator2; import mage.player.ai.score.GameStateEvaluator2;
import mage.players.Player; import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -0,0 +1,152 @@
package mage.player.ai;
import mage.MageItem;
import mage.MageObject;
import mage.cards.Card;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.player.ai.score.GameStateEvaluator2;
import mage.players.PlayableObjectsList;
import mage.players.Player;
import java.util.Comparator;
import java.util.UUID;
/**
* AI related code - compare and sort possible targets due target/effect type
*
* @author JayDi85
*/
public class PossibleTargetsComparator {
UUID abilityControllerId;
Game game;
PlayableObjectsList playableItems = new PlayableObjectsList();
public PossibleTargetsComparator(UUID abilityControllerId, Game game) {
this.abilityControllerId = abilityControllerId;
this.game = game;
}
public void findPlayableItems() {
this.playableItems = this.game.getPlayer(this.abilityControllerId).getPlayableObjects(this.game, Zone.ALL);
}
private int getScoreFromBattlefield(MageItem item) {
if (item instanceof Permanent) {
// use battlefield score instead simple life
return GameStateEvaluator2.evaluatePermanent((Permanent) item, game, false);
} else {
return getScoreFromLife(item);
}
}
private String getName(MageItem item) {
if (item instanceof Player) {
return ((Player) item).getName();
} else if (item instanceof MageObject) {
return ((MageObject) item).getName();
} else {
return "unknown";
}
}
public static int getLifeForDamage(MageItem item, Game game) {
int res = 0;
if (item instanceof Player) {
res = ((Player) item).getLife();
} else if (item instanceof Card) {
Card card = (Card) item;
if (card.isPlaneswalker(game)) {
res = card.getCounters(game).getCount(CounterType.LOYALTY);
} else if (card.isBattle(game)) {
res = card.getCounters(game).getCount(CounterType.DEFENSE);
} else {
int damage = 0;
if (card instanceof Permanent) {
damage = ((Permanent) card).getDamage();
}
res = Math.max(0, card.getToughness().getValue() - damage);
}
}
return res;
}
private int getScoreFromLife(MageItem item) {
// TODO: replace permanent/card life by battlefield score?
int res = getLifeForDamage(item, game);
if (res == 0 && item instanceof Card) {
res = ((Card) item).getManaValue();
}
return res;
}
private boolean isMyItem(MageItem item) {
return PossibleTargetsSelector.isMyItem(this.abilityControllerId, item);
}
// sort by name-id at the end, so AI will use same choices in all simulations
private final Comparator<MageItem> BY_NAME = (o1, o2) -> getName(o2).compareTo(getName(o1));
private final Comparator<MageItem> BY_ID = Comparator.comparing(MageItem::getId);
private final Comparator<MageItem> BY_ME = (o1, o2) -> Boolean.compare(
isMyItem(o2),
isMyItem(o1)
);
private final Comparator<MageItem> BY_BIGGER_SCORE = (o1, o2) -> Integer.compare(
getScoreFromBattlefield(o2),
getScoreFromBattlefield(o1)
);
private final Comparator<MageItem> BY_PLAYABLE = (o1, o2) -> Boolean.compare(
this.playableItems.containsObject(o2.getId()),
this.playableItems.containsObject(o1.getId())
);
private final Comparator<MageItem> BY_LAND = (o1, o2) -> {
boolean isLand1 = o1 instanceof MageObject && ((MageObject) o1).isLand(game);
boolean isLand2 = o2 instanceof MageObject && ((MageObject) o2).isLand(game);
return Boolean.compare(isLand2, isLand1);
};
private final Comparator<MageItem> BY_TYPE_PLAYER = (o1, o2) -> Boolean.compare(
o2 instanceof Player,
o1 instanceof Player
);
private final Comparator<MageItem> BY_TYPE_PLANESWALKER = (o1, o2) -> {
boolean isPlaneswalker1 = o1 instanceof MageObject && ((MageObject) o1).isPlaneswalker(game);
boolean isPlaneswalker2 = o2 instanceof MageObject && ((MageObject) o2).isPlaneswalker(game);
return Boolean.compare(isPlaneswalker2, isPlaneswalker1);
};
private final Comparator<MageItem> BY_TYPE_BATTLE = (o1, o2) -> {
boolean isBattle1 = o1 instanceof MageObject && ((MageObject) o1).isBattle(game);
boolean isBattle2 = o2 instanceof MageObject && ((MageObject) o2).isBattle(game);
return Boolean.compare(isBattle2, isBattle1);
};
private final Comparator<MageItem> BY_TYPES = BY_TYPE_PLANESWALKER
.thenComparing(BY_TYPE_BATTLE)
.thenComparing(BY_TYPE_PLAYER);
/**
* Default sorting for good effects - put the biggest items to the top
*/
public final Comparator<MageItem> ANY_MOST_VALUABLE_FIRST = BY_TYPES
.thenComparing(BY_BIGGER_SCORE)
.thenComparing(BY_NAME)
.thenComparing(BY_ID);
public final Comparator<MageItem> ANY_MOST_VALUABLE_LAST = ANY_MOST_VALUABLE_FIRST.reversed();
/**
* Sorting for discard effects - put the biggest unplayable at the top, lands at the end anyway
*/
public final Comparator<MageItem> ANY_UNPLAYABLE_AND_USELESS = BY_LAND.reversed()
.thenComparing(BY_PLAYABLE.reversed())
.thenComparing(ANY_MOST_VALUABLE_FIRST);
}

View file

@ -0,0 +1,187 @@
package mage.player.ai;
import mage.MageItem;
import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.ControllableOrOwnerable;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
import mage.target.common.TargetDiscard;
import java.util.*;
import java.util.stream.Collectors;
/**
* AI related code - find possible targets and sort it due priority
*
* @author JayDi85
*/
public class PossibleTargetsSelector {
Outcome outcome;
Target target;
UUID abilityControllerId;
Ability source;
Game game;
PossibleTargetsComparator comparators;
// possible targets lists
List<MageItem> me = new ArrayList<>();
List<MageItem> opponents = new ArrayList<>();
List<MageItem> any = new ArrayList<>(); // for outcomes with any target like copy
public PossibleTargetsSelector(Outcome outcome, Target target, UUID abilityControllerId, Ability source, Game game) {
this.outcome = outcome;
this.target = target;
this.abilityControllerId = abilityControllerId;
this.source = source;
this.game = game;
this.comparators = new PossibleTargetsComparator(abilityControllerId, game);
}
public void findNewTargets(Set<UUID> fromTargetsList) {
// collect new valid targets
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game, fromTargetsList).stream()
.filter(id -> !target.contains(id))
.filter(id -> target.canTarget(abilityControllerId, id, source, game))
.map(id -> {
Player player = game.getPlayer(id);
if (player != null) {
return player;
} else {
return game.getObject(id);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// split targets between me and opponents
found.forEach(item -> {
if (isMyItem(abilityControllerId, item)) {
this.me.add(item);
} else {
this.opponents.add(item);
}
this.any.add(item);
});
if (target instanceof TargetDiscard) {
// sort due unplayable
sortByUnplayableAndUseless();
} else {
// sort due good/bad outcome
sortByMostValuableTargets();
}
}
/**
* Sorting for any good/bad effects
*/
private void sortByMostValuableTargets() {
if (isGoodEffect()) {
// for good effect must choose the biggest objects
this.me.sort(comparators.ANY_MOST_VALUABLE_FIRST);
this.opponents.sort(comparators.ANY_MOST_VALUABLE_LAST);
this.any.sort(comparators.ANY_MOST_VALUABLE_FIRST);
} else {
// for bad effect must choose the smallest objects
this.me.sort(comparators.ANY_MOST_VALUABLE_LAST);
this.opponents.sort(comparators.ANY_MOST_VALUABLE_FIRST);
this.any.sort(comparators.ANY_MOST_VALUABLE_LAST);
}
}
/**
* Sorting for discard
*/
private void sortByUnplayableAndUseless() {
// used
// no good or bad effect - you must choose
comparators.findPlayableItems();
this.me.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
this.opponents.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
this.any.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
}
/**
* Priority targets. Try to use as much as possible.
*/
public List<MageItem> getGoodTargets() {
if (isAnyEffect()) {
return this.any;
}
if (isGoodEffect()) {
return this.me;
} else {
return this.opponents;
}
}
/**
* Optional targets. Try to ignore bad targets (e.g. opponent's creatures for your good effect).
*/
public List<MageItem> getBadTargets() {
if (isAnyEffect()) {
return Collections.emptyList();
}
if (isGoodEffect()) {
return this.opponents;
} else {
return this.me;
}
}
public static boolean isMyItem(UUID abilityControllerId, MageItem item) {
if (item instanceof Player) {
return item.getId().equals(abilityControllerId);
} else if (item instanceof ControllableOrOwnerable) {
return ((ControllableOrOwnerable) item).getControllerOrOwnerId().equals(abilityControllerId);
}
return false;
}
private boolean isAnyEffect() {
boolean isAnyEffect = outcome.anyTargetHasSameValue();
if (hasGoodExile()) {
isAnyEffect = true;
}
return isAnyEffect;
}
private boolean isGoodEffect() {
boolean isGoodEffect = outcome.isGood();
if (hasGoodExile()) {
isGoodEffect = true;
}
return isGoodEffect;
}
private boolean hasGoodExile() {
// exile workaround: exile is bad, but exile from library or graveyard in most cases is good
// (more exiled -- more good things you get, e.g. delve's pay or search cards with same name)
if (outcome == Outcome.Exile) {
if (Zone.GRAVEYARD.match(target.getZone())
|| Zone.LIBRARY.match(target.getZone())) {
// TargetCardInGraveyardBattlefieldOrStack - used for additional payment like Craft, so do not allow big cards for it
if (!(target instanceof TargetCardInGraveyardBattlefieldOrStack)) {
return true;
}
}
}
return false;
}
boolean hasAnyTargets() {
return !this.any.isEmpty();
}
}

View file

@ -1,4 +1,4 @@
package mage.player.ai.ma; package mage.player.ai.score;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;

View file

@ -1,8 +1,7 @@
package mage.player.ai; package mage.player.ai.score;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.player.ai.ma.ArtificialScoringSystem;
import mage.players.Player; import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -1,4 +1,4 @@
package mage.player.ai.ma; package mage.player.ai.score;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.keyword.*; import mage.abilities.keyword.*;
@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* TODO: outdated, replace by edh or commander brackets ability score
* @author nantuko * @author nantuko
*/ */
public final class MagicAbility { public final class MagicAbility {
@ -18,10 +19,10 @@ public final class MagicAbility {
put(DoubleStrikeAbility.getInstance().getRule(), 100); put(DoubleStrikeAbility.getInstance().getRule(), 100);
put(new ExaltedAbility().getRule(), 10); put(new ExaltedAbility().getRule(), 10);
put(FirstStrikeAbility.getInstance().getRule(), 50); put(FirstStrikeAbility.getInstance().getRule(), 50);
put(FlashAbility.getInstance().getRule(), 0); put(FlashAbility.getInstance().getRule(), 20);
put(FlyingAbility.getInstance().getRule(), 50); put(FlyingAbility.getInstance().getRule(), 50);
put(new ForestwalkAbility().getRule(), 10); put(new ForestwalkAbility().getRule(), 10);
put(HasteAbility.getInstance().getRule(), 0); put(HasteAbility.getInstance().getRule(), 20);
put(IndestructibleAbility.getInstance().getRule(), 150); put(IndestructibleAbility.getInstance().getRule(), 150);
put(InfectAbility.getInstance().getRule(), 60); put(InfectAbility.getInstance().getRule(), 60);
put(IntimidateAbility.getInstance().getRule(), 50); put(IntimidateAbility.getInstance().getRule(), 50);
@ -46,7 +47,7 @@ public final class MagicAbility {
if (!scores.containsKey(ability.getRule())) { if (!scores.containsKey(ability.getRule())) {
//System.err.println("Couldn't find ability score: " + ability.getClass().getSimpleName() + " - " + ability.toString()); //System.err.println("Couldn't find ability score: " + ability.getClass().getSimpleName() + " - " + ability.toString());
//TODO: add handling protection from ..., levelup, kicker, etc. abilities //TODO: add handling protection from ..., levelup, kicker, etc. abilities
return 0; return 2; // more abilities - more score in any use cases
} }
return scores.get(ability.getRule()); return scores.get(ability.getRule());
} }

View file

@ -1,65 +0,0 @@
package mage.player.ai.simulators;
import mage.abilities.ActivatedAbility;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.player.ai.ComputerPlayer;
import mage.player.ai.PermanentEvaluator;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class ActionSimulator {
private ComputerPlayer player;
private List<Card> playableInstants = new ArrayList<>();
private List<ActivatedAbility> playableAbilities = new ArrayList<>();
private Game game;
public ActionSimulator(ComputerPlayer player) {
this.player = player;
}
public void simulate(Game game) {
}
public int evaluateState() {
// must find all leaved opponents
Player opponent = game.getPlayer(game.getOpponents(player.getId(), false).stream().findFirst().orElse(null));
if (opponent == null) {
return Integer.MAX_VALUE;
}
if (game.checkIfGameIsOver()) {
if (player.hasLost() || opponent.hasWon()) {
return Integer.MIN_VALUE;
}
if (opponent.hasLost() || player.hasWon()) {
return Integer.MAX_VALUE;
}
}
int value = player.getLife();
value -= opponent.getLife();
PermanentEvaluator evaluator = new PermanentEvaluator();
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
value += evaluator.evaluate(permanent, game);
}
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
value -= evaluator.evaluate(permanent, game);
}
value += player.getHand().size();
value -= opponent.getHand().size();
return value;
}
}

View file

@ -1,148 +0,0 @@
package mage.player.ai.simulators;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CombatGroupSimulator implements Serializable {
public List<CreatureSimulator> attackers = new ArrayList<>();
public List<CreatureSimulator> blockers = new ArrayList<>();
public UUID defenderId;
public boolean defenderIsPlaneswalker;
public int unblockedDamage;
private CreatureSimulator attacker;
public CombatGroupSimulator(UUID defenderId, List<UUID> attackers, List<UUID> blockers, Game game) {
this.defenderId = defenderId;
for (UUID attackerId: attackers) {
Permanent permanent = game.getPermanent(attackerId);
this.attackers.add(new CreatureSimulator(permanent));
}
for (UUID blockerId: blockers) {
Permanent permanent = game.getPermanent(blockerId);
this.blockers.add(new CreatureSimulator(permanent));
}
//NOTE: assumes no banding
attacker = this.attackers.get(0);
}
private boolean hasFirstOrDoubleStrike() {
for (CreatureSimulator creature: attackers) {
if (creature.hasDoubleStrike || creature.hasFirstStrike)
return true;
}
for (CreatureSimulator creature: blockers) {
if (creature.hasDoubleStrike || creature.hasFirstStrike)
return true;
}
return false;
}
public boolean canBlock(Permanent blocker, Game game) {
return blocker.canBlock(attacker.id, game);
}
public void simulateCombat(Game game) {
unblockedDamage = 0;
if (hasFirstOrDoubleStrike())
assignDamage(true, game);
assignDamage(false, game);
}
private void assignDamage(boolean first, Game game) {
if (blockers.isEmpty()) {
if (canDamage(attacker, first))
unblockedDamage += attacker.power;
}
else if (blockers.size() == 1) {
CreatureSimulator blocker = blockers.get(0);
if (canDamage(attacker, first)) {
if (attacker.hasTrample) {
int lethalDamage = blocker.getLethalDamage(game);
if (attacker.power > lethalDamage) {
blocker.damage += lethalDamage;
unblockedDamage += attacker.power - lethalDamage;
}
else {
blocker.damage += attacker.power;
}
}
}
if (canDamage(blocker, first)) {
attacker.damage += blocker.power;
}
}
else {
int damage = attacker.power;
for (CreatureSimulator blocker: blockers) {
if (damage > 0 && canDamage(attacker, first)) {
int lethalDamage = blocker.getLethalDamage(game);
if (damage > lethalDamage) {
blocker.damage += lethalDamage;
damage -= lethalDamage;
}
else {
blocker.damage += damage;
damage = 0;
}
}
if (canDamage(blocker, first)) {
attacker.damage += blocker.power;
}
}
if (damage > 0) {
if (attacker.hasTrample) {
unblockedDamage += damage;
}
else {
blockers.get(0).damage += damage;
}
}
}
}
private boolean canDamage(CreatureSimulator creature, boolean first) {
if (first && (creature.hasFirstStrike || creature.hasDoubleStrike))
return true;
if (!first && (!creature.hasFirstStrike || creature.hasDoubleStrike))
return true;
return false;
}
/**
* returns 3 attacker survives blockers destroyed
* returns 2 both destroyed
* returns 1 both survive
* returns 0 attacker destroyed blockers survive
*
* @return int
*/
public int evaluateCombat() {
int survivingBlockers = 0;
for (CreatureSimulator blocker: blockers) {
if (blocker.damage < blocker.toughness)
survivingBlockers++;
}
if (attacker.isDead()) {
if (survivingBlockers > 0) {
return 0;
}
return 2;
}
else {
if (survivingBlockers > 0) {
return 1;
}
return 3;
}
}
}

View file

@ -1,86 +0,0 @@
package mage.player.ai.simulators;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CombatSimulator implements Serializable {
public List<CombatGroupSimulator> groups = new ArrayList<>();
public List<UUID> defenders = new ArrayList<>();
public Map<UUID, Integer> playersLife = new HashMap<>();
public Map<UUID, Integer> planeswalkerLoyalty = new HashMap<>();
public UUID attackerId;
public int rating = 0;
public static CombatSimulator load(Game game) {
CombatSimulator simCombat = new CombatSimulator();
for (CombatGroup group: game.getCombat().getGroups()) {
simCombat.groups.add(new CombatGroupSimulator(group.getDefenderId(), group.getAttackers(), group.getBlockers(), game));
}
for (UUID defenderId: game.getCombat().getDefenders()) {
simCombat.defenders.add(defenderId);
Player player = game.getPlayer(defenderId);
if (player != null) {
simCombat.playersLife.put(defenderId, player.getLife());
}
else {
Permanent permanent = game.getPermanent(defenderId);
simCombat.planeswalkerLoyalty.put(defenderId, permanent.getCounters(game).getCount(CounterType.LOYALTY));
}
}
return simCombat;
}
public CombatSimulator() {}
public void clear() {
groups.clear();
defenders.clear();
attackerId = null;
}
public void simulate(Game game) {
for (CombatGroupSimulator group: groups) {
group.simulateCombat(game);
}
}
public int evaluate() {
Map<UUID, Integer> damage = new HashMap<>();
int result = 0;
for (CombatGroupSimulator group: groups) {
if (!damage.containsKey(group.defenderId)) {
damage.put(group.defenderId, group.unblockedDamage);
}
else {
damage.put(group.defenderId, damage.get(group.defenderId) + group.unblockedDamage);
}
}
//check for lethal damage to player
for (Entry<UUID, Integer> entry: playersLife.entrySet()) {
if (damage.containsKey(entry.getKey()) && entry.getValue() <= damage.get(entry.getKey())) {
//TODO: check for protection
//NOTE: not applicable for mulitplayer games
return Integer.MAX_VALUE;
}
}
for (CombatGroupSimulator group: groups) {
result += group.evaluateCombat();
}
rating = result;
return result;
}
}

View file

@ -1,57 +0,0 @@
package mage.player.ai.simulators;
import mage.abilities.keyword.DoubleStrikeAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CreatureSimulator implements Serializable {
public UUID id;
public int damage;
public int power;
public int toughness;
public boolean hasFirstStrike;
public boolean hasDoubleStrike;
public boolean hasTrample;
public Permanent permanent;
public CreatureSimulator(Permanent permanent) {
this.id = permanent.getId();
this.damage = permanent.getDamage();
this.power = permanent.getPower().getValue();
this.toughness = permanent.getToughness().getValue();
this.hasDoubleStrike = permanent.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
this.hasFirstStrike = permanent.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId());
this.hasTrample = permanent.getAbilities().containsKey(TrampleAbility.getInstance().getId());
this.permanent = permanent;
}
public boolean isDead() {
return damage >= toughness;
}
public int getLethalDamage(Game game) {
List<FilterCreaturePermanent> usePowerInsteadOfToughnessForDamageLethalityFilters = game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters();
/*
* for handling Zilortha, Strength Incarnate:
* 2020-04-17
* Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on.
*/
boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream()
.anyMatch(filter -> filter.match(permanent, game));
int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ?
// Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isnt destroyed unless it has at least 1 damage marked on it.
Math.max(power, 1) : toughness;
return Math.max(lethalDamageThreshold - damage, 0);
}
}

View file

@ -196,7 +196,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
} catch (ExecutionException e) { } catch (ExecutionException e) {
// real games: must catch and log // real games: must catch and log
// unit tests: must raise again for fast fail // unit tests: must raise again for fast fail
if (this.isTestsMode()) { if (this.isTestMode() && this.isFastFailInTestMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e, e); throw new IllegalStateException("One of the simulated games raise the error: " + e, e);
} }
} }

View file

@ -288,6 +288,16 @@ public final class SimulatedPlayerMCTS extends MCTSPlayer {
@Override @Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
// nothing to choose
target.prepareAmount(source, game);
if (target.getAmountRemaining() <= 0) {
return false;
}
if (target.getMaxNumberOfTargets() == 0 && target.getMinNumberOfTargets() == 0) {
return false;
}
Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game); Set<UUID> possibleTargets = target.possibleTargets(playerId, source, game);
if (possibleTargets.isEmpty()) { if (possibleTargets.isEmpty()) {
return !target.isRequired(source); return !target.isRequired(source);

View file

@ -297,7 +297,7 @@ public class HumanPlayer extends PlayerImpl {
return; return;
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Setting game priority for " + getId() + " [" + DebugUtil.getMethodNameWithSource(1) + ']'); logger.debug("Setting game priority for " + getId() + " [" + DebugUtil.getMethodNameWithSource(1, "method") + ']');
} }
game.getState().setPriorityPlayerId(getId()); game.getState().setPriorityPlayerId(getId());
} }
@ -328,7 +328,7 @@ public class HumanPlayer extends PlayerImpl {
while (loop) { while (loop) {
// start waiting for next answer // start waiting for next answer
response.clear(); response.clear();
response.setActiveAction(game, DebugUtil.getMethodNameWithSource(1)); response.setActiveAction(game, DebugUtil.getMethodNameWithSource(1, "method"));
game.resumeTimer(getTurnControlledBy()); game.resumeTimer(getTurnControlledBy());
responseOpenedForAnswer = true; responseOpenedForAnswer = true;
@ -690,12 +690,8 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
// choose one or multiple permanents // choose one or multiple targets
UUID abilityControllerId = playerId; UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
if (target.getTargetController() != null
&& target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
}
if (options == null) { if (options == null) {
options = new HashMap<>(); options = new HashMap<>();
} }
@ -782,11 +778,7 @@ public class HumanPlayer extends PlayerImpl {
} }
// choose one or multiple targets // choose one or multiple targets
UUID abilityControllerId = playerId; UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
if (target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
}
Map<String, Serializable> options = new HashMap<>(); Map<String, Serializable> options = new HashMap<>();
while (canRespond()) { while (canRespond()) {
@ -869,13 +861,7 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
UUID abilityControllerId; UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
if (target.getTargetController() != null
&& target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
} else {
abilityControllerId = playerId;
}
while (canRespond()) { while (canRespond()) {
@ -966,13 +952,7 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
UUID abilityControllerId; UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
if (target.getTargetController() != null
&& target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
} else {
abilityControllerId = playerId;
}
while (canRespond()) { while (canRespond()) {
boolean required = target.isRequiredExplicitlySet() ? target.isRequired() : target.isRequired(source); boolean required = target.isRequiredExplicitlySet() ? target.isRequired() : target.isRequired(source);
@ -1042,14 +1022,20 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
// nothing to choose
target.prepareAmount(source, game);
if (target.getAmountRemaining() <= 0) {
return false;
}
if (target.getMaxNumberOfTargets() == 0 && target.getMinNumberOfTargets() == 0) {
return false;
}
if (source == null) { if (source == null) {
return false; return false;
} }
UUID abilityControllerId = playerId; UUID abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
if (target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
}
int amountTotal = target.getAmountTotal(game, source); int amountTotal = target.getAmountTotal(game, source);
if (amountTotal == 0) { if (amountTotal == 0) {

View file

@ -196,6 +196,7 @@
<deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed.jar" className="mage.deck.OldSchool9394EC"/> <deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed.jar" className="mage.deck.OldSchool9394EC"/>
<deckType name="Constructed - Premodern" jar="mage-deck-constructed.jar" className="mage.deck.Premodern"/> <deckType name="Constructed - Premodern" jar="mage-deck-constructed.jar" className="mage.deck.Premodern"/>
<deckType name="Constructed - Freeform" jar="mage-deck-constructed.jar" className="mage.deck.Freeform"/> <deckType name="Constructed - Freeform" jar="mage-deck-constructed.jar" className="mage.deck.Freeform"/>
<deckType name="Constructed - Freeform Unlimited" jar="mage-deck-constructed.jar" className="mage.deck.FreeformUnlimited"/>
<deckType name="Variant Magic - Commander" jar="mage-deck-constructed.jar" className="mage.deck.Commander"/> <deckType name="Variant Magic - Commander" jar="mage-deck-constructed.jar" className="mage.deck.Commander"/>
<deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed.jar" className="mage.deck.DuelCommander"/> <deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed.jar" className="mage.deck.DuelCommander"/>
<deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed.jar" className="mage.deck.MTGO1v1Commander"/> <deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed.jar" className="mage.deck.MTGO1v1Commander"/>

View file

@ -192,11 +192,6 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency> <dependency>
<groupId>javax.xml.bind</groupId> <groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId> <artifactId>jaxb-api</artifactId>
@ -218,7 +213,7 @@
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId> <artifactId>shiro-core</artifactId>
<version>1.8.0</version> <version>1.13.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.mail</groupId> <groupId>javax.mail</groupId>

View file

@ -190,6 +190,7 @@
<deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.OldSchool9394EC"/> <deckType name="Constructed - Old School 93/94 - EC Rules" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.OldSchool9394EC"/>
<deckType name="Constructed - Premodern" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Premodern"/> <deckType name="Constructed - Premodern" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Premodern"/>
<deckType name="Constructed - Freeform" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Freeform"/> <deckType name="Constructed - Freeform" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Freeform"/>
<deckType name="Constructed - Freeform Unlimited" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.FreeformUnlimited"/>
<deckType name="Variant Magic - Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Commander"/> <deckType name="Variant Magic - Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.Commander"/>
<deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.DuelCommander"/> <deckType name="Variant Magic - Duel Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.DuelCommander"/>
<deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.MTGO1v1Commander"/> <deckType name="Variant Magic - MTGO 1v1 Commander" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.MTGO1v1Commander"/>

View file

@ -4,6 +4,8 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.constants.Constants; import mage.constants.Constants;
import mage.game.Game; import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.game.GameController; import mage.server.game.GameController;
import mage.server.managers.ChatManager; import mage.server.managers.ChatManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
@ -40,11 +42,39 @@ public class ChatManagerImpl implements ChatManager {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
} }
@Override @Override
public UUID createChatSession(String info) { public UUID createRoomChatSession(UUID roomId) {
return createChatSession("Room " + roomId)
.withRoom(roomId)
.getChatId();
}
@Override
public UUID createTourneyChatSession(Tournament tournament) {
return createChatSession("Tourney " + tournament.getId())
.withTourney(tournament)
.getChatId();
}
@Override
public UUID createTableChatSession(Table table) {
return createChatSession("Table " + table.getId())
.withTable(table)
.getChatId();
}
@Override
public UUID createGameChatSession(Game game) {
return createChatSession("Game " + game.getId())
.withGame(game)
.getChatId();
}
private ChatSession createChatSession(String info) {
ChatSession chatSession = new ChatSession(managerFactory, info); ChatSession chatSession = new ChatSession(managerFactory, info);
chatSessions.put(chatSession.getChatId(), chatSession); chatSessions.put(chatSession.getChatId(), chatSession);
return chatSession.getChatId(); return chatSession;
} }
@Override @Override
@ -94,7 +124,7 @@ public class ChatManagerImpl implements ChatManager {
ChatSession chatSession = chatSessions.get(chatId); ChatSession chatSession = chatSessions.get(chatId);
Optional<User> user = managerFactory.userManager().getUserByName(userName); Optional<User> user = managerFactory.userManager().getUserByName(userName);
if (chatSession != null) { if (chatSession != null) {
// special commads // special commands
if (message.startsWith("\\") || message.startsWith("/")) { if (message.startsWith("\\") || message.startsWith("/")) {
if (user.isPresent()) { if (user.isPresent()) {
if (!performUserCommand(user.get(), message, chatId, false)) { if (!performUserCommand(user.get(), message, chatId, false)) {

View file

@ -1,6 +1,9 @@
package mage.server; package mage.server;
import mage.collectors.DataCollectorServices;
import mage.game.Game; import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.interfaces.callback.ClientCallback; import mage.interfaces.callback.ClientCallback;
import mage.interfaces.callback.ClientCallbackMethod; import mage.interfaces.callback.ClientCallbackMethod;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
@ -27,6 +30,13 @@ public class ChatSession {
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage? private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage?
// only 1 field must be filled per chat type
// TODO: rework chat sessions to share logic (one server room/lobby + one table/subtable + one games/match)
private UUID roomId = null;
private UUID tourneyId = null;
private UUID tableId = null;
private UUID gameId = null;
private final ConcurrentMap<UUID, String> users = new ConcurrentHashMap<>(); // active users private final ConcurrentMap<UUID, String> users = new ConcurrentHashMap<>(); // active users
private final Set<UUID> usersHistory = new HashSet<>(); // all users that was here (need for system messages like connection problem) private final Set<UUID> usersHistory = new HashSet<>(); // all users that was here (need for system messages like connection problem)
private final UUID chatId; private final UUID chatId;
@ -40,6 +50,26 @@ public class ChatSession {
this.info = info; this.info = info;
} }
public ChatSession withRoom(UUID roomId) {
this.roomId = roomId;
return this;
}
public ChatSession withTourney(Tournament tournament) {
this.tourneyId = tournament.getId();
return this;
}
public ChatSession withTable(Table table) {
this.tableId = table.getId();
return this;
}
public ChatSession withGame(Game game) {
this.gameId = game.getId();
return this;
}
public void join(UUID userId) { public void join(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user -> { managerFactory.userManager().getUser(userId).ifPresent(user -> {
if (!users.containsKey(userId)) { if (!users.containsKey(userId)) {
@ -112,9 +142,36 @@ public class ChatSession {
// TODO: is it freeze on someone's connection fail/freeze with play multiple games/chats/lobby? // TODO: is it freeze on someone's connection fail/freeze with play multiple games/chats/lobby?
// TODO: send messages in another thread?! // TODO: send messages in another thread?!
if (!message.isEmpty()) { if (!message.isEmpty()) {
ChatMessage chatMessage = new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay);
switch (messageType) {
case USER_INFO:
case STATUS:
case TALK:
if (this.roomId != null) {
DataCollectorServices.getInstance().onChatRoom(this.roomId, userName, message);
} else if (this.tourneyId != null) {
DataCollectorServices.getInstance().onChatTourney(this.tourneyId, userName, message);
} else if (this.tableId != null) {
DataCollectorServices.getInstance().onChatTable(this.tableId, userName, message);
} else if (this.gameId != null) {
DataCollectorServices.getInstance().onChatGame(this.gameId, userName, message);
}
break;
case GAME:
// game logs processing in other place
break;
case WHISPER_FROM:
case WHISPER_TO:
// ignore private messages
break;
default:
throw new IllegalStateException("Unsupported message type " + messageType);
}
// TODO: wtf, remove all that locks/tries and make it simpler
Set<UUID> clientsToRemove = new HashSet<>(); Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, chatMessage);
new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay));
List<UUID> chatUserIds = new ArrayList<>(); List<UUID> chatUserIds = new ArrayList<>();
final Lock r = lock.readLock(); final Lock r = lock.readLock();
r.lock(); r.lock();

View file

@ -5,6 +5,7 @@ import mage.cards.decks.DeckCardLists;
import mage.cards.decks.DeckValidatorFactory; import mage.cards.decks.DeckValidatorFactory;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionRepository; import mage.cards.repository.ExpansionRepository;
import mage.collectors.DataCollectorServices;
import mage.constants.Constants; import mage.constants.Constants;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.constants.PlayerAction; import mage.constants.PlayerAction;
@ -29,6 +30,7 @@ import mage.server.managers.ManagerFactory;
import mage.server.services.impl.FeedbackServiceImpl; import mage.server.services.impl.FeedbackServiceImpl;
import mage.server.tournament.TournamentFactory; import mage.server.tournament.TournamentFactory;
import mage.server.util.ServerMessagesUtil; import mage.server.util.ServerMessagesUtil;
import mage.util.DebugUtil;
import mage.utils.*; import mage.utils.*;
import mage.view.*; import mage.view.*;
import mage.view.ChatMessage.MessageColor; import mage.view.ChatMessage.MessageColor;
@ -68,6 +70,13 @@ public class MageServerImpl implements MageServer {
this.detailsMode = detailsMode; this.detailsMode = detailsMode;
this.callExecutor = managerFactory.threadExecutor().getCallExecutor(); this.callExecutor = managerFactory.threadExecutor().getCallExecutor();
ServerMessagesUtil.instance.getMessages(); ServerMessagesUtil.instance.getMessages();
// additional logs
DataCollectorServices.init(
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_PRINT_GAME_LOGS,
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_SAVE_GAME_HISTORY
);
DataCollectorServices.getInstance().onServerStart();
} }
@Override @Override

View file

@ -14,7 +14,7 @@ public abstract class RoomImpl implements Room {
public RoomImpl(ChatManager chatManager) { public RoomImpl(ChatManager chatManager) {
roomId = UUID.randomUUID(); roomId = UUID.randomUUID();
chatId = chatManager.createChatSession("Room " + roomId); chatId = chatManager.createRoomChatSession(roomId);
} }
/** /**

View file

@ -8,7 +8,6 @@ import mage.util.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.callback.InvokerCallbackHandler;
import javax.annotation.Nonnull;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -29,7 +28,7 @@ public class SessionManagerImpl implements SessionManager {
} }
@Override @Override
public Optional<Session> getSession(@Nonnull String sessionId) { public Optional<Session> getSession(String sessionId) {
return Optional.ofNullable(sessions.getOrDefault(sessionId, null)); return Optional.ofNullable(sessions.getOrDefault(sessionId, null));
} }
@ -180,12 +179,12 @@ public class SessionManagerImpl implements SessionManager {
} }
@Override @Override
public boolean isValidSession(@Nonnull String sessionId) { public boolean isValidSession(String sessionId) {
return sessions.containsKey(sessionId); return sessions.containsKey(sessionId);
} }
@Override @Override
public Optional<User> getUser(@Nonnull String sessionId) { public Optional<User> getUser(String sessionId) {
Session session = sessions.get(sessionId); Session session = sessions.get(sessionId);
if (session != null) { if (session != null) {
return managerFactory.userManager().getUser(sessions.get(sessionId).getUserId()); return managerFactory.userManager().getUser(sessions.get(sessionId).getUserId());

View file

@ -72,7 +72,7 @@ public class TableController {
} }
this.table = new Table(roomId, options.getGameType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getDeckType()), this.table = new Table(roomId, options.getGameType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), match, options.getBannedUsers(), options.isPlaneChase()); options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), match, options.getBannedUsers(), options.isPlaneChase());
this.chatId = managerFactory.chatManager().createChatSession("Match Table " + table.getId()); this.chatId = managerFactory.chatManager().createTableChatSession(table);
init(); init();
} }
@ -94,7 +94,7 @@ public class TableController {
} }
table = new Table(roomId, options.getTournamentType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getMatchOptions().getDeckType()), table = new Table(roomId, options.getTournamentType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getMatchOptions().getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), tournament, options.getMatchOptions().getBannedUsers(), options.isPlaneChase()); options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), tournament, options.getMatchOptions().getBannedUsers(), options.isPlaneChase());
chatId = managerFactory.chatManager().createChatSession("Tourney table " + table.getId()); chatId = managerFactory.chatManager().createTableChatSession(table);
} }
private void init() { private void init() {

View file

@ -1,25 +0,0 @@
package mage.server.challenge;
import mage.constants.Zone;
import mage.game.match.Match;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* C U R R E N T L Y U N U S E D
*
* Loads challenges from scenarios.
* Configure games by initializing starting game board.
*/
public enum ChallengeManager {
instance;
public void prepareChallenge(UUID playerId, Match match) {
Map<Zone, String> commands = new HashMap<>();
commands.put(Zone.OUTSIDE, "life:3");
match.getGame().cheat(playerId, commands);
}
}

View file

@ -1,7 +0,0 @@
package mage.server.exceptions;
/**
* Created by igoudt on 14-1-2017.
*/
public class UserNotFoundException extends Exception {
}

View file

@ -85,11 +85,11 @@ public class GameController implements GameCallback {
public GameController(ManagerFactory managerFactory, Game game, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId, UUID choosingPlayerId, GameOptions gameOptions) { public GameController(ManagerFactory managerFactory, Game game, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId, UUID choosingPlayerId, GameOptions gameOptions) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
gameExecutor = managerFactory.threadExecutor().getGameExecutor(); this.gameExecutor = managerFactory.threadExecutor().getGameExecutor();
responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor(); this.responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor();
gameSessionId = UUID.randomUUID(); this.gameSessionId = UUID.randomUUID();
this.userPlayerMap = userPlayerMap; this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Game " + game.getId()); this.chatId = managerFactory.chatManager().createGameChatSession(game);
this.userRequestingRollback = null; this.userRequestingRollback = null;
this.game = game; this.game = game;
this.game.setSaveGame(managerFactory.configSettings().isSaveGameActivated()); this.game.setSaveGame(managerFactory.configSettings().isSaveGameActivated());

View file

@ -1,9 +1,10 @@
package mage.server.managers; package mage.server.managers;
import mage.game.Game; import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.ChatSession; import mage.server.ChatSession;
import mage.server.DisconnectReason; import mage.server.DisconnectReason;
import mage.server.exceptions.UserNotFoundException;
import mage.view.ChatMessage; import mage.view.ChatMessage;
import java.util.List; import java.util.List;
@ -11,7 +12,13 @@ import java.util.UUID;
public interface ChatManager { public interface ChatManager {
UUID createChatSession(String info); UUID createRoomChatSession(UUID roomId);
UUID createTourneyChatSession(Tournament tournament);
UUID createTableChatSession(Table table);
UUID createGameChatSession(Game game);
void joinChat(UUID chatId, UUID userId); void joinChat(UUID chatId, UUID userId);

View file

@ -7,12 +7,11 @@ import mage.server.Session;
import mage.server.User; import mage.server.User;
import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.callback.InvokerCallbackHandler;
import javax.annotation.Nonnull;
import java.util.Optional; import java.util.Optional;
public interface SessionManager { public interface SessionManager {
Optional<Session> getSession(@Nonnull String sessionId); Optional<Session> getSession(String sessionId);
void createSession(String sessionId, InvokerCallbackHandler callbackHandler); void createSession(String sessionId, InvokerCallbackHandler callbackHandler);
@ -37,9 +36,9 @@ public interface SessionManager {
boolean checkAdminAccess(String sessionId); boolean checkAdminAccess(String sessionId);
boolean isValidSession(@Nonnull String sessionId); boolean isValidSession(String sessionId);
Optional<User> getUser(@Nonnull String sessionId); Optional<User> getUser(String sessionId);
boolean extendUserSession(String sessionId, String pingInfo); boolean extendUserSession(String sessionId, String pingInfo);

View file

@ -1,18 +0,0 @@
package mage.server.services;
/**
* Responsible for gathering logs and storing them in DB.
*
* @author noxx
*/
@FunctionalInterface
public interface LogService {
/**
* Logs any information
*
* @param key Log key. Should be the same for the same types of logs.
* @param args Any parameters in string representation.
*/
void log(String key, String... args);
}

View file

@ -1,18 +0,0 @@
package mage.server.services;
/**
* Common interface for all services.
*
* @author noxx
*/
public interface MageService {
/**
* Restores data on startup.
*/
void initService();
/**
* Dumps data to DB.
*/
void saveData();
}

View file

@ -54,7 +54,7 @@ public class TournamentController {
public TournamentController(ManagerFactory managerFactory, Tournament tournament, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId) { public TournamentController(ManagerFactory managerFactory, Tournament tournament, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
this.userPlayerMap = userPlayerMap; this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Tournament " + tournament.getId()); this.chatId = managerFactory.chatManager().createTourneyChatSession(tournament);
this.tournament = tournament; this.tournament = tournament;
this.tableId = tableId; this.tableId = tableId;
init(); init();
@ -261,9 +261,7 @@ public class TournamentController {
table.setState(TableState.STARTING); table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId()); tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> { tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId); pair.setMatchAndTable(match, table.getId());
pair.setMatch(match);
pair.setTableId(table.getId());
player1.setState(TournamentPlayerState.DUELING); player1.setState(TournamentPlayerState.DUELING);
player2.setState(TournamentPlayerState.DUELING); player2.setState(TournamentPlayerState.DUELING);
}); });
@ -291,9 +289,7 @@ public class TournamentController {
table.setState(TableState.STARTING); table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId()); tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> { tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId); round.setMatchAndTable(match, table.getId());
round.setMatch(match);
round.setTableId(table.getId());
for (TournamentPlayer player : round.getAllPlayers()) { for (TournamentPlayer player : round.getAllPlayers()) {
player.setState(TournamentPlayerState.DUELING); player.setState(TournamentPlayerState.DUELING);
} }

View file

@ -14,6 +14,7 @@ import mage.constants.Duration;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.target.TargetPermanent;
import mage.target.common.TargetAnyTarget; import mage.target.common.TargetAnyTarget;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
@ -39,7 +40,7 @@ public final class AbunaAcolyte extends CardImpl {
Ability ability1 = new SimpleActivatedAbility(new PreventDamageToTargetEffect(Duration.EndOfTurn, 1), new TapSourceCost()); Ability ability1 = new SimpleActivatedAbility(new PreventDamageToTargetEffect(Duration.EndOfTurn, 1), new TapSourceCost());
ability1.addTarget(new TargetAnyTarget()); ability1.addTarget(new TargetAnyTarget());
Ability ability2 = new SimpleActivatedAbility(new PreventDamageToTargetEffect(Duration.EndOfTurn, 2), new TapSourceCost()); Ability ability2 = new SimpleActivatedAbility(new PreventDamageToTargetEffect(Duration.EndOfTurn, 2), new TapSourceCost());
ability2.addTarget(new TargetCreaturePermanent(filter)); ability2.addTarget(new TargetPermanent(filter));
this.addAbility(ability1); this.addAbility(ability1);
this.addAbility(ability2); this.addAbility(ability2);
} }

View file

@ -13,6 +13,7 @@ import mage.constants.CardType;
import mage.constants.ComparisonType; import mage.constants.ComparisonType;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.PowerPredicate; import mage.filter.predicate.mageobject.PowerPredicate;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import mage.target.common.TargetCreaturePermanentAmount; import mage.target.common.TargetCreaturePermanentAmount;
@ -33,7 +34,7 @@ public final class AbzanCharm extends CardImpl {
// Choose one - // Choose one -
// *Exile target creature with power 3 or greater // *Exile target creature with power 3 or greater
this.getSpellAbility().addTarget(new TargetCreaturePermanent(FILTER)); this.getSpellAbility().addTarget(new TargetPermanent(FILTER));
this.getSpellAbility().addEffect(new ExileTargetEffect()); this.getSpellAbility().addEffect(new ExileTargetEffect());
// *You draw two cards and you lose 2 life // *You draw two cards and you lose 2 life

View file

@ -16,10 +16,13 @@ import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterControlledPermanent;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID; import java.util.UUID;
import static mage.filter.StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE;
/** /**
* @author JRHerlehy * @author JRHerlehy
*/ */
@ -47,7 +50,7 @@ public final class AcademyJourneymage extends CardImpl {
// When Academy Journeymage enters the battlefield, return target creature an opponent controls to its owner's hand. // When Academy Journeymage enters the battlefield, return target creature an opponent controls to its owner's hand.
ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandTargetEffect()); ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandTargetEffect());
ability.addTarget(new TargetCreaturePermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE)); ability.addTarget(new TargetPermanent(FILTER_OPPONENTS_PERMANENT_CREATURE));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,7 +1,6 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID;
import mage.abilities.dynamicvalue.common.CardsInAllGraveyardsCount; import mage.abilities.dynamicvalue.common.CardsInAllGraveyardsCount;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect;
@ -11,8 +10,9 @@ import mage.constants.CardType;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.mageobject.NamePredicate;
import java.util.UUID;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class AccumulatedKnowledge extends CardImpl { public final class AccumulatedKnowledge extends CardImpl {
@ -24,13 +24,13 @@ public final class AccumulatedKnowledge extends CardImpl {
} }
public AccumulatedKnowledge(UUID ownerId, CardSetInfo setInfo) { public AccumulatedKnowledge(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{1}{U}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}");
// Draw a card, then draw cards equal to the number of cards named Accumulated Knowledge in all graveyards. // Draw a card, then draw cards equal to the number of cards named Accumulated Knowledge in all graveyards.
this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1));
Effect effect = new DrawCardSourceControllerEffect(new CardsInAllGraveyardsCount(filter)); Effect effect = new DrawCardSourceControllerEffect(new CardsInAllGraveyardsCount(filter));
effect.setText(", then draw cards equal to the number of cards named {this} in all graveyards"); effect.setText(", then draw cards equal to the number of cards named Accumulated Knowledge in all graveyards");
this.getSpellAbility().addEffect(effect); this.getSpellAbility().addEffect(effect);
} }

View file

@ -44,9 +44,7 @@ public final class AcererakTheArchlich extends CardImpl {
Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandSourceEffect(true)) Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandSourceEffect(true))
.withInterveningIf(AcererakTheArchlichCondition.instance); .withInterveningIf(AcererakTheArchlichCondition.instance);
ability.addEffect(new VentureIntoTheDungeonEffect().concatBy("and")); ability.addEffect(new VentureIntoTheDungeonEffect().concatBy("and"));
ability.addHint(CurrentDungeonHint.instance); this.addAbility(ability.addHint(CurrentDungeonHint.instance).addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher());
ability.addHint(CompletedDungeonCondition.getHint());
this.addAbility(ability, new CompletedDungeonWatcher());
// Whenever Acererak the Archlich attacks, for each opponent, you create a 2/2 black Zombie creature token unless that player sacrifices a creature. // Whenever Acererak the Archlich attacks, for each opponent, you create a 2/2 black Zombie creature token unless that player sacrifices a creature.
this.addAbility(new AttacksTriggeredAbility(new AcererakTheArchlichEffect())); this.addAbility(new AttacksTriggeredAbility(new AcererakTheArchlichEffect()));
@ -83,7 +81,7 @@ class AcererakTheArchlichEffect extends OneShotEffect {
AcererakTheArchlichEffect() { AcererakTheArchlichEffect() {
super(Outcome.Benefit); super(Outcome.Benefit);
staticText = "for each opponent, you create a 2/2 black Zombie creature " + staticText = "for each opponent, you create a 2/2 black Zombie creature " +
"token unless that player sacrifices a creature"; "token unless that player sacrifices a creature of their choice";
} }
private AcererakTheArchlichEffect(final AcererakTheArchlichEffect effect) { private AcererakTheArchlichEffect(final AcererakTheArchlichEffect effect) {

View file

@ -15,7 +15,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.*;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledCreaturePermanent;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.TargetPermanent;
import java.util.UUID; import java.util.UUID;
@ -60,7 +60,7 @@ public final class AcesBaseballBat extends CardImpl {
// Equip legendary creature (1) // Equip legendary creature (1)
this.addAbility(new EquipAbility( this.addAbility(new EquipAbility(
Outcome.AddAbility, new GenericManaCost(1), Outcome.AddAbility, new GenericManaCost(1),
new TargetControlledCreaturePermanent(filterLegendary), false new TargetPermanent(filterLegendary), false
)); ));
// Equip {3} // Equip {3}

View file

@ -5,7 +5,7 @@ import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.condition.common.BeforeBlockersAreDeclaredCondition; import mage.abilities.condition.common.BeforeBlockersAreDeclaredCondition;
import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.decorator.ConditionalActivatedAbility; import mage.abilities.common.ActivateIfConditionActivatedAbility;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.DestroyTargetEffect;
@ -39,7 +39,7 @@ public final class AcidicDagger extends CardImpl {
// {4}, {tap}: Whenever target creature deals combat damage to a non-Wall creature this turn, // {4}, {tap}: Whenever target creature deals combat damage to a non-Wall creature this turn,
// destroy that non-Wall creature. When the targeted creature leaves the battlefield this turn, // destroy that non-Wall creature. When the targeted creature leaves the battlefield this turn,
// sacrifice Acidic Dagger. Activate this ability only before blockers are declared. // sacrifice Acidic Dagger. Activate this ability only before blockers are declared.
Ability ability = new ConditionalActivatedAbility( Ability ability = new ActivateIfConditionActivatedAbility(
new CreateDelayedTriggeredAbilityEffect(new AcidicDaggerDestroyNonWallAbility()), new CreateDelayedTriggeredAbilityEffect(new AcidicDaggerDestroyNonWallAbility()),
new GenericManaCost(4), new GenericManaCost(4),
BeforeBlockersAreDeclaredCondition.instance); BeforeBlockersAreDeclaredCondition.instance);

View file

@ -11,8 +11,11 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import static mage.filter.StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE;
/** /**
* *
* @author North * @author North
@ -21,7 +24,7 @@ public final class ActOfAggression extends CardImpl {
public ActOfAggression(UUID ownerId, CardSetInfo setInfo) { public ActOfAggression(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{R/P}{R/P}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{R/P}{R/P}");
this.getSpellAbility().addTarget(new TargetCreaturePermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURE)); this.getSpellAbility().addTarget(new TargetPermanent(FILTER_OPPONENTS_PERMANENT_CREATURE));
this.getSpellAbility().addEffect(new GainControlTargetEffect(Duration.EndOfTurn)); this.getSpellAbility().addEffect(new GainControlTargetEffect(Duration.EndOfTurn));
this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Untap that creature")); this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Untap that creature"));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.EndOfTurn).setText("It gains haste until end of turn.")); this.getSpellAbility().addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.EndOfTurn).setText("It gains haste until end of turn."));

View file

@ -1,7 +1,5 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID;
import mage.abilities.common.AttacksAttachedTriggeredAbility; import mage.abilities.common.AttacksAttachedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.GenericManaCost;
@ -9,35 +7,35 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.abilities.effects.common.continuous.BoostEquippedEffect; import mage.abilities.effects.common.continuous.BoostEquippedEffect;
import mage.abilities.hint.Hint; import mage.abilities.hint.common.ArtifactYouControlHint;
import mage.abilities.hint.ValueHint;
import mage.abilities.keyword.EquipAbility; import mage.abilities.keyword.EquipAbility;
import mage.constants.Outcome;
import mage.constants.PutCards;
import mage.constants.SubType;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.PutCards;
import mage.constants.SubType;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledArtifactPermanent; import mage.filter.common.FilterControlledArtifactPermanent;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import java.util.UUID;
/** /**
*
* @author sobiech * @author sobiech
*/ */
public final class AdaptiveOmnitool extends CardImpl { public final class AdaptiveOmnitool extends CardImpl {
private final static DynamicValue artifactYouControlCount = new PermanentsOnBattlefieldCount(new FilterControlledArtifactPermanent()); private final static DynamicValue artifactYouControlCount = new PermanentsOnBattlefieldCount(new FilterControlledArtifactPermanent());
private final static Hint hint = new ValueHint("Artifacts you control", artifactYouControlCount);
public AdaptiveOmnitool(UUID ownerId, CardSetInfo setInfo) { public AdaptiveOmnitool(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}");
this.subtype.add(SubType.EQUIPMENT); this.subtype.add(SubType.EQUIPMENT);
// Equipped creature gets +1/+1 for each artifact you control. // Equipped creature gets +1/+1 for each artifact you control.
this.addAbility( this.addAbility(
new SimpleStaticAbility(new BoostEquippedEffect(artifactYouControlCount, artifactYouControlCount)).addHint(hint) new SimpleStaticAbility(new BoostEquippedEffect(artifactYouControlCount, artifactYouControlCount)).addHint(ArtifactYouControlHint.instance)
); );
// Whenever equipped creature attacks, look at the top six cards of your library. You may reveal an artifact card from among them and put it into your hand. Put the rest on the bottom of your library in a random order. // Whenever equipped creature attacks, look at the top six cards of your library. You may reveal an artifact card from among them and put it into your hand. Put the rest on the bottom of your library in a random order.

View file

@ -31,7 +31,7 @@ public final class AdaptiveTrainingPost extends CardImpl {
this.addAbility(new SpellCastControllerTriggeredAbility( this.addAbility(new SpellCastControllerTriggeredAbility(
new AddCountersSourceEffect(CounterType.CHARGE.createInstance()), new AddCountersSourceEffect(CounterType.CHARGE.createInstance()),
StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false
).withInterveningIf(condition)); ).withInterveningIf(condition).withRuleTextReplacement(true));
// Remove three charge counters from this artifact: When you next cast an instant or sorcery spell this turn, copy it and you may choose new targets for the copy. // Remove three charge counters from this artifact: When you next cast an instant or sorcery spell this turn, copy it and you may choose new targets for the copy.
this.addAbility(new SimpleActivatedAbility( this.addAbility(new SimpleActivatedAbility(

View file

@ -1,7 +1,6 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbility;
@ -20,13 +19,15 @@ import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AnotherPredicate; import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import java.util.UUID;
/** /**
* *
* @author jeffwadsworth * @author jeffwadsworth
*/ */
public final class AdmonitionAngel extends CardImpl { public final class AdmonitionAngel extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("nonland permanent other than Admonition Angel"); private static final FilterPermanent filter = new FilterPermanent("nonland permanent other than {this}");
static { static {
filter.add(AnotherPredicate.instance); filter.add(AnotherPredicate.instance);

View file

@ -14,6 +14,7 @@ import mage.constants.TargetController;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
/** /**
@ -38,7 +39,7 @@ public final class AdvocateOfTheBeast extends CardImpl {
// At the beginning of your end step, put a +1/+1 counter on target Beast creature you control. // At the beginning of your end step, put a +1/+1 counter on target Beast creature you control.
Ability ability = new BeginningOfEndStepTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance())); Ability ability = new BeginningOfEndStepTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance()));
Target target = new TargetCreaturePermanent(filter); Target target = new TargetPermanent(filter);
ability.addTarget(target); ability.addTarget(target);
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,6 +1,5 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
@ -10,12 +9,12 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.TargetPermanent;
import java.util.UUID;
/** /**
*
* @author fireshoes * @author fireshoes
*/ */
public final class AegisAutomaton extends CardImpl { public final class AegisAutomaton extends CardImpl {
@ -29,7 +28,7 @@ public final class AegisAutomaton extends CardImpl {
// {4}{W}: Return another target creature you control to its owner's hand. // {4}{W}: Return another target creature you control to its owner's hand.
Ability ability = new SimpleActivatedAbility(new ReturnToHandTargetEffect(), new ManaCostsImpl<>("{4}{W}")); Ability ability = new SimpleActivatedAbility(new ReturnToHandTargetEffect(), new ManaCostsImpl<>("{4}{W}"));
ability.addTarget(new TargetControlledCreaturePermanent(StaticFilters.FILTER_ANOTHER_TARGET_CREATURE_YOU_CONTROL)); ability.addTarget(new TargetPermanent(StaticFilters.FILTER_ANOTHER_TARGET_CREATURE_YOU_CONTROL));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -16,6 +16,7 @@ import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.PowerPredicate; import mage.filter.predicate.mageobject.PowerPredicate;
import mage.filter.predicate.mageobject.ToughnessPredicate; import mage.filter.predicate.mageobject.ToughnessPredicate;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
/** /**
@ -37,7 +38,7 @@ public final class AegisOfTheMeek extends CardImpl {
// {1}, {T}: Target 1/1 creature gets +1/+2 until end of turn. // {1}, {T}: Target 1/1 creature gets +1/+2 until end of turn.
Ability ability = new SimpleActivatedAbility(new BoostTargetEffect(1, 2, Duration.EndOfTurn), new ManaCostsImpl<>("{1}")); Ability ability = new SimpleActivatedAbility(new BoostTargetEffect(1, 2, Duration.EndOfTurn), new ManaCostsImpl<>("{1}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.addTarget(new TargetCreaturePermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -12,6 +12,7 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate; import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
/** /**
@ -31,7 +32,7 @@ public final class AerialPredation extends CardImpl {
// Destroy target creature with flying. You gain 2 life. // Destroy target creature with flying. You gain 2 life.
this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); this.getSpellAbility().addTarget(new TargetPermanent(filter));
this.getSpellAbility().addEffect(new DestroyTargetEffect()); this.getSpellAbility().addEffect(new DestroyTargetEffect());
this.getSpellAbility().addEffect(new GainLifeEffect(2)); this.getSpellAbility().addEffect(new GainLifeEffect(2));
} }

View file

@ -84,7 +84,7 @@ enum AerialSurveyorCondition implements Condition {
@Override @Override
public String toString() { public String toString() {
return ""; return "defending player controls more lands than you";
} }
} }

View file

@ -16,6 +16,7 @@ import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.AbilityPredicate; import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID; import java.util.UUID;
@ -42,7 +43,7 @@ public final class AerieOuphes extends CardImpl {
// Sacrifice Aerie Ouphes: Aerie Ouphes deals damage equal to its power to target creature with flying. // Sacrifice Aerie Ouphes: Aerie Ouphes deals damage equal to its power to target creature with flying.
Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(SourcePermanentPowerValue.NOT_NEGATIVE) Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(SourcePermanentPowerValue.NOT_NEGATIVE)
.setText("it deals damage equal to its power to target creature with flying"), new SacrificeSourceCost()); .setText("it deals damage equal to its power to target creature with flying"), new SacrificeSourceCost());
ability.addTarget(new TargetCreaturePermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);
// Persist // Persist

View file

@ -44,7 +44,7 @@ class AetherBarrierEffect extends SacrificeEffect {
AetherBarrierEffect() { AetherBarrierEffect() {
super(new FilterPermanent("permanent to sacrifice"), 1, "that player"); super(new FilterPermanent("permanent to sacrifice"), 1, "that player");
this.staticText = "that player sacrifices a permanent unless they pay {1}"; this.staticText = "that player sacrifices a permanent of their choice unless they pay {1}";
} }
private AetherBarrierEffect(final AetherBarrierEffect effect) { private AetherBarrierEffect(final AetherBarrierEffect effect) {

View file

@ -1,19 +1,22 @@
package mage.cards.a; package mage.cards.a;
import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility;
import mage.abilities.costs.common.PayEnergyCost; import mage.abilities.costs.common.PayEnergyCost;
import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.cards.CardsImpl; import mage.cards.CardsImpl;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
@ -34,8 +37,8 @@ public final class AetherfluxConduit extends CardImpl {
this.addAbility(new SpellCastControllerTriggeredAbility(new AetherfluxConduitManaEffect(), false)); this.addAbility(new SpellCastControllerTriggeredAbility(new AetherfluxConduitManaEffect(), false));
// {T}, Pay fifty {E}: Draw seven cards. You may cast any number of spells from your hand without paying their mana costs. // {T}, Pay fifty {E}: Draw seven cards. You may cast any number of spells from your hand without paying their mana costs.
final Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(7), new TapSourceCost()); Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(7), new TapSourceCost());
ability.addCost(new PayEnergyCost(50).setText("Pay fifty {E}")); ability.addCost(new PayEnergyCost(50).setText("pay fifty {E}"));
ability.addEffect(new AetherfluxConduitCastEffect()); ability.addEffect(new AetherfluxConduitCastEffect());
this.addAbility(ability); this.addAbility(ability);
} }
@ -68,10 +71,21 @@ class AetherfluxConduitManaEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Optional.ofNullable(this.getValue("spellCast")) int amount = Optional
.ofNullable(this.getValue("spellCast"))
.map(Spell.class::cast) .map(Spell.class::cast)
.ifPresent(spell -> new GetEnergyCountersControllerEffect(spell.getManaValue()).apply(game, source)); .map(Spell::getStackAbility)
return true; .map(Ability::getManaCostsToPay)
.map(ManaCost::getUsedManaToPay)
.map(Mana::count)
.orElse(0);
return amount > 0
&& Optional
.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.filter(player -> player.addCounters(CounterType.ENERGY.createInstance(amount), player.getId(), source, game))
.isPresent();
} }
} }

View file

@ -1,22 +1,22 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.triggers.BeginningOfCombatTriggeredAbility;
import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.IndestructibleAbility; import mage.abilities.keyword.IndestructibleAbility;
import mage.constants.SubType; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledCreaturePermanent;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.TargetPermanent;
import java.util.UUID;
/** /**
*
* @author TheElk801 * @author TheElk801
*/ */
public final class AethershieldArtificer extends CardImpl { public final class AethershieldArtificer extends CardImpl {
@ -45,7 +45,7 @@ public final class AethershieldArtificer extends CardImpl {
IndestructibleAbility.getInstance(), IndestructibleAbility.getInstance(),
Duration.EndOfTurn Duration.EndOfTurn
).setText("and gains indestructible until end of turn")); ).setText("and gains indestructible until end of turn"));
ability.addTarget(new TargetControlledCreaturePermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -13,6 +13,7 @@ import mage.filter.common.FilterAttackingOrBlockingCreature;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
/** /**
@ -28,7 +29,7 @@ public final class Aethertow extends CardImpl {
// Put target attacking or blocking creature on top of its owner's library. // Put target attacking or blocking creature on top of its owner's library.
this.getSpellAbility().addEffect(new AethertowEffect()); this.getSpellAbility().addEffect(new AethertowEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); this.getSpellAbility().addTarget(new TargetPermanent(filter));
// Conspire // Conspire
this.addAbility(new ConspireAbility(ConspireAbility.ConspireTargets.ONE)); this.addAbility(new ConspireAbility(ConspireAbility.ConspireTargets.ONE));

Some files were not shown because too many files have changed in this diff Show more