() {
- @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) {
// return real path to symbols (default or user defined)
@@ -420,8 +392,8 @@ public final class ManaSymbols {
case SMALL:
path = path + Constants.RESOURCE_SET_FOLDER_SMALL;
break;
- case MEDIUM:
- path = path + Constants.RESOURCE_SET_FOLDER_MEDIUM;
+ case LARGE:
+ path = path + Constants.RESOURCE_SET_FOLDER_LARGE;
break;
case SVG:
path = path + Constants.RESOURCE_SET_FOLDER_SVG;
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java
index e3288ff43c7..acdd0aec2d5 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java
@@ -663,7 +663,9 @@ public class CardPluginImpl implements CardPlugin {
// mana symbols (low quality)
jobs = new GathererSymbols();
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)
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java
index ad44256a076..57ca99791f9 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java
@@ -46,8 +46,10 @@ public class Downloader extends AbstractLaternaBean {
public Downloader() {
// 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);
- for (int i = 0, numThreads = 10; i < numThreads; i++) {
+ for (int i = 0, numThreads = maxThreads; i < numThreads; i++) {
Fiber fiber = f.create();
fiber.start();
fibers.add(fiber);
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java
index f12baabcae2..7a5c97ab04d 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java
@@ -16,7 +16,9 @@ import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
/**
* Download: set code symbols download from wizards web size
*
- * 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 {
@@ -41,9 +43,10 @@ public class GathererSets implements Iterable {
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);
+ // 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",
"HOP",
"ARN", "ATQ", "LEG", "DRK", "FEM", "HML",
@@ -61,14 +64,16 @@ public class GathererSets implements Iterable {
"TSP", "TSB", "PLC", "FUT",
"LRW", "MOR",
"SHM", "EVE",
- "MED", "ME2", "ME3", "ME4",
+ "ME2", "ME3", "ME4",
"POR", "P02", "PTK",
"ARC", "DD3EVG",
- "W16", "W17",
+ "W16", "W17",
// "PALP" -- Gatherer does not have the set Asia Pacific Land Program
// "ATH" -- has cards from many sets, symbol does not exist on gatherer
// "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
};
@@ -98,21 +103,25 @@ public class GathererSets implements Iterable {
"GNT", "UMA", "GRN",
"RNA", "WAR", "MH1",
"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",
"C21", "MH2", "AFR", "AFC", "J21", "MID", "MIC", "VOW", "VOC", "YMID",
- "NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "YDMU", "40K", "GN3",
- "UNF", "BRO", "BRC", "BOT", "30A", "J22", "SCD", "DMR", "ONE", "ONC",
+ "NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "40K", "GN3",
+ "UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC",
"MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT",
"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
};
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
};
+
private static final String[] symbolsOnlySpecial = {
"MPS", "MP2"
};
@@ -130,6 +139,7 @@ public class GathererSets implements Iterable {
codeReplacements.put("APC", "AP");
codeReplacements.put("ARN", "AN");
codeReplacements.put("ATQ", "AQ");
+ codeReplacements.put("BTD", "BD");
codeReplacements.put("CMA", "CM1");
codeReplacements.put("CHR", "CH");
codeReplacements.put("DVD", "DD3_DVD");
@@ -165,14 +175,16 @@ public class GathererSets implements Iterable {
codeReplacements.put("UGIN", "FRF_UGIN");
codeReplacements.put("UGL", "UG");
codeReplacements.put("ULG", "GU");
+ codeReplacements.put("UNF", "UNFS");
codeReplacements.put("USG", "UZ");
codeReplacements.put("VIS", "VI");
codeReplacements.put("WTH", "WL");
+ codeReplacements.put("YMID", "Y22");
+ codeReplacements.put("YNEO", "Y22NEO");
}
public GathererSets() {
-
- outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS);
+ outDir = new File(getImagesDir() + Constants.RESOURCE_PATH_SYMBOLS_RARITY_DEFAULT_PATH);
if (!outDir.exists()) {
outDir.mkdirs();
@@ -287,9 +299,9 @@ public class GathererSets implements Iterable {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
- jobs.add(generateDownloadJob(symbol, "C", "C"));
- jobs.add(generateDownloadJob(symbol, "U", "U"));
- jobs.add(generateDownloadJob(symbol, "R", "R"));
+ jobs.add(generateDownloadJob(symbol, "C", "common"));
+ jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
+ jobs.add(generateDownloadJob(symbol, "R", "rare"));
}
CheckSearchResult(symbol, exp, canDownload, true, true, true, false);
}
@@ -299,10 +311,10 @@ public class GathererSets implements Iterable {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
- jobs.add(generateDownloadJob(symbol, "C", "C"));
- jobs.add(generateDownloadJob(symbol, "U", "U"));
- jobs.add(generateDownloadJob(symbol, "R", "R"));
- jobs.add(generateDownloadJob(symbol, "M", "M"));
+ jobs.add(generateDownloadJob(symbol, "C", "common"));
+ jobs.add(generateDownloadJob(symbol, "U", "uncommon"));
+ jobs.add(generateDownloadJob(symbol, "R", "rare"));
+ jobs.add(generateDownloadJob(symbol, "M", "mythic"));
}
CheckSearchResult(symbol, exp, canDownload, true, true, true, true);
}
@@ -312,7 +324,7 @@ public class GathererSets implements Iterable {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
- jobs.add(generateDownloadJob(symbol, "M", "M"));
+ jobs.add(generateDownloadJob(symbol, "M", "mythic"));
}
CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
}
@@ -322,7 +334,7 @@ public class GathererSets implements Iterable {
canDownload = false;
if (exp != null && exp.getReleaseDate().before(compareDate)) {
canDownload = true;
- jobs.add(generateDownloadJob(symbol, "M", "S"));
+ jobs.add(generateDownloadJob(symbol, "M", "special"));
}
CheckSearchResult(symbol, exp, canDownload, false, false, false, true);
}
@@ -334,11 +346,22 @@ public class GathererSets implements Iterable {
}
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)) {
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);
}
}
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java
index b3704e3820d..1bd36368647 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java
@@ -75,7 +75,7 @@ public class GathererSymbols implements Iterable {
continue;
}
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)
// TODO: comment and try download without workaround, keep fix for symbols with "Resource not found" error
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java
index 7bbdd07853b..5a23faff7d7 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java
@@ -588,7 +588,10 @@ public class ScryfallImageSupportCards {
add("FIN"); // Final Fantasy
add("FIC"); // Final Fantasy Commander
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("TLA"); // Avatar: The Last Airbender
// Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks
add("CALC"); // Custom Alchemized versions of existing cards
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java
index 19aecedcf0e..0248b50c5e0 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java
@@ -352,6 +352,9 @@ public class ScryfallImageSupportTokens {
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");
+ // UND
+ put("UND/Goblin", "https://api.scryfall.com/cards/tund/2?format=image");
+
// THB
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");
@@ -829,12 +832,18 @@ public class ScryfallImageSupportTokens {
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/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/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/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/Myr", "https://api.scryfall.com/cards/sld/2101?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/Spirit/1", "https://api.scryfall.com/cards/sld/1341?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/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/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/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");
@@ -1843,6 +1854,12 @@ public class ScryfallImageSupportTokens {
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");
+ // 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
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");
@@ -2026,6 +2043,7 @@ public class ScryfallImageSupportTokens {
// DIS
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
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");
// 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 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/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/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/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/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/Spirit", "https://api.scryfall.com/cards/twoc/17/en?format=image");
- put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/3/en?format=image");
+ put("WOC/Virtuous", "https://api.scryfall.com/cards/twoc/2/en?format=image");
// WHO
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 Rhino", "https://api.scryfall.com/cards/twho/3/en?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/Glimmer", "https://api.scryfall.com/cards/tdsk/4?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/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/Shard", "https://api.scryfall.com/cards/tdsk/2?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");
// DSC
@@ -2698,7 +2712,8 @@ public class ScryfallImageSupportTokens {
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/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/Myr", "https://api.scryfall.com/cards/ttdc/30/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/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/Wall", "https://api.scryfall.com/cards/ttdc/7/en?format=image");
// ACR
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");
// 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/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
put("JVC/Elemental Shaman", "https://api.scryfall.com/cards/tjvc/4?format=image");
@@ -2756,6 +2816,7 @@ public class ScryfallImageSupportTokens {
// UGL
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/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/Squirrel", "https://api.scryfall.com/cards/tugl/6?format=image");
put("UGL/Zombie", "https://api.scryfall.com/cards/tugl/3?format=image");
diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java
index a8fb8ddc6c1..704395e0378 100644
--- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java
+++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java
@@ -157,7 +157,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
@Override
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) {
diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml
index 36fe0dc82ca..d2935eb53cc 100644
--- a/Mage.Common/pom.xml
+++ b/Mage.Common/pom.xml
@@ -66,7 +66,6 @@
org.apache.commons
commons-lang3
- test
diff --git a/Mage.Common/src/main/java/mage/cards/action/impl/EmptyCallback.java b/Mage.Common/src/main/java/mage/cards/action/impl/EmptyCallback.java
deleted file mode 100644
index 3da223590e6..00000000000
--- a/Mage.Common/src/main/java/mage/cards/action/impl/EmptyCallback.java
+++ /dev/null
@@ -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) {
-
- }
-}
diff --git a/Mage.Common/src/main/java/mage/filters/MageBufferedImageOp.java b/Mage.Common/src/main/java/mage/filters/MageBufferedImageOp.java
deleted file mode 100644
index f6adc23d756..00000000000
--- a/Mage.Common/src/main/java/mage/filters/MageBufferedImageOp.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Mage.Common/src/main/java/mage/utils/ActionWithUUIDResult.java b/Mage.Common/src/main/java/mage/utils/ActionWithUUIDResult.java
deleted file mode 100644
index 1133a0bde24..00000000000
--- a/Mage.Common/src/main/java/mage/utils/ActionWithUUIDResult.java
+++ /dev/null
@@ -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 {
-}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/AmountTestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/AmountTestableResult.java
new file mode 100644
index 00000000000..5b92ba19745
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/AmountTestableResult.java
@@ -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 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;
+ }
+}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/AnnounceXTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/AnnounceXTestableDialog.java
index bd346448472..5d3b6d5fd41 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/AnnounceXTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/AnnounceXTestableDialog.java
@@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -25,22 +26,33 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
public AnnounceXTestableDialog(boolean isYou, boolean isMana, int min, int max) {
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.isMana = isMana;
this.min = min;
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
- public List 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;
String message = "message with html";
- int chooseRes;
- chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana);
- List result = new ArrayList<>();
- result.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
- return result;
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
+ int chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana);
+ List res = new ArrayList<>();
+ res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
+
+ ((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
}
static public void register(TestableDialogsRunner runner) {
@@ -48,17 +60,17 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
List isManas = Arrays.asList(false, true);
for (boolean isYou : isYous) {
for (boolean isMana : isManas) {
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10));
- runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 0).aiMustChoose(0, 0));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 1).aiMustChoose(0, 1));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 3).aiMustChoose(0, 3));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 50).aiMustChoose(0, 50));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 0, 500).aiMustChoose(0, 500));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 1).aiMustChoose(1, 1));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 3).aiMustChoose(1, 3));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 1, 50).aiMustChoose(1, 50));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 3).aiMustChoose(3, 3));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 3, 10).aiMustChoose(3, 10));
+ runner.registerDialog(new AnnounceXTestableDialog(isYou, isMana, 10, 10).aiMustChoose(10, 10));
}
}
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java
index 0ee78b67e51..2ae8d4cae79 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableDialog.java
@@ -1,11 +1,12 @@
package mage.utils.testers;
-import mage.constants.SubType;
-import mage.filter.common.FilterCreaturePermanent;
+import mage.constants.ComparisonType;
+import mage.filter.FilterPermanent;
+import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
-import mage.target.common.TargetCreaturePermanent;
+import mage.target.TargetPermanent;
import mage.target.common.TargetPermanentOrPlayer;
/**
@@ -15,14 +16,27 @@ import mage.target.common.TargetPermanentOrPlayer;
*/
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 name;
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.name = name;
this.description = description;
+ this.result = result;
+ }
+
+ @Override
+ public void setRegNumber(Integer regNumber) {
+ this.regNumber = regNumber;
+ }
+
+ @Override
+ public Integer getRegNumber() {
+ return this.regNumber;
}
@Override
@@ -41,13 +55,23 @@ abstract class BaseTestableDialog implements TestableDialog {
}
@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
- game.informPlayer(player, result);
+ game.informPlayer(player, String.join("
", getResult().getResDetails()));
// reset game and gui (in most use cases it must return to player's priority)
game.firePriorityEvent(player.getId());
}
+ @Override
+ public TestableResult getResult() {
+ return this.result;
+ }
+
static Target createAnyTarget(int min, int max) {
return createAnyTarget(min, max, false);
}
@@ -56,19 +80,18 @@ abstract class BaseTestableDialog implements TestableDialog {
return new TargetPermanentOrPlayer(min, max).withNotTarget(notTarget);
}
- static Target createCreatureTarget(int min, int max) {
- return createCreatureTarget(min, max, false);
- }
+ private static final FilterPermanent impossibleFilter = new FilterPermanent();
- private static Target createCreatureTarget(int min, int max, boolean notTarget) {
- return new TargetCreaturePermanent(min, max).withNotTarget(notTarget);
+ static {
+ impossibleFilter.add(new ManaValuePredicate(ComparisonType.OR_LESS, -1));
}
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) {
- return new TargetCreaturePermanent(min, max, new FilterCreaturePermanent(SubType.TROOPER, "rare type"), notTarget);
+ @Override
+ public String toString() {
+ return this.getGroup() + " - " + this.getName() + " - " + this.getDescription();
}
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/BaseTestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableResult.java
new file mode 100644
index 00000000000..6b3856e6f95
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/BaseTestableResult.java
@@ -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 resInfo = new ArrayList<>();
+
+ @Override
+ public String getResDebugSource() {
+ return this.resDebugSource;
+ }
+
+ @Override
+ public boolean getResStatus() {
+ return this.resStatus;
+ }
+
+ @Override
+ public List getResDetails() {
+ return this.resInfo;
+ }
+
+ @Override
+ public String getResAssert() {
+ return null; // TODO: implement
+ }
+
+ @Override
+ public void onFinish(String resDebugSource, boolean resStatus, List 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();
+ }
+}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChoiceTestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/ChoiceTestableResult.java
new file mode 100644
index 00000000000..acf520e29e2
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChoiceTestableResult.java
@@ -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 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;
+ }
+}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java
index 6a7847ec4fc..37bb11eb381 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseAmountTestableDialog.java
@@ -7,6 +7,7 @@ import mage.players.Player;
import mage.target.TargetAmount;
import mage.target.Targets;
import mage.target.common.TargetAnyTargetAmount;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -32,27 +33,39 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
public ChooseAmountTestableDialog(boolean isYou, String name, int distributeAmount, int targetsMin, int targetsMax) {
super(String.format("player.chooseTarget(%s, amount)", isYou ? "you" : "AI"),
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.distributeAmount = distributeAmount;
this.targetsMin = targetsMin;
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
- public List 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);
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)
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseTargetAmount(Outcome.Benefit, choosingTarget, source, game);
- List result = new ArrayList<>();
+ List res = new ArrayList<>();
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 {
- 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) {
@@ -61,53 +74,55 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
List 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) {
// up to
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 0));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 0).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 1).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 0, 0, 3).aiMustChoose(false, 0));
+ 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", 1, 0, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 1, 0, 0).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 1, 0, 3).aiMustChoose(true, 1));
+ 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", 2, 0, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 2, 0, 0).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 2, 0, 3).aiMustChoose(true, 1));
+ 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", 3, 0, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 3, 0, 0).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 3, 0, 3).aiMustChoose(true, 1));
+ 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", 5, 0, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to, invalid", 5, 0, 0).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 3).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "up to", 5, 0, 5).aiMustChoose(true, 1));
// need target
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 1));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 1).aiMustChoose(false, 0));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 0, 1, 3).aiMustChoose(false, 0));
+ 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, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 1, 1, 3).aiMustChoose(true, 1));
+ 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, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 2, 1, 3).aiMustChoose(true, 1));
+ 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, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 3, 1, 3).aiMustChoose(true, 1));
+ 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, 3));
- runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 5));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 1).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 3).aiMustChoose(true, 1));
+ runner.registerDialog(new ChooseAmountTestableDialog(isYou, "need", 5, 1, 5).aiMustChoose(true, 1));
}
}
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java
index 8837d9b92ce..e0dbc8e45dd 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseCardsTestableDialog.java
@@ -13,6 +13,7 @@ import mage.players.Player;
import mage.target.TargetCard;
import mage.target.Targets;
import mage.target.common.TargetCardInHand;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -36,16 +37,17 @@ class ChooseCardsTestableDialog extends BaseTestableDialog {
public ChooseCardsTestableDialog(boolean isTargetChoice, boolean notTarget, boolean isYou, String name, TargetCard target) {
super(String.format("%s(%s, %s, cards)",
- isTargetChoice ? "player.chooseTarget" : "player.choose",
- isYou ? "you" : "AI",
- notTarget ? "not target" : "target"), name, target.toString());
+ isTargetChoice ? "player.chooseTarget" : "player.choose",
+ isYou ? "you" : "AI",
+ notTarget ? "not target" : "target"), name, target.toString(),
+ new TargetTestableResult());
this.isTargetChoice = isTargetChoice;
this.target = target.withNotTarget(notTarget);
this.isYou = isYou;
}
@Override
- public List 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();
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()));
boolean chooseRes;
+ String chooseDebugSource;
if (this.isTargetChoice) {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingCards, choosingTarget, source, game);
} else {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingCards, choosingTarget, source, game);
}
- List result = new ArrayList<>();
+ List res = new ArrayList<>();
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 {
- 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) {
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java
index 7e90c6cb124..e87451a54ae 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseChoiceTestableDialog.java
@@ -5,6 +5,7 @@ import mage.choices.*;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -24,28 +25,36 @@ class ChooseChoiceTestableDialog extends BaseTestableDialog {
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.choice = choice;
}
@Override
- public List 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;
Choice dialog = this.choice.copy();
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choose(Outcome.Benefit, dialog, game);
- List result = new ArrayList<>();
- result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
- result.add("");
+ List res = new ArrayList<>();
+ res.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
+ res.add("");
+ String choice;
if (dialog.isKeyChoice()) {
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 {
- 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) {
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java
index 68bc9bbb265..f25b2fae670 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChoosePileTestableDialog.java
@@ -6,6 +6,7 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -28,14 +29,18 @@ class ChoosePileTestableDialog extends BaseTestableDialog {
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.pileSize1 = pileSize1;
this.pileSize2 = pileSize2;
}
@Override
- public List 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
String mainMessage = "main message with html" + CardUtil.getSourceLogName(game, source);
@@ -47,11 +52,13 @@ class ChoosePileTestableDialog extends BaseTestableDialog {
List pile2 = all.stream().limit(this.pileSize2).collect(Collectors.toList());
Player choosingPlayer = this.isYou ? player : opponent;
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choosePile(Outcome.Benefit, mainMessage, pile1, pile2, game);
- List result = new ArrayList<>();
- result.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
- result.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2"));
- return result;
+ List res = new ArrayList<>();
+ res.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
+ res.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2"));
+
+ this.getResult().onFinish(chooseDebugSource, chooseRes, res);
}
static public void register(TestableDialogsRunner runner) {
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
index 65284618774..017aa055b27 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseTargetTestableDialog.java
@@ -6,6 +6,7 @@ import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.Targets;
+import mage.util.DebugUtil;
import java.util.ArrayList;
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) {
super(String.format("%s%s(%s, %s)",
- isPlayerChoice ? "player.choose" : "target.choose",
- isTargetChoice ? "Target" : "", // chooseTarget or choose
- isYou ? "you" : "AI",
- notTarget ? "not target" : "target"), name, target.toString());
+ isPlayerChoice ? "player.choose" : "target.choose",
+ isTargetChoice ? "Target" : "", // chooseTarget or choose
+ isYou ? "you" : "AI",
+ notTarget ? "not target" : "target"),
+ name,
+ target.toString(),
+ new TargetTestableResult()
+ );
this.isPlayerChoice = isPlayerChoice;
this.isTargetChoice = isTargetChoice;
this.target = target.withNotTarget(notTarget);
@@ -42,34 +47,48 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
}
@Override
- public List 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();
Player choosingPlayer = this.isYou ? player : opponent;
boolean chooseRes;
+ String chooseDebugSource;
if (this.isPlayerChoice) {
// player.chooseXXX
if (this.isTargetChoice) {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingTarget, source, game);
} else {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingTarget, source, game);
}
} else {
// target.chooseXXX
if (this.isTargetChoice) {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.chooseTarget(Outcome.Benefit, choosingPlayer.getId(), source, game);
} else {
+ chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.choose(Outcome.Benefit, choosingPlayer.getId(), source, game);
}
}
- List result = new ArrayList<>();
+ List res = new ArrayList<>();
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 {
- 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) {
@@ -84,37 +103,29 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) {
for (boolean isTargetChoice : isTargetChoices) {
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 1", createAnyTarget(1, 1)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3", createAnyTarget(3, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 5", createAnyTarget(5, 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 0-1", createAnyTarget(0, 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-5", createAnyTarget(0, 5)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-3", createAnyTarget(1, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-3", createAnyTarget(2, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 1-5", createAnyTarget(1, 5)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 2-5", createAnyTarget(2, 5)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "any 3-5", createAnyTarget(3, 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 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)).aiMustChoose(true, 1));
+ 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)).aiMustChoose(true, 5));
+ 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)).aiMustChoose(true, 1));
+ 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)).aiMustChoose(true, 5));
+ 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)).aiMustChoose(true, 3));
+ 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)).aiMustChoose(true, 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)).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 1", createImpossibleTarget(1, 1)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 3", createImpossibleTarget(3, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-1", createImpossibleTarget(0, 1)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 0-3", createImpossibleTarget(0, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 1-3", createImpossibleTarget(1, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible 2-3", createImpossibleTarget(2, 3)));
- runner.registerDialog(new ChooseTargetTestableDialog(isPlayerChoice, isTargetChoice, notTarget, isYou, "impossible max", createImpossibleTarget(0, Integer.MAX_VALUE)));
- //
- /*
- 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)));
- */
+ 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)).aiMustChoose(false, 0));
+ 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)).aiMustChoose(false, 0));
+ 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)).aiMustChoose(false, 0));
+ 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)).aiMustChoose(false, 0));
}
}
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java
index 3e8137af32b..80a1e7a8f69 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/ChooseUseTestableDialog.java
@@ -5,6 +5,7 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -27,7 +28,11 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
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.trueText = trueText;
this.falseText = falseText;
@@ -42,8 +47,9 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
}
@Override
- public List 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;
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseUse(
Outcome.Benefit,
messageMain,
@@ -53,9 +59,10 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
source,
game
);
- List result = new ArrayList<>();
- result.add(chooseRes ? "TRUE" : "FALSE");
- return result;
+ List res = new ArrayList<>();
+ res.add(chooseRes ? "TRUE" : "FALSE");
+
+ this.getResult().onFinish(chooseDebugSource, chooseRes, res);
}
static public void register(TestableDialogsRunner runner) {
diff --git a/Mage.Common/src/main/java/mage/utils/testers/GetAmountTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/GetAmountTestableDialog.java
index 0a66bac8f9d..2007af09a56 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/GetAmountTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/GetAmountTestableDialog.java
@@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
+import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -26,37 +27,53 @@ class GetAmountTestableDialog extends BaseTestableDialog {
public GetAmountTestableDialog(boolean isYou, int min, int max) {
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.min = min;
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
- public List 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;
String message = "message with html";
- int chooseRes;
- chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game);
- List result = new ArrayList<>();
- result.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
- return result;
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
+ int chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game);
+ List res = new ArrayList<>();
+ res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
+
+ ((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
}
static public void register(TestableDialogsRunner runner) {
List isYous = Arrays.asList(false, true);
for (boolean isYou : isYous) {
- runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 50));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 3));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 3, 10));
- runner.registerDialog(new GetAmountTestableDialog(isYou, 10, 10));
+ // TODO: add good and bad effects:
+ // - on good: choose random big value
+ // - on bad: choose lower value
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 0).aiMustChoose(0, 0));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 1).aiMustChoose(0, 1));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 3).aiMustChoose(0, 3));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 50).aiMustChoose(0, 50));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 0, 500).aiMustChoose(0, 500));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 1).aiMustChoose(1, 1));
+ runner.registerDialog(new GetAmountTestableDialog(isYou, 1, 3).aiMustChoose(1, 3));
+ 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));
}
}
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/GetMultiAmountTestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/GetMultiAmountTestableDialog.java
index 2c1f9046f3a..8f5f89be557 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/GetMultiAmountTestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/GetMultiAmountTestableDialog.java
@@ -5,12 +5,14 @@ import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
+import mage.util.DebugUtil;
import mage.util.MultiAmountMessage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
+import java.util.stream.IntStream;
/**
* Part of testable game dialogs
@@ -36,7 +38,9 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
public GetMultiAmountTestableDialog(boolean isYou, String info, int totalMin, int totalMax, List> options) {
super(String.format("player.getMultiAmount(%s)", isYou ? "you" : "AI"),
String.format("%s, %d options from [%d-%d]", info, options.size(), totalMin, totalMax),
- "");
+ "",
+ new MultiAmountTestableResult()
+ );
this.isYou = isYou;
this.totalMin = totalMin;
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 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
- public List 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;
//String message = "message with html";
- List chooseRes;
List options = this.amountOptions.stream().map(MultiAmountMessage::copy).collect(Collectors.toList());
- chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints(
+ String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
+ List chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints(
Outcome.Benefit,
options,
this.totalMin,
@@ -63,24 +89,24 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
game
);
- List result = new ArrayList<>();
- result.add(getGroup() + " - " + this.getName());
+ List res = new ArrayList<>();
+ res.add(getGroup() + " - " + this.getName());
int selectedIndex = -1;
int selectedTotal = 0;
for (Integer selectedValue : chooseRes) {
selectedIndex++;
selectedTotal += selectedValue;
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,
option.min,
option.max,
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) {
@@ -88,28 +114,29 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
for (boolean isYou : isYous) {
// make sure default values are valid due min/max settings
+ // TODO: add bad effect for AI (must test default distribution)
+
// 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, 3, genSameOptions(1, 0, 3, 0)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 1, genSameOptions(1, 1, 1, 1)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 1 def", 1, 3, genSameOptions(1, 1, 3, 1)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 5 def", 0, 10, genSameOptions(1, 0, 10, 5)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "one, 10 def", 10, 10, genSameOptions(1, 0, 10, 10)));
+ 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)).aiMustChoose(3));
+ 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)).aiMustChoose(3));
+ 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)).aiMustChoose(10));
// 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)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 1 def", 1, 5, genSameOptions(3, 1, 3, 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)));
- runner.registerDialog(new GetMultiAmountTestableDialog(isYou, "many, 20 def", 60, 60, genSameOptions(3, 0, 60, 20)));
+ 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, 1 def", 1, 5, genSameOptions(3, 1, 3, 1)).aiMustChoose(2, 2, 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, 20 def", 60, 60, genSameOptions(3, 0, 60, 20)).aiMustChoose(20, 20, 20));
// 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> genSameOptions(int amount, int min, int max, int def) {
+ private static List> genSameOptions(int options, int min, int max, int def) {
List> res = new ArrayList<>();
- for (int i = 0; i < amount; i++) {
+ for (int i = 0; i < options; i++) {
// min, max, default
res.add(Arrays.asList(min, max, def));
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/MultiAmountTestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/MultiAmountTestableResult.java
new file mode 100644
index 00000000000..53ae9a4f5ab
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/MultiAmountTestableResult.java
@@ -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 selectedValues;
+
+ boolean aiAssertEnabled = false;
+ List aiAssertValues = new ArrayList<>();
+
+ public void onFinish(String resDebugSource, boolean status, List info, List 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;
+ }
+}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/TargetTestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/TargetTestableResult.java
new file mode 100644
index 00000000000..1f49cb8d831
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/TargetTestableResult.java
@@ -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 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;
+ }
+}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java
index 1479ee9f306..d5e86740ff8 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialog.java
@@ -4,8 +4,6 @@ import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
-import java.util.List;
-
/**
* Part of testable game dialogs
*
@@ -17,7 +15,11 @@ import java.util.List;
*
* @author JayDi85
*/
-interface TestableDialog {
+public interface TestableDialog {
+
+ void setRegNumber(Integer regNumber);
+
+ Integer getRegNumber();
String getGroup();
@@ -25,7 +27,20 @@ interface TestableDialog {
String getDescription();
- List 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);
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java
index 3c2cfb3840d..0970d8ecc94 100644
--- a/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java
+++ b/Mage.Common/src/main/java/mage/utils/testers/TestableDialogsRunner.java
@@ -8,8 +8,10 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -55,7 +57,7 @@ import java.util.stream.Collectors;
*/
public class TestableDialogsRunner {
- private final List dialogs = new ArrayList<>();
+ private final Map dialogs = new LinkedHashMap<>();
static final int LAST_SELECTED_GROUP_ID = 997;
static final int LAST_SELECTED_DIALOG_ID = 998;
@@ -79,12 +81,14 @@ public class TestableDialogsRunner {
}
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) {
// select group or fast links
- List groups = this.dialogs.stream()
+ List groups = this.dialogs.values().stream()
.map(TestableDialog::getGroup)
.distinct()
.sorted()
@@ -129,8 +133,9 @@ public class TestableDialogsRunner {
// all fine, can show it and finish
lastSelectedGroup = needGroup;
lastSelectedDialog = needDialog;
- List resInfo = needDialog.showDialog(player, source, game, opponent);
- needDialog.showResult(player, game, String.join("
", resInfo));
+ needDialog.prepare();
+ needDialog.showDialog(player, source, game, opponent);
+ needDialog.showResult(player, game);
}
private Choice prepareSelectGroupChoice(List groups) {
@@ -199,5 +204,9 @@ public class TestableDialogsRunner {
}
return choice;
}
+
+ public Collection getDialogs() {
+ return this.dialogs.values();
+ }
}
diff --git a/Mage.Common/src/main/java/mage/utils/testers/TestableResult.java b/Mage.Common/src/main/java/mage/utils/testers/TestableResult.java
new file mode 100644
index 00000000000..7615aad678a
--- /dev/null
+++ b/Mage.Common/src/main/java/mage/utils/testers/TestableResult.java
@@ -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 getResDetails();
+
+ /**
+ * Save new result after show dialog
+ */
+ void onFinish(String chooseDebugSource, boolean resStatus, List 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();
+}
diff --git a/Mage.Common/src/main/java/mage/view/ChatMessage.java b/Mage.Common/src/main/java/mage/view/ChatMessage.java
index adf541553cb..02a5b6db31c 100644
--- a/Mage.Common/src/main/java/mage/view/ChatMessage.java
+++ b/Mage.Common/src/main/java/mage/view/ChatMessage.java
@@ -25,7 +25,12 @@ public class ChatMessage implements Serializable {
}
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 {
diff --git a/Mage.Common/src/main/java/mage/view/GameView.java b/Mage.Common/src/main/java/mage/view/GameView.java
index 59d47955831..4b6d06c7c95 100644
--- a/Mage.Common/src/main/java/mage/view/GameView.java
+++ b/Mage.Common/src/main/java/mage/view/GameView.java
@@ -67,6 +67,7 @@ public class GameView implements Serializable {
// TODO: implement and support in admin tools
private int totalErrorsCount;
private int totalEffectsCount;
+ private int gameCycle;
public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) {
Player createdForPlayer = null;
@@ -214,6 +215,7 @@ public class GameView implements Serializable {
this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed;
this.totalErrorsCount = game.getTotalErrorsCount();
this.totalEffectsCount = game.getTotalEffectsCount();
+ this.gameCycle = game.getState().getApplyEffectsCounter();
}
private void checkPaid(UUID uuid, StackAbility stackAbility) {
@@ -358,4 +360,8 @@ public class GameView implements Serializable {
public int getTotalEffectsCount() {
return this.totalEffectsCount;
}
+
+ public int getGameCycle() {
+ return this.gameCycle;
+ }
}
diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AbstractCommander.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AbstractCommander.java
index 99168e5ea3b..50468b91552 100644
--- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AbstractCommander.java
+++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/AbstractCommander.java
@@ -5,11 +5,13 @@ import mage.abilities.Ability;
import mage.abilities.common.CanBeYourCommanderAbility;
import mage.abilities.common.CommanderChooseColorAbility;
import mage.abilities.keyword.CompanionAbility;
+import mage.abilities.keyword.StationLevelAbility;
import mage.cards.Card;
import mage.cards.decks.Constructed;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidatorErrorType;
import mage.constants.CardType;
+import mage.constants.SubType;
import mage.filter.FilterMana;
import mage.util.CardUtil;
import mage.util.ManaUtil;
@@ -51,9 +53,19 @@ public abstract class AbstractCommander extends Constructed {
protected abstract boolean checkBanned(Map counts);
protected boolean checkCommander(Card commander, Set commanders) {
- return commander.hasCardTypeForDeckbuilding(CardType.CREATURE) && commander.isLegendary()
- || commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance())
- || (validators.stream().anyMatch(validator -> validator.specialCheck(commander)) && commanders.size() == 2);
+ if (commander.getAbilities().contains(CanBeYourCommanderAbility.getInstance())) {
+ return true;
+ }
+ 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 commanders) {
diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformUnlimited.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformUnlimited.java
new file mode 100644
index 00000000000..8dfcfc03f78
--- /dev/null
+++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/FreeformUnlimited.java
@@ -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;
+ }
+}
diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Historic.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Historic.java
index 26a3595c5c6..e6dd75163cb 100644
--- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Historic.java
+++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Historic.java
@@ -32,7 +32,6 @@ public class Historic extends Constructed {
banned.add("Agent of Treachery");
banned.add("Brainstorm");
banned.add("Channel");
- banned.add("Counterspell");
banned.add("Dark Ritual");
banned.add("Demonic Tutor");
banned.add("Fires of Invention");
diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java
index ec41f96cae2..0796aaf056d 100644
--- a/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java
+++ b/Mage.Server.Plugins/Mage.Deck.Constructed/src/mage/deck/Standard.java
@@ -19,10 +19,13 @@ public class Standard extends Constructed {
setCodes.addAll(makeLegalSets());
- banned.add("The Meathook Massacre");
- banned.add("Fable of the Mirror-Breaker");
- banned.add("Reckoner Bankbuster");
- banned.add("Invoke Despair");
+ banned.add("Abuelo's Awakening");
+ banned.add("Cori-Steel Cutter");
+ banned.add("Heartfire Hero");
+ banned.add("Hopeless Nightmare");
+ banned.add("Monstrous Rage");
+ banned.add("This Town Ain't Big Enough");
+ banned.add("Up the Beanstalk");
}
static List makeLegalSets() {
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java
index 14ba9d0ea2b..f25152f9a90 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java
+++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java
@@ -23,6 +23,7 @@ import mage.game.stack.StackAbility;
import mage.game.stack.StackObject;
import mage.player.ai.ma.optimizers.TreeOptimizer;
import mage.player.ai.ma.optimizers.impl.*;
+import mage.player.ai.score.GameStateEvaluator2;
import mage.player.ai.util.CombatInfo;
import mage.player.ai.util.CombatUtil;
import mage.players.Player;
@@ -113,6 +114,13 @@ public class ComputerPlayer6 extends ComputerPlayer {
this.actionCache = player.actionCache;
}
+ /**
+ * Change simulation timeout - used for AI stability tests only
+ */
+ public void setMaxThinkTimeSecs(int maxThinkTimeSecs) {
+ this.maxThinkTimeSecs = maxThinkTimeSecs;
+ }
+
@Override
public ComputerPlayer6 copy() {
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);
}
Game game = node.getGame();
- if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
- && Thread.interrupted()) {
- Thread.currentThread().interrupt();
- logger.debug("interrupted");
+ if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
+ logger.debug("AI game sim interrupted by timeout");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
}
// Condition to stop deeper simulation
@@ -399,7 +405,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (effect != null
&& stackObject.getControllerId().equals(playerId)) {
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)) {
Game sim = game.createSimulationForAI();
StackAbility newAbility = (StackAbility) stackObject.copy();
@@ -430,6 +436,8 @@ public class ComputerPlayer6 extends ComputerPlayer {
* @return
*/
protected Integer addActionsTimed() {
+ // TODO: all actions added and calculated one by one,
+ // multithreading do not supported here
// run new game simulation in parallel thread
FutureTask task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE));
threadPoolSimulations.execute(task);
@@ -445,15 +453,23 @@ public class ComputerPlayer6 extends ComputerPlayer {
}
} catch (TimeoutException | InterruptedException e) {
// 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);
} catch (ExecutionException e) {
// 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);
// real games: must catch and log
// 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);
}
} catch (Throwable e) {
@@ -465,11 +481,33 @@ public class ComputerPlayer6 extends ComputerPlayer {
return 0;
}
+ private void printFreezeNode(SimulationNode2 root) {
+ // print simple tree - there are possible multiple child nodes, but ignore it - same for abilities
+ List 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) {
- if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
- && Thread.interrupted()) {
- Thread.currentThread().interrupt();
- logger.info("interrupted");
+ if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
+ logger.debug("AI game sim interrupted by timeout");
return GameStateEvaluator2.evaluate(playerId, game).getTotalScore();
}
node.setGameValue(game.getState().getValue(true).hashCode());
@@ -497,9 +535,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
int bestValSubNodes = Integer.MIN_VALUE;
for (Ability action : allActions) {
actionNumber++;
- if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
- && Thread.interrupted()) {
- Thread.currentThread().interrupt();
+ if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.currentThread().isInterrupted()) {
logger.info("Sim Prio [" + depth + "] -- interrupted");
break;
}
@@ -848,10 +884,12 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (targets.isEmpty()) {
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) {
target.addTarget(targetId, source, game);
- if (target.isChoiceCompleted(game)) {
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
targets.clear();
return true;
}
@@ -866,10 +904,12 @@ public class ComputerPlayer6 extends ComputerPlayer {
if (targets.isEmpty()) {
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) {
target.add(targetId, game);
- if (target.isChoiceCompleted(game)) {
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
targets.clear();
return true;
}
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java
index f78cceee8f1..09af732d422 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java
+++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java
@@ -3,6 +3,7 @@ package mage.player.ai;
import mage.abilities.Ability;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
+import mage.player.ai.score.GameStateEvaluator2;
import org.apache.log4j.Logger;
import java.util.Date;
@@ -111,8 +112,6 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
protected void calculateActions(Game 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();
Game sim = createSimulation(game);
SimulationNode2.resetCount();
@@ -143,17 +142,9 @@ public class ComputerPlayer7 extends ComputerPlayer6 {
}
}
} 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 {
logger.debug("Next Action exists!");
}
diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
index 873b4c5bbcd..32bc78c75b7 100644
--- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
+++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java
@@ -13,7 +13,7 @@ import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep;
import mage.game.turn.EndOfCombatStep;
import mage.game.turn.Step;
-import mage.player.ai.GameStateEvaluator2;
+import mage.player.ai.score.GameStateEvaluator2;
import mage.players.Player;
import org.apache.log4j.Logger;
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java
index ace8620e6b2..20a8114e4f2 100644
--- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java
@@ -1,20 +1,12 @@
package mage.player.ai;
-import mage.ApprovingObject;
-import mage.ConditionalMana;
-import mage.MageObject;
-import mage.Mana;
+import mage.*;
import mage.abilities.*;
import mage.abilities.costs.mana.*;
-import mage.abilities.effects.Effect;
-import mage.abilities.effects.common.DamageTargetEffect;
-import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect;
-import mage.abilities.keyword.*;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.abilities.mana.ManaOptions;
import mage.cards.Card;
import mage.cards.Cards;
-import mage.cards.CardsImpl;
import mage.cards.RateCard;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
@@ -24,31 +16,20 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.choices.Choice;
import mage.constants.*;
-import mage.counters.CounterType;
-import mage.filter.FilterCard;
-import mage.filter.FilterPermanent;
-import mage.filter.StaticFilters;
-import mage.filter.common.*;
-import mage.filter.predicate.permanent.ControllerIdPredicate;
+import mage.filter.common.FilterLandCard;
import mage.game.Game;
-import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
-import mage.game.events.GameEvent;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
-import mage.game.stack.Spell;
-import mage.game.stack.StackObject;
import mage.game.tournament.Tournament;
-import mage.player.ai.simulators.CombatGroupSimulator;
-import mage.player.ai.simulators.CombatSimulator;
-import mage.player.ai.simulators.CreatureSimulator;
import mage.players.ManaPoolItem;
import mage.players.Player;
import mage.players.PlayerImpl;
import mage.players.net.UserData;
import mage.players.net.UserGroup;
-import mage.target.*;
-import mage.target.common.*;
+import mage.target.Target;
+import mage.target.TargetAmount;
+import mage.target.TargetCard;
import mage.util.*;
import org.apache.log4j.Logger;
@@ -57,21 +38,21 @@ import java.util.*;
import java.util.Map.Entry;
/**
- * AI: basic server side bot with simple actions support (game, draft, construction/sideboarding)
+ * AI: basic server side bot with simple actions support (game, draft, construction/sideboarding).
+ * Full and minimum implementation of all choose dialogs to allow AI to start and finish a real game.
+ * Used as parent class for any AI implementations.
*
- * TODO: combine choose and chooseTarget to single logic to use shared code
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public class ComputerPlayer extends PlayerImpl {
- private static final Logger log = Logger.getLogger(ComputerPlayer.class);
- private long lastThinkTime = 0; // msecs for last AI actions calc
+ private static final Logger logger = Logger.getLogger(ComputerPlayer.class);
- protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
+ protected static final int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
// debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout)
- protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = true; // DebugUtil.AI_ENABLE_DEBUG_MODE;
+ public static final boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; // DebugUtil.AI_ENABLE_DEBUG_MODE;
// AI agents uses game simulation thread for all calcs and it's high CPU consumption
// More AI threads - more parallel AI games can be calculate
@@ -80,16 +61,13 @@ public class ComputerPlayer extends PlayerImpl {
// How-to use:
// * 1 for debug or stable
// * 5 for good performance on average computer
- // * use your's CPU cores for best performance
+ // * use yours CPU cores for best performance
// TODO: add server config to control max AI threads (with CPU cores by default)
// TODO: rework AI implementation to use multiple sims calculation instead one by one
- final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
+ final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
- private final transient Map unplayable = new TreeMap<>();
- private final transient List playableNonInstant = new ArrayList<>();
- private final transient List playableInstant = new ArrayList<>();
- private final transient List playableAbilities = new ArrayList<>();
+ // remember picked cards for better draft choices
private final transient List pickedCards = new ArrayList<>();
private final transient List chosenColors = new ArrayList<>();
@@ -125,9 +103,8 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseMulligan(Game game) {
- log.debug("chooseMulligan");
if (hand.size() < 6
- || isTestsMode() // ignore mulligan in tests
+ || isTestMode() // ignore mulligan in tests
|| game.getClass().getName().contains("Momir") // ignore mulligan in Momir games
) {
return false;
@@ -144,1524 +121,253 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) {
- if (log.isDebugEnabled()) {
- log.debug("choose: " + outcome.toString() + ':' + target.toString());
- }
-
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
-
- // controller hints:
- // - target.getTargetController(), this.getId() -- player that must makes choices (must be same with this.getId)
- // - target.getAbilityController(), abilityControllerId -- affected player/controller for all actions/filters
- // - affected controller can be different from target controller (another player makes choices for controller)
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
- UUID sourceId = source != null ? source.getSourceId() : null;
-
- boolean required = target.isRequired(sourceId, game);
- Set possibleTargets = target.possibleTargets(abilityControllerId, source, game);
- if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getMinNumberOfTargets()) {
- required = false;
- }
-
- UUID randomOpponentId = getRandomOpponent(game);
-
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- return selectPlayer(outcome, target, abilityControllerId, randomOpponentId, game, required);
- }
-
- if (target.getOriginalTarget() instanceof TargetDiscard) {
- findPlayables(game);
- // discard not playable first
- if (!unplayable.isEmpty()) {
- for (int i = unplayable.size() - 1; i >= 0; i--) {
- UUID targetId = unplayable.values().toArray(new Card[0])[i].getId();
- if (target.canTarget(abilityControllerId, targetId, source, game) && !target.contains(targetId)) {
- target.add(targetId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- if (!hand.isEmpty()) {
- for (int i = 0; i < hand.size(); i++) {
- UUID targetId = hand.toArray(new UUID[0])[i];
- if (target.canTarget(abilityControllerId, targetId, source, game) && !target.contains(targetId)) {
- target.add(targetId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetControlledPermanent
- || target.getOriginalTarget() instanceof TargetSacrifice) {
- List targets;
- TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
- targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets());
- if (!outcome.isGood()) {
- Collections.reverse(targets);
- }
- for (Permanent permanent : targets) {
- if (origTarget.canTarget(abilityControllerId, permanent.getId(), source, game, false) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanent) {
- FilterPermanent filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
- filter = (FilterPermanent) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported permanent filter in computer's choose method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List targets;
- if (outcome.isCanTargetAll()) {
- targets = threats(null, source, filter, game, target.getTargets());
- } else {
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, filter, game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, filter, game, target.getTargets());
- }
- if (targets.isEmpty() && target.isRequired()) {
- if (!outcome.isGood()) {
- targets = threats(abilityControllerId, source, filter, game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, filter, game, target.getTargets());
- }
- }
- }
-
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- // AI workaround to stop adding more targets in "up to" on bad outcome for itself
- if (target.isChosen(game) && target.getMinNumberOfTargets() == target.getTargets().size()) {
- if (outcome.isGood() && hasOpponent(permanent.getControllerId(), game)) {
- return isAddedSomething;
- }
- if (!outcome.isGood() && !hasOpponent(permanent.getControllerId(), game)) {
- return isAddedSomething;
- }
- }
- // add the target
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInHand
- || (target.getZone() == Zone.HAND && (target.getOriginalTarget() instanceof TargetCard))) {
- List cards = new ArrayList<>();
- for (UUID cardId : target.possibleTargets(this.getId(), source, game)) {
- Card card = game.getCard(cardId);
- if (card != null) {
- cards.add(card);
- }
- }
- while ((outcome.isGood() ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game); // TODO: why it add as much as possible instead go to isChosen check like above?
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetAnyTarget) {
- List targets;
- TargetAnyTarget origTarget = (TargetAnyTarget) target.getOriginalTarget();
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (!required) {
- return isAddedSomething;
- }
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
- List targets;
- TargetPermanentOrPlayer origTarget = (TargetPermanentOrPlayer) target.getOriginalTarget();
- List ownedTargets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- List opponentTargets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- if (outcome.isGood()) {
- targets = ownedTargets;
- } else {
- targets = opponentTargets;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- isAddedSomething = true;
- target.add(permanent.getId(), game);
- return true;
- }
- }
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (!target.isRequired(sourceId, game) || target.getMinNumberOfTargets() == 0) {
- return isAddedSomething; // TODO: need research why it here (between diff type of targets)
- }
- if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- target.add(getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- if (outcome.isGood()) { // no other valid targets so use a permanent
- targets = opponentTargets;
- } else {
- targets = ownedTargets;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.add(permanent.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : player.getGraveyard().getCards(game)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
-
- // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay)
- boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile;
- while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInGraveyard
- || (target.getZone() == Zone.GRAVEYARD && (target.getOriginalTarget() instanceof TargetCard))) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : player.getGraveyard().getCards(game)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
-
- // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay)
- boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile;
- while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game))
- && !cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
-
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard
- || target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- TargetCard originalTarget = (TargetCard) target.getOriginalTarget();
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards(originalTarget.getFilter(), game));
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null && !target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInExile) {
- FilterCard filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
- filter = (FilterCard) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported exile target filter in computer's choose method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List cards = game.getExile().getCards(filter, game);
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null && !target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetSource) {
- Set targets;
- targets = target.possibleTargets(abilityControllerId, source, game);
- for (UUID targetId : targets) {
- MageObject targetObject = game.getObject(targetId);
- if (targetObject != null) {
- if (target.canTarget(abilityControllerId, targetObject.getId(), source, game)) {
- if (!target.contains(targetObject.getId())) {
- target.add(targetObject.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- return true;
- }
- }
- }
- }
- }
- if (!required) {
- return isAddedSomething;
- }
- throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCard
- && (target.getZone() == Zone.COMMAND)) { // Hellkite Courser
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- for (Card card : game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game)) {
- cards.add(card);
- }
- }
- }
- while (!cards.isEmpty()) {
- Card card = selectCard(abilityControllerId, cards, outcome, target, game);
- if (card != null) {
- cards.remove(card);
- if (!target.contains(card.getId())) {
- target.add(card.getId(), game);
- isAddedSomething = true;
- if (target.isChosen(game)) {
- //return true; // TODO: why it add as much as possible instead go to isChosen check like above?
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
- } //end of choose method
-
- @Override
- public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
- if (log.isDebugEnabled()) {
- log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
- }
-
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
-
- // target - real target, make all changes and add targets to it
- // target.getOriginalTarget() - copy spell effect replaces original target with TargetWithAdditionalFilter
- // use originalTarget to get filters and target class info
- // source can be null (as example: legendary rule permanent selection)
- UUID sourceId = source != null ? source.getSourceId() : null;
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- boolean required = target.isRequired(sourceId, game);
- Set possibleTargets = target.possibleTargets(abilityControllerId, source, game);
- if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getMinNumberOfTargets()) {
- required = false;
- }
-
- List goodList = new ArrayList<>();
- List badList = new ArrayList<>();
- List allList = new ArrayList<>();
-
- UUID randomOpponentId = getRandomOpponent(game);
-
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- return selectPlayerTarget(outcome, target, source, abilityControllerId, randomOpponentId, game, required);
- }
-
- // Angel of Serenity trigger
- if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- isAddedSomething = false;
- for (Card card : cards) {
- // check permanents first; they have more intrinsic worth
- if (card instanceof Permanent) {
- Permanent p = ((Permanent) card);
- if (outcome.isGood()
- && p.isControlledBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, p.getId(), source, game) && !target.contains(p.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(p.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- if (!outcome.isGood()
- && !p.isControlledBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, p.getId(), source, game) && !target.contains(p.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(p.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- // check the graveyards last
- if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) {
- if (outcome.isGood()
- && card.isOwnedBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- if (!outcome.isGood()
- && !card.isOwnedBy(abilityControllerId)) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetDiscard
- || target.getOriginalTarget() instanceof TargetCardInHand) {
- isAddedSomething = false;
- if (outcome.isGood()) {
- // good
- Cards cards = new CardsImpl(possibleTargets);
- List cardsInHand = new ArrayList<>(cards.getCards(game));
- while (!target.isChosen(game)
- && !cardsInHand.isEmpty()
- && target.getMaxNumberOfTargets() > target.getTargets().size()) {
- Card card = selectBestCardTarget(cardsInHand, Collections.emptyList(), target, source, game);
- if (card != null) {
- if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- cardsInHand.remove(card);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- } else {
- // bad
- findPlayables(game);
- for (Card card : unplayable.values()) {
- if (possibleTargets.contains(card.getId())
- && target.canTarget(abilityControllerId, card.getId(), source, game)
- && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- if (!hand.isEmpty()) {
- for (Card card : hand.getCards(game)) {
- if (possibleTargets.contains(card.getId())
- && target.canTarget(abilityControllerId, card.getId(), source, game)
- && !target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetControlledPermanent
- || target.getOriginalTarget() instanceof TargetSacrifice) {
- TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
- List targets;
- targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets());
- if (!outcome.isGood()) {
- Collections.reverse(targets);
- }
- isAddedSomething = false;
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- if (target.getMinNumberOfTargets() <= target.getTargets().size() && (!outcome.isGood() || target.getMaxNumberOfTargets() <= target.getTargets().size())) {
- return true; // TODO: need research - is it good optimization for good/bad effects?
- }
- }
- }
- return isAddedSomething;
-
- }
-
- // TODO: implemented findBestPlayerTargets
- // TODO: add findBest*Targets for all target types
- // TODO: Much of this code needs to be re-written to move code into Target.possibleTargets
- // A) Having it here makes this function ridiculously long
- // B) Each time a new target type is added, people must remember to add it here
- if (target.getOriginalTarget() instanceof TargetPermanent) {
- FilterPermanent filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
- filter = (FilterPermanent) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- findBestPermanentTargets(outcome, abilityControllerId, sourceId, source, filter,
- game, target, goodList, badList, allList);
-
- // use good list all the time and add maximum targets
- isAddedSomething = false;
- for (Permanent permanent : goodList) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (target.getTargets().size() >= target.getMaxNumberOfTargets()) {
- break;
- }
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- }
- }
-
- // use bad list only on required target and add minimum targets
- if (required) {
- for (Permanent permanent : badList) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (target.getTargets().size() >= target.getMinNumberOfTargets()) {
- break;
- }
- target.addTarget(permanent.getId(), source, game);
- isAddedSomething = true;
- }
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetAnyTarget) {
- List targets;
- TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget());
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
-
- if (targets.isEmpty()) {
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
- }
-
- if (targets.isEmpty() && required) {
- targets = game.getBattlefield().getActivePermanents(((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), playerId, game);
- }
- for (Permanent permanent : targets) {
- List alreadyTargeted = target.getTargets();
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
- tryAddTarget(target, permanent.getId(), source, game);
- }
- }
- }
-
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
-
- //if (!target.isRequired())
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) {
- List targets;
- TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget());
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
-
- if (targets.isEmpty()) {
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
- }
-
- if (targets.isEmpty() && target.isRequired(source)) {
- targets = game.getBattlefield().getActivePermanents(((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), playerId, game);
- }
- for (Permanent permanent : targets) {
- List alreadyTargeted = target.getTargets();
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) {
- return tryAddTarget(target, permanent.getId(), source, game);
- }
- }
- }
- }
-
- if (target.getOriginalTarget() instanceof TargetPlayerOrPlaneswalker
- || target.getOriginalTarget() instanceof TargetOpponentOrPlaneswalker) {
- List targets;
- TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget());
-
- // TODO: in multiplayer game there many opponents - if random opponents don't have targets then AI must use next opponent, but it skips
- // (e.g. you randomOpponentId must be replaced by List randomOpponents)
- // normal cycle (good for you, bad for opponents)
- // possible good/bad permanents
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets());
- }
-
- // possible good/bad players
- if (targets.isEmpty()) {
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
- }
-
- // can't find targets (e.g. effect is bad, but you need take targets from yourself)
- if (targets.isEmpty() && required) {
- targets = game.getBattlefield().getActivePermanents(origTarget.getFilterPermanent(), playerId, game);
- }
-
- // try target permanent
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- return tryAddTarget(target, permanent.getId(), source, game);
- }
- }
-
- // try target player as normal
- if (outcome.isGood()) {
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game) && !target.contains(randomOpponentId)) {
- return tryAddTarget(target, randomOpponentId, source, game);
- }
-
- // try target player as bad (bad on itself, good on opponent)
- for (UUID opponentId : game.getOpponents(getId(), true)) {
- if (target.canTarget(abilityControllerId, opponentId, source, game) && !target.contains(opponentId)) {
- return tryAddTarget(target, opponentId, source, game);
- }
- }
- if (target.canTarget(abilityControllerId, getId(), source, game) && !target.contains(getId())) {
- return tryAddTarget(target, getId(), source, game);
- }
-
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null && !target.contains(card.getId())) {
- return tryAddTarget(target, card.getId(), source, game);
- }
- //if (!target.isRequired())
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInLibrary) {
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getLibrary().getCards(game));
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null && !target.contains(card.getId())) {
- return tryAddTarget(target, card.getId(), source, game);
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard) {
- List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards((FilterCard) target.getFilter(), game));
- isAddedSomething = false;
- while (!target.isChosen(game) && !cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game); // TODO: why it add as much as possible instead go to isChosen check like above in choose?
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetSpell
- || target.getOriginalTarget() instanceof TargetStackObject) {
- if (!game.getStack().isEmpty()) {
- for (StackObject o : game.getStack()) {
- if (o instanceof Spell
- && !source.getId().equals(o.getStackAbility().getId())
- && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)
- && !target.contains(o.getId())) {
- return tryAddTarget(target, o.getId(), source, game);
- }
- }
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetSpellOrPermanent) {
- // TODO: Also check if a spell should be selected
- TargetSpellOrPermanent origTarget = (TargetSpellOrPermanent) target.getOriginalTarget();
- List targets;
- boolean outcomeTargets = true;
- if (outcome.isGood()) {
- targets = threats(abilityControllerId, source, origTarget.getPermanentFilter(), game, target.getTargets());
- } else {
- targets = threats(randomOpponentId, source, origTarget.getPermanentFilter(), game, target.getTargets());
- }
- if (targets.isEmpty() && required) {
- targets = threats(null, source, origTarget.getPermanentFilter(), game, target.getTargets());
- Collections.reverse(targets);
- outcomeTargets = false;
- }
- for (Permanent permanent : targets) {
- if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.contains(permanent.getId())) {
- target.addTarget(permanent.getId(), source, game);
- if (!outcomeTargets || target.getMaxNumberOfTargets() <= target.getTargets().size()) {
- return true; // TODO: need logic research (e.g. select as much as possible on good outcome?)
- }
- }
- }
- if (!game.getStack().isEmpty()) {
- for (StackObject stackObject : game.getStack()) {
- if (stackObject instanceof Spell && source != null && !source.getId().equals(stackObject.getStackAbility().getId())) {
- if (target.getFilter().match(stackObject, game) && !target.contains(stackObject.getId())) {
- return tryAddTarget(target, stackObject.getId(), source, game);
- }
- }
- }
- }
- return false;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInOpponentsGraveyard) {
- List cards = new ArrayList<>();
- for (UUID uuid : game.getOpponents(getId(), true)) {
- Player player = game.getPlayer(uuid);
- if (player != null) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- //if (!target.isRequired())
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetDefender) {
- List targets = new ArrayList<>(possibleTargets);
- isAddedSomething = false;
- while (!targets.isEmpty()) {
- UUID randomDefender = RandomUtil.randomFromCollection(possibleTargets);
- if (randomDefender != null) {
- targets.remove(randomDefender);
- if (!target.contains(randomDefender)) {
- isAddedSomething = true;
- target.addTarget(randomDefender, source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInExile) {
- FilterCard filter = null;
- if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
- filter = (FilterCard) target.getOriginalTarget().getFilter();
- }
- if (filter == null) {
- throw new IllegalStateException("Unsupported exile target filter in computer's chooseTarget method: "
- + target.getOriginalTarget().getClass().getCanonicalName());
- }
-
- List cards = new ArrayList<>();
- for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) {
- Card card = game.getCard(uuid);
- if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) {
- cards.add(card);
- }
- }
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetActivatedAbility) {
- List stackObjects = new ArrayList<>();
- for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) {
- StackObject stackObject = game.getStack().getStackObject(uuid);
- if (stackObject != null) {
- stackObjects.add(stackObject);
- }
- }
- while (!stackObjects.isEmpty()) {
- StackObject pick = stackObjects.get(0);
- if (pick != null) {
- stackObjects.remove(0);
- if (!target.contains(pick.getId())) {
- isAddedSomething = true;
- target.addTarget(pick.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetActivatedOrTriggeredAbility) {
- List targets = new ArrayList<>(target.possibleTargets(source.getControllerId(), source, game));
- isAddedSomething = false;
- while (!targets.isEmpty()) {
- UUID id = targets.get(0);
- if (id != null) {
- targets.remove(0);
- if (!target.contains(id)) {
- isAddedSomething = true;
- target.addTarget(id, source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) {
- List cards = new ArrayList<>();
- for (Player player : game.getPlayers().values()) {
- cards.addAll(player.getGraveyard().getCards(game));
- cards.addAll(game.getBattlefield().getAllActivePermanents(new FilterPermanent(), player.getId(), game));
- }
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- }
-
- if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
- List cards = new ArrayList<>(new CardsImpl(possibleTargets).getCards(game));
- isAddedSomething = false;
- while (!cards.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cards, outcome, target, source, game);
- if (card != null) {
- cards.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- isAddedSomething = true;
- target.addTarget(card.getId(), source, game);
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
- }
- return isAddedSomething;
- }
-
- throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName());
- } //end of chooseTarget method
-
- protected Card selectCard(UUID abilityControllerId, List cards, Outcome outcome, Target target, Game game) {
- return selectCardInner(abilityControllerId, cards, outcome, target, null, game);
- }
-
- protected Card selectCardTarget(UUID abilityControllerId, List cards, Outcome outcome, Target target, Ability targetingSource, Game game) {
- return selectCardInner(abilityControllerId, cards, outcome, target, targetingSource, game);
+ return makeChoice(outcome, target, source, game, null);
}
/**
- * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
+ * Default choice logic for any choose dialogs due effect's outcome and possible target priority
*/
- protected Card selectCardInner(UUID abilityControllerId, List cards, Outcome outcome, Target target, Ability targetingSource, Game game) {
- Card card;
- while (!cards.isEmpty()) {
- if (outcome.isGood()) {
- card = selectBestCardInner(cards, Collections.emptyList(), target, targetingSource, game);
- } else {
- card = selectWorstCardInner(cards, Collections.emptyList(), target, targetingSource, game);
- }
- if (!target.getTargets().contains(card.getId())) {
- if (targetingSource != null) {
- if (target.canTarget(abilityControllerId, card.getId(), targetingSource, game)) {
- return card;
- }
- } else {
- return card;
- }
- }
- cards.remove(card); // TODO: research parent code - is it depends on original list? Can be bugged
+ private boolean makeChoice(Outcome outcome, Target target, Ability source, Game game, Cards fromCards) {
+ // choose itself for starting player all the time
+ if (target.getMessage(game).equals("Select a starting player")) {
+ target.add(this.getId(), game);
+ return true;
}
- return null;
+
+ // nothing to choose
+ if (fromCards != null && fromCards.isEmpty()) {
+ return false;
+ }
+
+ UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
+
+ // nothing to choose, e.g. X=0
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return false;
+ }
+
+ // default logic for any targets
+ PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
+ possibleTargetsSelector.findNewTargets(fromCards);
+ // good targets -- choose as much as possible
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ target.add(item.getId(), game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
+ }
+ }
+ // bad targets -- choose as low as possible
+ for (MageItem item : possibleTargetsSelector.getBadTargets()) {
+ if (target.isChosen(game)) {
+ break;
+ }
+ target.add(item.getId(), game);
+ }
+
+ return target.isChosen(game) && !target.getTargets().isEmpty();
+ }
+
+ /**
+ * Default choice logic for X or amount values
+ */
+ private int makeChoiceAmount(int min, int max, Game game, Ability source, boolean isManaPay) {
+ // fast calc on nothing to choose
+ if (min >= max) {
+ return min;
+ }
+
+ // TODO: add good/bad effects support
+ // TODO: add simple game simulations like declare blocker (need to find only workable payment)?
+ // TODO: remove random logic or make it more stable (e.g. use same value in same game cycle)
+
+ // protection from too big values
+ int realMin = min;
+ int realMax = max;
+ if (max == Integer.MAX_VALUE) {
+ realMax = Math.max(realMin, 10); // AI don't need huge values for X, cause can't use infinite combos
+ }
+
+ int xValue;
+ if (isManaPay) {
+ // as X mana payment - due available mana
+ xValue = Math.max(0, getAvailableManaProducers(game).size() - source.getManaCostsToPay().getUnpaid().manaValue());
+ } else {
+ // as X actions
+ xValue = RandomUtil.nextInt(realMax + 1);
+ }
+
+ if (xValue > realMax) {
+ xValue = realMax;
+ }
+ if (xValue < realMin) {
+ xValue = realMin;
+ }
+
+ return xValue;
+ }
+
+ @Override
+ public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
+ return makeChoice(outcome, target, source, game, null);
}
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
- // TODO: make same code for chooseTarget (without filter and target type dependence)
- if (log.isDebugEnabled()) {
- log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
- }
- UUID sourceId = source != null ? source.getSourceId() : null;
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- // process multiple opponents by random
- List opponents = new ArrayList<>(game.getOpponents(getId(), true));
- Collections.shuffle(opponents);
-
- List targets;
-
- // ONE KILL PRIORITY: player -> planeswalker -> creature
- if (outcome == Outcome.Damage) {
- // player kill
- for (UUID opponentId : opponents) {
- Player opponent = game.getPlayer(opponentId);
- if (opponent != null
- && target.canTarget(abilityControllerId, opponentId, source, game)
- && opponent.getLife() <= target.getAmountRemaining()) {
- return tryAddTarget(target, opponentId, opponent.getLife(), source, game);
- }
- }
-
- // permanents kill
- for (UUID opponentId : opponents) {
- targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT_CREATURE_OR_PLANESWALKER_A, game, target.getTargets());
-
- // planeswalker kill
- for (Permanent permanent : targets) {
- if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- int loy = permanent.getCounters(game).getCount(CounterType.LOYALTY);
- if (loy <= target.getAmountRemaining()) {
- return tryAddTarget(target, permanent.getId(), loy, source, game);
- }
- }
- }
-
- // creature kill
- for (Permanent permanent : targets) {
- if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- if (permanent.getToughness().getValue() <= target.getAmountRemaining()) {
- return tryAddTarget(target, permanent.getId(), permanent.getToughness().getValue(), source, game);
- }
- }
- }
- }
- }
-
- // NORMAL PRIORITY: planeswalker -> player -> creature
- // own permanents will be checked multiple times... that's ok
- for (UUID opponentId : opponents) {
- if (outcome.isGood()) {
- targets = threats(getId(), source, StaticFilters.FILTER_PERMANENT, game, target.getTargets());
- } else {
- targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT, game, target.getTargets());
- }
-
- // planeswalkers
- for (Permanent permanent : targets) {
- if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
- }
- }
-
- // players
- if (outcome.isGood() && target.canTarget(abilityControllerId, getId(), source, game)) {
- return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game);
- }
- if (!outcome.isGood() && target.canTarget(abilityControllerId, opponentId, source, game)) {
- return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game);
- }
-
- // creature
- for (Permanent permanent : targets) {
- if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
- }
- }
- }
-
- // BAD PRIORITY, e.g. need bad target on yourself or good target on opponent
- // priority: creature (non killable, killable) -> planeswalker -> player
- if (!target.isRequired(sourceId, game)) {
+ // nothing to choose, e.g. X=0
+ target.prepareAmount(source, game);
+ if (target.getAmountRemaining() <= 0) {
+ return false;
+ }
+ if (target.getMaxNumberOfTargets() == 0 && target.getMinNumberOfTargets() == 0) {
return false;
}
- for (UUID opponentId : opponents) {
- if (!outcome.isGood()) {
- // bad on yourself, uses the weakest targets
- targets = threats(getId(), source, StaticFilters.FILTER_PERMANENT, game, target.getTargets(), false);
- } else {
- targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT, game, target.getTargets(), false);
- }
- // creatures - non killable (TODO: add extra skill checks like undestructeable)
- for (Permanent permanent : targets) {
- if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- int safeDamage = Math.min(permanent.getToughness().getValue() - 1, target.getAmountRemaining());
- if (safeDamage > 0) {
- return tryAddTarget(target, permanent.getId(), safeDamage, source, game);
+ UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
+
+ // nothing to choose, e.g. X=0
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return false;
+ }
+
+ PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
+ possibleTargetsSelector.findNewTargets(null);
+
+ // nothing to choose, e.g. no valid targets
+ if (!possibleTargetsSelector.hasAnyTargets()) {
+ return false;
+ }
+
+ // KILL PRIORITY
+ if (outcome == Outcome.Damage) {
+ // opponent first
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId()) || !(item instanceof Player)) {
+ continue;
+ }
+ int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
+ if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
+ target.addTarget(item.getId(), leftLife, source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
}
}
}
- // creatures - all
- for (Permanent permanent : targets) {
- if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
+ // opponent's creatures second
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId()) || (item instanceof Player)) {
+ continue;
+ }
+ int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
+ if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
+ target.addTarget(item.getId(), leftLife, source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
+ }
}
}
- // planeswalkers
- for (Permanent permanent : targets) {
- if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
- return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
+ // opponent's any
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId())) {
+ continue;
+ }
+ target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
}
}
+
+ // own - non-killable
+ for (MageItem item : possibleTargetsSelector.getBadTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId())) {
+ continue;
+ }
+ // stop as fast as possible on bad outcome
+ if (target.isChosen(game)) {
+ return !target.getTargets().isEmpty();
+ }
+ int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
+ if (leftLife > 1) {
+ target.addTarget(item.getId(), Math.min(leftLife - 1, target.getAmountRemaining()), source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
+ }
+ }
+ }
+
+ // own - any
+ for (MageItem item : possibleTargetsSelector.getBadTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId())) {
+ continue;
+ }
+ // stop as fast as possible on bad outcome
+ if (target.isChosen(game)) {
+ return !target.getTargets().isEmpty();
+ }
+ target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
+ }
+ }
+
+ return target.isChosen(game);
}
- // players
- for (UUID opponentId : opponents) {
- if (target.canTarget(abilityControllerId, getId(), source, game)) {
- // on itself
- return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game);
- } else if (target.canTarget(abilityControllerId, opponentId, source, game)) {
- // on opponent
- return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game);
+
+ // non-damage effect like counters - give all to first valid item
+ for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId())) {
+ continue;
+ }
+ target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
+ }
+ }
+ for (MageItem item : possibleTargetsSelector.getBadTargets()) {
+ if (target.getAmountRemaining() <= 0) {
+ break;
+ }
+ if (target.contains(item.getId())) {
+ continue;
+ }
+ // stop as fast as possible on bad outcome
+ if (target.isChosen(game)) {
+ return !target.getTargets().isEmpty();
+ }
+ target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
+ if (target.isChoiceCompleted(abilityControllerId, source, game)) {
+ return true;
}
}
- // it's ok on no targets available
- log.warn("No proper AI target handling or can't find permanents/cards to target: " + target.getClass().getName());
- return false;
+ return target.isChosen(game) && !target.getTargets().isEmpty();
}
@Override
public boolean priority(Game game) {
- game.resumeTimer(getTurnControlledBy());
- boolean result = priorityPlay(game);
- game.pauseTimer(getTurnControlledBy());
- return result;
- }
-
- private boolean priorityPlay(Game game) {
- UUID opponentId = getRandomOpponent(game);
- if (game.isActivePlayer(playerId)) {
- if (game.isMainPhase() && game.getStack().isEmpty()) {
- playLand(game);
- }
- switch (game.getTurnStepType()) {
- case UPKEEP:
- // TODO: is it needs here? Need research (e.g. for better choose in upkeep triggers)?
- findPlayables(game);
- break;
- case DRAW:
- break;
- case PRECOMBAT_MAIN:
- findPlayables(game);
- if (!playableAbilities.isEmpty()) {
- for (ActivatedAbility ability : playableAbilities) {
- if (ability.canActivate(playerId, game).canActivate()) {
- if (ability.getEffects().hasOutcome(ability, Outcome.PutLandInPlay)) {
- if (this.activateAbility(ability, game)) {
- return true;
- }
- }
- if (ability.getEffects().hasOutcome(ability, Outcome.PutCreatureInPlay)) {
- if (getOpponentBlockers(opponentId, game).size() <= 1) {
- if (this.activateAbility(ability, game)) {
- return true;
- }
- }
- }
- }
- }
- }
- break;
- case DECLARE_BLOCKERS:
- findPlayables(game);
- playRemoval(game.getCombat().getBlockers(), game);
- playDamage(game.getCombat().getBlockers(), game);
- break;
- case END_COMBAT:
- findPlayables(game);
- playDamage(game.getCombat().getBlockers(), game);
- break;
- case POSTCOMBAT_MAIN:
- findPlayables(game);
- if (game.getStack().isEmpty()) {
- if (!playableNonInstant.isEmpty()) {
- for (Card card : playableNonInstant) {
- if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
- if (this.activateAbility(card.getSpellAbility(), game)) {
- return true;
- }
- }
- }
- }
- if (!playableAbilities.isEmpty()) {
- for (ActivatedAbility ability : playableAbilities) {
- if (ability.canActivate(playerId, game).canActivate()) {
- if (!(ability.getEffects().get(0) instanceof BecomesCreatureSourceEffect)) {
- if (this.activateAbility(ability, game)) {
- return true;
- }
- }
- }
- }
- }
- }
- break;
- }
- } else {
- //respond to opponent events
- switch (game.getTurnStepType()) {
- case UPKEEP:
- findPlayables(game);
- break;
- case DECLARE_ATTACKERS:
- findPlayables(game);
- playRemoval(game.getCombat().getAttackers(), game);
- playDamage(game.getCombat().getAttackers(), game);
- break;
- case END_COMBAT:
- findPlayables(game);
- playDamage(game.getCombat().getAttackers(), game);
- break;
- }
- }
+ // minimum implementation for do nothing
pass(game);
- return true;
- } // end priorityPlay method
-
- protected void playLand(Game game) {
- log.debug("playLand");
- Set lands = new LinkedHashSet<>();
- for (Card landCard : hand.getCards(new FilterLandCard(), game)) {
- // remove lands that can not be played
- boolean canPlay = false;
- for (Ability ability : landCard.getAbilities(game)) {
- if (ability instanceof PlayLandAbility) {
- if (!game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, landCard.getId(), ability, playerId), null, game, true)) {
- canPlay = true;
- }
- }
- }
- if (canPlay) {
- lands.add(landCard);
- }
- }
- while (!lands.isEmpty() && this.canPlayLand()) {
- if (lands.size() == 1) {
- this.playLand(lands.iterator().next(), game, false);
- } else {
- playALand(lands, game);
- }
- }
- }
-
- protected void playALand(Set lands, Game game) {
- log.debug("playALand");
- //play a land that will allow us to play an unplayable
- for (Mana mana : unplayable.keySet()) {
- for (Card card : lands) {
- for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) {
- for (Mana netMana : ability.getNetMana(game)) {
- if (netMana.enough(mana)) {
- this.playLand(card, game, false);
- lands.remove(card);
- return;
- }
- }
- }
- }
- }
- //play a land that will get us closer to playing an unplayable
- for (Mana mana : unplayable.keySet()) {
- for (Card card : lands) {
- for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) {
- for (Mana netMana : ability.getNetMana(game)) {
- if (mana.contains(netMana)) {
- this.playLand(card, game, false);
- lands.remove(card);
- return;
- }
- }
- }
- }
- }
- //play first available land
- this.playLand(lands.iterator().next(), game, false);
- lands.remove(lands.iterator().next());
- }
-
- protected void findPlayables(Game game) {
- playableInstant.clear();
- playableNonInstant.clear();
- unplayable.clear();
- playableAbilities.clear();
- Set nonLands = hand.getCards(new FilterNonlandCard(), game);
- ManaOptions available = getManaAvailable(game);
-// available.addMana(manaPool.getMana());
-
- for (Card card : nonLands) {
- ManaOptions options = card.getManaCost().getOptions();
- if (!card.getManaCost().getVariableCosts().isEmpty()) {
- //don't use variable mana costs unless there is at least 3 extra mana for X
- for (Mana option : options) {
- option.add(Mana.GenericMana(3));
- }
- }
- for (Mana mana : options) {
- for (Mana avail : available) {
- if (mana.enough(avail)) {
- SpellAbility ability = card.getSpellAbility();
- GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, playerId);
- castEvent.setZone(game.getState().getZone(card.getMainCard().getId()));
- if (ability != null && ability.canActivate(playerId, game).canActivate()
- && !game.getContinuousEffects().preventedByRuleModification(castEvent, ability, game, true)) {
- if (card.isInstant(game)
- || card.hasAbility(FlashAbility.getInstance(), game)) {
- playableInstant.add(card);
- } else {
- playableNonInstant.add(card);
- }
- }
- } else if (!playableInstant.contains(card) && !playableNonInstant.contains(card)) {
- unplayable.put(mana.needed(avail), card);
- }
- }
- }
- }
- // TODO: wtf?! change to player.getPlayable
- for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
- for (ActivatedAbility ability : permanent.getAbilities().getActivatedAbilities(Zone.BATTLEFIELD)) {
- if (!ability.isManaActivatedAbility() && ability.canActivate(playerId, game).canActivate()) {
- if (ability instanceof EquipAbility && permanent.getAttachedTo() != null) {
- continue;
- }
- ManaOptions abilityOptions = ability.getManaCosts().getOptions();
- if (!ability.getManaCosts().getVariableCosts().isEmpty()) {
- //don't use variable mana costs unless there is at least 3 extra mana for X
- for (Mana option : abilityOptions) {
- option.add(Mana.GenericMana(3));
- }
- }
- if (abilityOptions.isEmpty()) {
- playableAbilities.add(ability);
- } else {
- for (Mana mana : abilityOptions) {
- for (Mana avail : available) {
- if (mana.enough(avail)) {
- playableAbilities.add(ability);
- }
- }
- }
- }
- }
- }
- }
- for (Card card : graveyard.getCards(game)) {
- for (ActivatedAbility ability : card.getAbilities(game).getActivatedAbilities(Zone.GRAVEYARD)) {
- if (ability.canActivate(playerId, game).canActivate()) {
- ManaOptions abilityOptions = ability.getManaCosts().getOptions();
- if (abilityOptions.isEmpty()) {
- playableAbilities.add(ability);
- } else {
- for (Mana mana : abilityOptions) {
- for (Mana avail : available) {
- if (mana.enough(avail)) {
- playableAbilities.add(ability);
- }
- }
- }
- }
- }
- }
- }
- if (log.isDebugEnabled()) {
- log.debug("findPlayables: " + playableInstant.toString() + "---" + playableNonInstant.toString() + "---" + playableAbilities.toString());
- }
+ return false;
}
@Override
@@ -1702,7 +408,7 @@ public class ComputerPlayer extends PlayerImpl {
for (Mana mana : manaAbility.getNetMana(game)) {
// if mana ability can produce non-useful mana then ignore whole ability here (example: {R} or {G})
// (AI can't choose a good mana option, so make sure any selection option will be compatible with cost)
- // AI support {Any} choice by lastUnpaidMana, so it can safly used in includesMana
+ // AI support {Any} choice by lastUnpaidMana, so it can safety used in includesMana
if (!unpaid.getMana().includesMana(mana)) {
continue ManaAbility;
} else if (mana.getAny() > 0) {
@@ -1807,7 +513,7 @@ public class ComputerPlayer extends PlayerImpl {
}
}
}
- // then pay colorless hybrid - more restrictive than mono hybrid
+ // then pay colorless hybrid - more restrictive than monohybrid
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof ColorlessHybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) {
@@ -1825,7 +531,7 @@ public class ComputerPlayer extends PlayerImpl {
}
}
}
- // then pay mono hybrid
+ // then pay monohybrid
for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) {
if (cost instanceof MonoHybridManaCost) {
for (Mana netMana : manaAbility.getNetMana(game)) {
@@ -1929,7 +635,7 @@ public class ComputerPlayer extends PlayerImpl {
);
}
- // cost can contains multiple mana types, must check each type (is it possible to pay a cost)
+ // cost can contain multiple mana types, must check each type (is it possible to pay a cost)
for (ManaType checkType : ManaUtil.getManaTypesInCost(checkCost)) {
// affected asThoughMana effect must fit a checkType with pool mana
ManaType possibleAsThoughPoolManaType = game.getContinuousEffects().asThoughMana(checkType, possiblePoolItem, abilityToPay.getSourceId(), abilityToPay, abilityToPay.getControllerId(), game);
@@ -1938,10 +644,10 @@ public class ComputerPlayer extends PlayerImpl {
}
boolean canPay;
if (possibleAsThoughPoolManaType == ManaType.COLORLESS) {
- // colorless can be payed by any color from the pool
+ // colorless can be paid by any color from the pool
canPay = possiblePoolItem.count() > 0;
} else {
- // colored must be payed by specific color from the pool (AsThough already changed it to fit with mana pool)
+ // colored must be paid by specific color from the pool (AsThough already changed it to fit with mana pool)
canPay = possiblePoolItem.get(possibleAsThoughPoolManaType) > 0;
}
if (canPay) {
@@ -1956,23 +662,20 @@ public class ComputerPlayer extends PlayerImpl {
Abilities manaAbilities = mageObject.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game);
if (manaAbilities.size() > 1) {
// Sort mana abilities by number of produced manas, to use ability first that produces most mana (maybe also conditional if possible)
- Collections.sort(manaAbilities, new Comparator() {
- @Override
- public int compare(ActivatedManaAbilityImpl a1, ActivatedManaAbilityImpl a2) {
- int a1Max = 0;
- for (Mana netMana : a1.getNetMana(game)) {
- if (netMana.count() > a1Max) {
- a1Max = netMana.count();
- }
+ Collections.sort(manaAbilities, (a1, a2) -> {
+ int a1Max = 0;
+ for (Mana netMana : a1.getNetMana(game)) {
+ if (netMana.count() > a1Max) {
+ a1Max = netMana.count();
}
- int a2Max = 0;
- for (Mana netMana : a2.getNetMana(game)) {
- if (netMana.count() > a2Max) {
- a2Max = netMana.count();
- }
- }
- return CardUtil.overflowDec(a2Max, a1Max);
}
+ int a2Max = 0;
+ for (Mana netMana : a2.getNetMana(game)) {
+ if (netMana.count() > a2Max) {
+ a2Max = netMana.count();
+ }
+ }
+ return CardUtil.overflowDec(a2Max, a1Max);
});
}
return manaAbilities;
@@ -1988,7 +691,6 @@ public class ComputerPlayer extends PlayerImpl {
* costs that can't be paid by any other producers
*
* @param unpaid - the amount of unpaid mana costs
- * @param game
* @return List
*/
private List getSortedProducers(ManaCosts unpaid, Game game) {
@@ -2024,12 +726,7 @@ public class ComputerPlayer extends PlayerImpl {
private List sortByValue(Map map) {
List> list = new LinkedList<>(map.entrySet());
- Collections.sort(list, new Comparator>() {
- @Override
- public int compare(Entry o1, Entry o2) {
- return (o1.getValue().compareTo(o2.getValue()));
- }
- });
+ Collections.sort(list, Comparator.comparing(Entry::getValue));
List result = new ArrayList<>();
for (Entry entry : list) {
result.add(entry.getKey());
@@ -2039,39 +736,7 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public int announceX(int min, int max, String message, Game game, Ability source, boolean isManaPay) {
- // fast calc on nothing to choose
- if (min >= max) {
- return min;
- }
-
- // TODO: add good/bad effects support
- // TODO: add simple game simulations like declare blocker (need to find only workable payment)?
- // TODO: remove random logic or make it more stable (e.g. use same value in same game cycle)
-
- // protection from too big values
- int realMin = min;
- int realMax = max;
- if (max == Integer.MAX_VALUE) {
- realMax = Math.max(realMin, 10); // AI don't need huge values for X, cause can't use infinite combos
- }
-
- int xValue;
- if (isManaPay) {
- // as X mana payment - due available mana
- xValue = Math.max(0, getAvailableManaProducers(game).size() - source.getManaCostsToPay().getUnpaid().manaValue());
- } else {
- // as X actions
- xValue = RandomUtil.nextInt(realMax + 1);
- }
-
- if (xValue > realMax) {
- xValue = realMax;
- }
- if (xValue < realMin) {
- xValue = realMin;
- }
-
- return xValue;
+ return makeChoiceAmount(min, max, game, source, isManaPay);
}
@Override
@@ -2085,12 +750,11 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) {
- return this.chooseUse(outcome, message, null, null, null, source, game);
+ return chooseUse(outcome, message, null, null, null, source, game);
}
@Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
- log.debug("chooseUse: " + outcome.isGood());
// Be proactive! Always use abilities, the evaluation function will decide if it's good or not
// Otherwise some abilities won't be used by AI like LoseTargetEffect that has "bad" outcome
// but still is good when targets opponent
@@ -2099,13 +763,14 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean choose(Outcome outcome, Choice choice, Game game) {
- log.debug("choose 3");
//TODO: improve this
// choose creature type
// TODO: WTF?! Creature types dialog text can changes, need to replace that code
if (choice.getMessage() != null && (choice.getMessage().equals("Choose creature type") || choice.getMessage().equals("Choose a creature type"))) {
- chooseCreatureType(outcome, choice, game);
+ if (chooseCreatureType(outcome, choice, game)) {
+ return true;
+ }
}
// choose the correct color to pay a spell (use last unpaid ability for color hint)
@@ -2206,133 +871,38 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
- if (cards == null || cards.isEmpty()) {
- return false;
- }
-
- boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- // we still use playerId when getting cards even if they don't control the search
- List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), playerId, source, game));
- isAddedSomething = false;
- while (!cardChoices.isEmpty()) {
- Card card = selectCardTarget(abilityControllerId, cardChoices, outcome, target, source, game);
- if (card != null) {
- cardChoices.remove(card); // selectCard don't remove cards (only on second+ tries)
- if (!target.contains(card.getId())) {
- target.addTarget(card.getId(), source, game);
- isAddedSomething = true;
- if (target.isChoiceCompleted(game)) {
- return true;
- }
- }
- } else {
- break;
- }
-
- // try to fill as much as possible for good effect (see while end) or half for bad (see if)
- if (target.isChosen(game) && !outcome.isGood() && target.getTargets().size() > target.getMinNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getMinNumberOfTargets()) / 2) {
- return true;
- }
- }
- return isAddedSomething;
+ return makeChoice(outcome, target, source, game, cards);
}
@Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
- log.debug("choose 2");
- if (cards == null || cards.isEmpty()) {
- return true;
- }
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target.getTargetController() != null
- && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), abilityControllerId, source, game));
- do {
- Card card = selectCard(abilityControllerId, cardChoices, outcome, target, game);
- if (card != null) {
- target.add(card.getId(), game);
- cardChoices.remove(card); // selectCard don't remove cards (only on second+ tries)
- } else {
- // We don't have any valid target to choose so stop choosing
- return target.isChosen(game);
- }
- // try to fill as much as possible for good effect (see while end) or half for bad (see if)
- if (outcome == Outcome.Neutral && target.getTargets().size() > target.getMinNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getMinNumberOfTargets()) / 2) {
- return target.isChosen(game);
- }
- } while (target.getTargets().size() < target.getMaxNumberOfTargets());
- return true;
+ return makeChoice(outcome, target, source, game, cards);
}
@Override
public boolean choosePile(Outcome outcome, String message, List extends Card> pile1, List extends Card> pile2, Game game) {
//TODO: improve this
- return true;
+ return true; // select left pile all the time
}
@Override
public void selectAttackers(Game game, UUID attackingPlayerId) {
- log.debug("selectAttackers");
- UUID opponentId = game.getCombat().getDefenders().iterator().next();
- Attackers attackers = getPotentialAttackers(game);
- List blockers = getOpponentBlockers(opponentId, game);
- List actualAttackers = new ArrayList<>();
- if (blockers.isEmpty()) {
- actualAttackers = attackers.getAttackers();
- } else if (attackers.size() - blockers.size() >= game.getPlayer(opponentId).getLife()) {
- actualAttackers = attackers.getAttackers();
- } else {
- CombatSimulator combat = simulateAttack(attackers, blockers, opponentId, game);
- if (combat.rating > 2) {
- for (CombatGroupSimulator group : combat.groups) {
- this.declareAttacker(group.attackers.get(0).id, group.defenderId, game, false);
- }
- }
- }
- for (Permanent attacker : actualAttackers) {
- this.declareAttacker(attacker.getId(), opponentId, game, false);
- }
+ // do nothing, parent class must implement it
}
@Override
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
- log.debug("selectBlockers");
-
- List blockers = getAvailableBlockers(game);
-
- CombatSimulator sim = simulateBlock(CombatSimulator.load(game), blockers, game);
-
- List groups = game.getCombat().getGroups();
- for (int i = 0; i < groups.size(); i++) {
- for (CreatureSimulator creature : sim.groups.get(i).blockers) {
- groups.get(i).addBlocker(creature.id, playerId, game);
- }
- }
+ // do nothing, parent class must implement it
}
@Override
public int chooseReplacementEffect(Map effectsMap, Map objectsMap, Game game) {
- log.debug("chooseReplacementEffect");
//TODO: implement this
- return 0;
+ return 0; // select first effect all the time
}
@Override
public Mode chooseMode(Modes modes, Ability source, Game game) {
- log.debug("chooseMode");
if (modes.getMode() != null && modes.getMaxModes(game, source) == modes.getSelectedModes().size()) {
// mode was already set by the AI
return modes.getMode();
@@ -2341,52 +911,30 @@ public class ComputerPlayer extends PlayerImpl {
// spell modes simulated by AI, see addModeOptions
// trigger modes chooses here
// TODO: add AI support to select best modes, current code uses first valid mode
- AvailableMode:
- for (Mode mode : modes.getAvailableModes(source, game)) {
- for (UUID selectedModeId : modes.getSelectedModes()) {
- Mode selectedMode = modes.get(selectedModeId);
- if (selectedMode.getId().equals(mode.getId())) {
- continue AvailableMode;
- }
- }
- if (mode.getTargets().canChoose(source.getControllerId(), source, game)) { // and where targets are available
- return mode;
- }
- }
- return null;
+ return modes.getAvailableModes(source, game).stream()
+ .filter(mode -> !modes.getSelectedModes().contains(mode.getId()))
+ .filter(mode -> mode.getTargets().canChoose(source.getControllerId(), source, game))
+ .findFirst()
+ .orElse(null);
}
@Override
public TriggeredAbility chooseTriggeredAbility(List abilities, Game game) {
- log.debug("chooseTriggeredAbility: " + abilities.toString());
//TODO: improve this
if (!abilities.isEmpty()) {
- return abilities.get(0);
+ return abilities.get(0); // select first trigger all the time
}
return null;
}
@Override
- // TODO: add AI support with outcome and replace random with min/max
public int getAmount(int min, int max, String message, Ability source, Game game) {
- log.debug("getAmount");
-
- // fast calc on nothing to choose
- if (min >= max) {
- return min;
- }
-
- if (min == 0) {
- return RandomUtil.nextInt(CardUtil.overflowInc(max, 1));
- }
- return min;
+ return makeChoiceAmount(min, max, game, source, false);
}
@Override
public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages,
int totalMin, int totalMax, MultiAmountType type, Game game) {
- log.debug("getMultiAmount");
-
int needCount = messages.size();
List defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax);
if (needCount == 0) {
@@ -2401,7 +949,7 @@ public class ComputerPlayer extends PlayerImpl {
}
// GOOD effect
- // values must be stable, so AI must able to simulate it and choose correct actions
+ // values must be stable, so AI must be able to simulate it and choose correct actions
// fill max values as much as possible
return MultiAmountType.prepareMaxValues(messages, totalMin, totalMax);
}
@@ -2413,8 +961,8 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public void sideboard(Match match, Deck deck) {
- //TODO: improve this
- match.submitDeck(playerId, deck);
+ // TODO: improve this
+ match.submitDeck(playerId, deck); // do not change a deck
}
private static void addBasicLands(Deck deck, String landName, int number) {
@@ -2422,7 +970,7 @@ public class ComputerPlayer extends PlayerImpl {
CardCriteria criteria = new CardCriteria();
if (!landSets.isEmpty()) {
- criteria.setCodes(landSets.toArray(new String[landSets.size()]));
+ criteria.setCodes(landSets.toArray(new String[0]));
}
criteria.rarities(Rarity.LAND).name(landName);
List cards = CardRepository.instance.findCards(criteria);
@@ -2478,13 +1026,10 @@ public class ComputerPlayer extends PlayerImpl {
// sort card pool by top score
List sortedCards = new ArrayList<>(cardPool);
- Collections.sort(sortedCards, new Comparator() {
- @Override
- public int compare(Card o1, Card o2) {
- Integer score1 = RateCard.rateCard(o1, colors);
- Integer score2 = RateCard.rateCard(o2, colors);
- return score2.compareTo(score1);
- }
+ Collections.sort(sortedCards, (o1, o2) -> {
+ Integer score1 = RateCard.rateCard(o1, colors);
+ Integer score2 = RateCard.rateCard(o2, colors);
+ return score2.compareTo(score1);
});
// get top cards
@@ -2576,28 +1121,11 @@ public class ComputerPlayer extends PlayerImpl {
tournament.submitDeck(playerId, deck);
}
- public Card selectBestCard(List cards, List chosenColors) {
- return selectBestCardInner(cards, chosenColors, null, null, null);
- }
-
- public Card selectBestCardTarget(List cards, List chosenColors, Target target, Ability targetingSource, Game game) {
- return selectBestCardInner(cards, chosenColors, target, targetingSource, game);
- }
-
- /**
- * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
- */
- public Card selectBestCardInner(List cards, List chosenColors, Target target, Ability targetingSource, Game game) {
+ public Card makePickCard(List cards, List chosenColors) {
if (cards.isEmpty()) {
return null;
}
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target != null && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
Card bestCard = null;
int maxScore = 0;
for (Card card : cards) {
@@ -2606,13 +1134,7 @@ public class ComputerPlayer extends PlayerImpl {
if (bestCard == null) { // we need any card to prevent NPE in callers
betterCard = true;
} else if (score > maxScore) { // we need better card
- if (target != null && targetingSource != null && game != null) {
- // but also check it can be targeted
- betterCard = target.canTarget(abilityControllerId, card.getId(), targetingSource, game);
- } else {
- // target object wasn't provided, so accepting it anyway
- betterCard = true;
- }
+ betterCard = true;
}
// is it better than previous one?
if (betterCard) {
@@ -2623,69 +1145,23 @@ public class ComputerPlayer extends PlayerImpl {
return bestCard;
}
- public Card selectWorstCard(List cards, List chosenColors) {
- return selectWorstCardInner(cards, chosenColors, null, null, null);
- }
-
- public Card selectWorstCardTarget(List cards, List chosenColors, Target target, Ability targetingSource, Game game) {
- return selectWorstCardInner(cards, chosenColors, target, targetingSource, game);
- }
-
- /**
- * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
- */
- public Card selectWorstCardInner(List cards, List chosenColors, Target target, Ability targetingSource, Game game) {
- if (cards.isEmpty()) {
- return null;
- }
-
- // sometimes a target selection can be made from a player that does not control the ability
- UUID abilityControllerId = playerId;
- if (target != null && target.getAbilityController() != null) {
- abilityControllerId = target.getAbilityController();
- }
-
- Card worstCard = null;
- int minScore = Integer.MAX_VALUE;
- for (Card card : cards) {
- int score = RateCard.rateCard(card, chosenColors);
- boolean worseCard = false;
- if (worstCard == null) { // we need any card to prevent NPE in callers
- worseCard = true;
- } else if (score < minScore) { // we need worse card
- if (target != null && targetingSource != null && game != null) {
- // but also check it can be targeted
- worseCard = target.canTarget(abilityControllerId, card.getId(), targetingSource, game);
- } else {
- // target object wasn't provided, so accepting it anyway
- worseCard = true;
- }
- }
- // is it worse than previous one?
- if (worseCard) {
- minScore = score;
- worstCard = card;
- }
- }
- return worstCard;
- }
-
@Override
public void pickCard(List cards, Deck deck, Draft draft) {
+ // method used by DRAFT bot too
if (cards.isEmpty()) {
throw new IllegalArgumentException("No cards to pick from.");
}
try {
- Card bestCard = selectBestCard(cards, chosenColors);
+ Card bestCard = makePickCard(cards, chosenColors);
int maxScore = RateCard.rateCard(bestCard, chosenColors);
int pickedCardRate = RateCard.getBaseCardScore(bestCard);
if (pickedCardRate <= 30) {
// if card is bad
// try to counter pick without any color restriction
- Card counterPick = selectBestCard(cards, Collections.emptyList());
+ Card counterPick = makePickCard(cards, Collections.emptyList());
int counterPickScore = RateCard.getBaseCardScore(counterPick);
- // card is really good
+ // card is perfect
// take it!
if (counterPickScore >= 80) {
bestCard = counterPick;
@@ -2708,19 +1184,15 @@ public class ComputerPlayer extends PlayerImpl {
colors += symbol.toString();
}
}
- log.debug("[DEBUG] AI picked: " + bestCard.getName() + ", score=" + maxScore + ", deck colors=" + colors);
draft.addPick(playerId, bestCard.getId(), null);
} catch (Exception e) {
- log.debug("Exception during AI pick card for draft playerId= " + getId());
+ logger.error("Error during AI pick card for draft playerId = " + getId(), e);
draft.addPick(playerId, cards.get(0).getId(), null);
}
}
/**
* Remember picked card with its score.
- *
- * @param card
- * @param score
*/
protected void rememberPick(Card card, int score) {
pickedCards.add(new PickedCard(card, score));
@@ -2730,22 +1202,17 @@ public class ComputerPlayer extends PlayerImpl {
* Choose 2 deck colors for draft: 1. there should be at least 3 cards in
* card pool 2. at least 2 cards should have different colors 3. get card
* colors as chosen starting from most rated card
- *
- * @return
*/
protected List chooseDeckColorsIfPossible() {
if (pickedCards.size() > 2) {
// sort by score and color mana symbol count in descending order
- pickedCards.sort(new Comparator() {
- @Override
- public int compare(PickedCard o1, PickedCard o2) {
- if (o1.score.equals(o2.score)) {
- Integer i1 = RateCard.getColorManaCount(o1.card);
- Integer i2 = RateCard.getColorManaCount(o2.card);
- return i2.compareTo(i1);
- }
- return o2.score.compareTo(o1.score);
+ pickedCards.sort((o1, o2) -> {
+ if (o1.score.equals(o2.score)) {
+ Integer i1 = RateCard.getColorManaCount(o1.card);
+ Integer i2 = RateCard.getColorManaCount(o2.card);
+ return i2.compareTo(i1);
}
+ return o2.score.compareTo(o1.score);
});
Set chosenSymbols = new HashSet<>();
for (PickedCard picked : pickedCards) {
@@ -2793,105 +1260,6 @@ public class ComputerPlayer extends PlayerImpl {
}
}
- protected Attackers getPotentialAttackers(Game game) {
- log.debug("getAvailableAttackers");
- Attackers attackers = new Attackers();
- List creatures = super.getAvailableAttackers(game);
- for (Permanent creature : creatures) {
- int potential = combatPotential(creature, game);
- if (potential > 0 && creature.getPower().getValue() > 0) {
- List l = attackers.get(potential);
- if (l == null) {
- attackers.put(potential, l = new ArrayList<>());
- }
- l.add(creature);
- }
- }
- return attackers;
- }
-
- protected int combatPotential(Permanent creature, Game game) {
- log.debug("combatPotential");
- if (!creature.canAttack(null, game)) {
- return 0;
- }
- int potential = creature.getPower().getValue();
- potential += creature.getAbilities().getEvasionAbilities().size();
- potential += creature.getAbilities().getProtectionAbilities().size();
- potential += creature.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) ? 1 : 0;
- potential += creature.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) ? 2 : 0;
- potential += creature.getAbilities().containsKey(TrampleAbility.getInstance().getId()) ? 1 : 0;
- return potential;
- }
-
- protected List getOpponentBlockers(UUID opponentId, Game game) {
- FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock();
- return game.getBattlefield().getAllActivePermanents(blockFilter, opponentId, game);
- }
-
- protected CombatSimulator simulateAttack(Attackers attackers, List blockers, UUID opponentId, Game game) {
- log.debug("simulateAttack");
- List attackersList = attackers.getAttackers();
- CombatSimulator best = new CombatSimulator();
- int bestResult = 0;
- //use binary digits to calculate powerset of attackers
- int powerElements = (int) Math.pow(2, attackersList.size());
- for (int i = 1; i < powerElements; i++) {
- String binary = Integer.toBinaryString(i);
- while (binary.length() < attackersList.size()) {
- binary = '0' + binary;
- }
- List trialAttackers = new ArrayList<>();
- for (int j = 0; j < attackersList.size(); j++) {
- if (binary.charAt(j) == '1') {
- trialAttackers.add(attackersList.get(j));
- }
- }
- CombatSimulator combat = new CombatSimulator();
- for (Permanent permanent : trialAttackers) {
- combat.groups.add(new CombatGroupSimulator(opponentId, Arrays.asList(permanent.getId()), new ArrayList(), game));
- }
- CombatSimulator test = simulateBlock(combat, blockers, game);
- if (test.evaluate() > bestResult) {
- best = test;
- bestResult = test.evaluate();
- }
- }
-
- return best;
- }
-
- protected CombatSimulator simulateBlock(CombatSimulator combat, List blockers, Game game) {
- log.debug("simulateBlock");
-
- TreeNode simulations;
-
- simulations = new TreeNode<>(combat);
- addBlockSimulations(blockers, simulations, game);
- combat.simulate(game);
-
- return getWorstSimulation(simulations);
-
- }
-
- protected void addBlockSimulations(List blockers, TreeNode node, Game game) {
- int numGroups = node.getData().groups.size();
- Copier copier = new Copier<>();
- for (Permanent blocker : blockers) {
- List subList = remove(blockers, blocker);
- for (int i = 0; i < numGroups; i++) {
- if (node.getData().groups.get(i).canBlock(blocker, game)) {
- CombatSimulator combat = copier.copy(node.getData());
- combat.groups.get(i).blockers.add(new CreatureSimulator(blocker));
- TreeNode child = new TreeNode<>(combat);
- node.addChild(child);
- addBlockSimulations(subList, child, game);
- combat.simulate(game);
- }
- }
- }
- }
-
protected List remove(List source, Permanent element) {
List newList = new ArrayList<>();
for (Permanent permanent : source) {
@@ -2902,172 +1270,13 @@ public class ComputerPlayer extends PlayerImpl {
return newList;
}
- protected CombatSimulator getBestSimulation(TreeNode simulations) {
- CombatSimulator best = simulations.getData();
- int bestResult = best.evaluate();
- for (TreeNode node : simulations.getChildren()) {
- CombatSimulator bestSub = getBestSimulation(node);
- if (bestSub.evaluate() > bestResult) {
- best = node.getData();
- bestResult = best.evaluate();
- }
- }
- return best;
- }
-
- protected CombatSimulator getWorstSimulation(TreeNode simulations) {
- CombatSimulator worst = simulations.getData();
- int worstResult = worst.evaluate();
- for (TreeNode node : simulations.getChildren()) {
- CombatSimulator worstSub = getWorstSimulation(node);
- if (worstSub.evaluate() < worstResult) {
- worst = node.getData();
- worstResult = worst.evaluate();
- }
- }
- return worst;
- }
-
- protected void findBestPermanentTargets(Outcome outcome, UUID abilityControllerId, UUID sourceId, Ability source, FilterPermanent filter, Game game, Target target,
- List goodList, List badList, List allList) {
- // searching for most valuable/powerfull permanents
- goodList.clear();
- badList.clear();
- allList.clear();
- List usedTargets = target.getTargets();
-
- // search all
- for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, abilityControllerId, source, game)) {
- if (usedTargets.contains(permanent.getId())) {
- continue;
- }
-
- if (outcome.isGood()) {
- // good effect
- if (permanent.isControlledBy(abilityControllerId)) {
- goodList.add(permanent);
- } else {
- badList.add(permanent);
- }
- } else {
- // bad effect
- if (permanent.isControlledBy(abilityControllerId)) {
- badList.add(permanent);
- } else {
- goodList.add(permanent);
- }
- }
- }
-
- // sort from tiny to big (more valuable)
- PermanentComparator comparator = new PermanentComparator(game);
- goodList.sort(comparator);
- badList.sort(comparator);
-
- // most valueable goes first in good list
- Collections.reverse(goodList);
- // most weakest goes first in bad list (no need to reverse)
- //Collections.reverse(badList);
-
- allList.addAll(goodList);
- allList.addAll(badList);
-
- // "can target all mode" don't need your/opponent lists -- all targets goes with same value
- if (outcome.isCanTargetAll()) {
- allList.sort(comparator); // bad sort
- if (outcome.isGood()) {
- Collections.reverse(allList); // good sort
- }
- goodList.clear();
- goodList.addAll(allList);
- badList.clear();
- badList.addAll(allList);
- }
- }
-
- protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets) {
- return threats(playerId, source, filter, game, targets, true);
- }
-
- protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets, boolean mostValueableGoFirst) {
- // most valuable/powerfull permanents goes at first
- List threats;
- if (playerId == null) {
- threats = game.getBattlefield().getActivePermanents(filter, this.getId(), source, game); // all permanents within the range of the player
- } else {
- FilterPermanent filterCopy = filter.copy();
- filterCopy.add(new ControllerIdPredicate(playerId));
- threats = game.getBattlefield().getActivePermanents(filter, this.getId(), source, game);
- }
- Iterator it = threats.iterator();
- while (it.hasNext()) { // remove permanents already targeted
- Permanent test = it.next();
- if (targets.contains(test.getId()) || (playerId != null && !test.getControllerId().equals(playerId))) {
- it.remove();
- }
- }
- Collections.sort(threats, new PermanentComparator(game));
- if (mostValueableGoFirst) {
- Collections.reverse(threats);
- }
- return threats;
- }
-
protected void logList(String message, List list) {
StringBuilder sb = new StringBuilder();
sb.append(message).append(": ");
for (MageObject object : list) {
sb.append(object.getName()).append(',');
}
- log.info(sb.toString());
- }
-
- protected void logAbilityList(String message, List list) {
- StringBuilder sb = new StringBuilder();
- sb.append(message).append(": ");
- for (Ability ability : list) {
- sb.append(ability.getRule()).append(',');
- }
- log.debug(sb.toString());
- }
-
- private void playRemoval(Set creatures, Game game) {
- for (UUID creatureId : creatures) {
- for (Card card : this.playableInstant) {
- if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
- for (Effect effect : card.getSpellAbility().getEffects()) {
- if (effect.getOutcome() == Outcome.DestroyPermanent || effect.getOutcome() == Outcome.ReturnToHand) {
- if (card.getSpellAbility().getTargets().get(0).canTarget(creatureId, card.getSpellAbility(), game)) {
- if (this.activateAbility(card.getSpellAbility(), game)) {
- return;
- }
- }
- }
- }
- }
- }
- }
- }
-
- private void playDamage(Set creatures, Game game) {
- for (UUID creatureId : creatures) {
- Permanent creature = game.getPermanent(creatureId);
- for (Card card : this.playableInstant) {
- if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
- for (Effect effect : card.getSpellAbility().getEffects()) {
- if (effect instanceof DamageTargetEffect) {
- if (card.getSpellAbility().getTargets().get(0).canTarget(creatureId, card.getSpellAbility(), game)) {
- if (((DamageTargetEffect) effect).getAmount() > (creature.getPower().getValue() - creature.getDamage())) {
- if (this.activateAbility(card.getSpellAbility(), game)) {
- return;
- }
- }
- }
- }
- }
- }
- }
- }
+ logger.info(sb.toString());
}
@Override
@@ -3080,151 +1289,13 @@ public class ComputerPlayer extends PlayerImpl {
return new ComputerPlayer(this);
}
- @Deprecated // TODO: replace by standard while cycle with cards.isempty and addTarget
- private boolean tryAddTarget(Target target, UUID id, Ability source, Game game) {
- // workaround to to check successfull targets add
- int before = target.getTargets().size();
- target.addTarget(id, source, game);
- int after = target.getTargets().size();
- return before != after;
- }
-
- private boolean tryAddTarget(Target target, UUID id, int amount, Ability source, Game game) {
- // workaround to to check successfull targets add
- int before = target.getTargets().size();
- target.addTarget(id, amount, source, game);
- int after = target.getTargets().size();
- return before != after;
- }
-
- private boolean selectPlayer(Outcome outcome, Target target, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- return selectPlayerInner(outcome, target, null, abilityControllerId, randomOpponentId, game, required);
- }
-
- private boolean selectPlayerTarget(Outcome outcome, Target target, Ability targetingSource, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- return selectPlayerInner(outcome, target, targetingSource, abilityControllerId, randomOpponentId, game, required);
- }
-
- /**
- * Sets a possible target player. Depends on bad/good outcome
- *
- * Return false on no more valid targets, e.g. can stop choose dialog
- *
- * @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
- */
- private boolean selectPlayerInner(Outcome outcome, Target target, Ability targetingSource, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) {
- Outcome affectedOutcome;
- if (abilityControllerId == this.playerId) {
- // selects for itself
- affectedOutcome = outcome;
- } else {
- // selects for another player
- affectedOutcome = Outcome.inverse(outcome);
- }
-
- if (target.getOriginalTarget() instanceof TargetOpponent) {
- if (targetingSource == null) {
- if (target.canTarget(randomOpponentId, game)) {
- if (!target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game)) {
- if (!target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- }
- for (UUID possibleOpponentId : game.getOpponents(getId(), true)) {
- if (targetingSource == null) {
- if (target.canTarget(possibleOpponentId, game)) {
- if (!target.contains(possibleOpponentId)) {
- target.add(possibleOpponentId, game);
- return true;
- }
- }
- } else if (target.canTarget(abilityControllerId, possibleOpponentId, targetingSource, game)) {
- if (!target.contains(possibleOpponentId)) {
- target.addTarget(possibleOpponentId, targetingSource, game);
- return true;
- }
- }
- }
- return false;
- }
-
- UUID sourceId = targetingSource != null ? targetingSource.getSourceId() : null;
- if (target.getOriginalTarget() instanceof TargetPlayer) {
- if (affectedOutcome.isGood()) {
- if (targetingSource == null) {
- // good
- if (target.canTarget(getId(), game) && !target.contains(getId())) {
- target.add(getId(), game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- }
- } else {
- // good
- if (target.canTarget(abilityControllerId, getId(), targetingSource, game) && !target.contains(getId())) {
- target.addTarget(getId(), targetingSource, game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game) && !target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- }
- }
- } else if (targetingSource == null) {
- // bad
- if (target.canTarget(randomOpponentId, game) && !target.contains(randomOpponentId)) {
- target.add(randomOpponentId, game);
- return true;
- }
- if (target.isRequired(sourceId, game)) {
- if (target.canTarget(getId(), game) && !target.contains(getId())) {
- target.add(getId(), game);
- return true;
- }
- }
- } else {
- // bad
- if (target.canTarget(abilityControllerId, randomOpponentId, targetingSource, game) && !target.contains(randomOpponentId)) {
- target.addTarget(randomOpponentId, targetingSource, game);
- return true;
- }
- if (required) {
- if (target.canTarget(abilityControllerId, getId(), targetingSource, game) && !target.contains(getId())) {
- target.addTarget(getId(), targetingSource, game);
- return true;
- }
- }
- }
- return false;
- }
-
- return false;
- }
-
- /**
- * Returns an opponent by random
- */
- @Deprecated // TODO: rework all usages and replace to all possible opponents instead single
- private UUID getRandomOpponent(Game game) {
- return RandomUtil.randomFromCollection(game.getOpponents(getId(), true));
- }
-
@Override
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
- Map useable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana);
- return useable.values().stream().findFirst().orElse(null);
+ Map usable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana);
+ return usable.values().stream()
+ .filter(a -> a.getTargets().canChoose(getId(), a, game))
+ .findFirst()
+ .orElse(null);
}
@Override
@@ -3262,12 +1333,4 @@ public class ComputerPlayer extends PlayerImpl {
// all human players converted to computer and analyse
this.human = false;
}
-
- public long getLastThinkTime() {
- return lastThinkTime;
- }
-
- public void setLastThinkTime(long lastThinkTime) {
- this.lastThinkTime = lastThinkTime;
- }
-}
+}
\ No newline at end of file
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java
new file mode 100644
index 00000000000..7f4f199b5ab
--- /dev/null
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsComparator.java
@@ -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 BY_NAME = (o1, o2) -> getName(o2).compareTo(getName(o1));
+ private final Comparator BY_ID = Comparator.comparing(MageItem::getId);
+
+ private final Comparator BY_ME = (o1, o2) -> Boolean.compare(
+ isMyItem(o2),
+ isMyItem(o1)
+ );
+
+ private final Comparator BY_BIGGER_SCORE = (o1, o2) -> Integer.compare(
+ getScoreFromBattlefield(o2),
+ getScoreFromBattlefield(o1)
+ );
+
+ private final Comparator BY_PLAYABLE = (o1, o2) -> Boolean.compare(
+ this.playableItems.containsObject(o2.getId()),
+ this.playableItems.containsObject(o1.getId())
+ );
+
+ private final Comparator 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 BY_TYPE_PLAYER = (o1, o2) -> Boolean.compare(
+ o2 instanceof Player,
+ o1 instanceof Player
+ );
+
+ private final Comparator 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 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 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 ANY_MOST_VALUABLE_FIRST = BY_TYPES
+ .thenComparing(BY_BIGGER_SCORE)
+ .thenComparing(BY_NAME)
+ .thenComparing(BY_ID);
+ public final Comparator 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 ANY_UNPLAYABLE_AND_USELESS = BY_LAND.reversed()
+ .thenComparing(BY_PLAYABLE.reversed())
+ .thenComparing(ANY_MOST_VALUABLE_FIRST);
+
+}
diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java
new file mode 100644
index 00000000000..c3d9f25ea22
--- /dev/null
+++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/PossibleTargetsSelector.java
@@ -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 me = new ArrayList<>();
+ List opponents = new ArrayList<>();
+ List 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 fromTargetsList) {
+ // collect new valid targets
+ List 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 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