diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java index 8e3770ce582..d4c51bdca46 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java @@ -27,20 +27,29 @@ */ package mage.client.deck.generator; -import mage.cards.Sets; -import mage.cards.decks.Deck; -import mage.client.MageFrame; -import mage.client.dialog.PreferencesDialog; -import mage.client.util.gui.ColorsChooser; -import mage.client.util.sets.ConstructedFormats; - -import javax.swing.*; -import java.awt.*; +import java.awt.Component; +import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import mage.cards.ConstructedFormats; +import mage.cards.Sets; +import mage.cards.decks.Deck; +import mage.cards.decks.generator.DeckOptions; +import mage.client.MageFrame; +import mage.client.dialog.PreferencesDialog; +import mage.client.util.gui.ColorsChooser; /** * @@ -195,6 +204,10 @@ public class DeckGeneratorDialog { return null; } + public DeckOptions getOptions() { + return new DeckOptions(getSelectedColors(), getSelectedFormat(), getDeckSize(), isSingleton(), useArtifacts(), useNonBasicLand()); + } + public String getSelectedFormat() { return (String) cbSets.getSelectedItem(); } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java index dee42eb34a3..ef9ec3d3f25 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java @@ -64,7 +64,7 @@ import mage.client.cards.CardGrid; import mage.client.cards.ICardGrid; import mage.client.constants.Constants.SortBy; import mage.client.deckeditor.table.TableModel; -import mage.client.util.sets.ConstructedFormats; +import mage.cards.ConstructedFormats; import mage.constants.CardType; import mage.filter.FilterCard; import mage.filter.predicate.Predicate; diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/CollectionViewerPanel.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/CollectionViewerPanel.java index e0a43c0818f..740641052f7 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/CollectionViewerPanel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/CollectionViewerPanel.java @@ -45,7 +45,7 @@ import mage.client.MageFrame; import mage.client.cards.BigCard; import mage.client.dialog.PreferencesDialog; import mage.client.plugins.impl.Plugins; -import mage.client.util.sets.ConstructedFormats; +import mage.cards.ConstructedFormats; import org.apache.log4j.Logger; diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java index 257f63e42c3..89cef1c89f4 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java @@ -43,7 +43,7 @@ import mage.client.util.audio.AudioManager; import mage.client.util.Command; import mage.client.util.Config; import mage.client.util.ImageHelper; -import mage.client.util.sets.ConstructedFormats; +import mage.cards.ConstructedFormats; import mage.components.ImagePanel; import mage.constants.Rarity; import mage.view.CardView; diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index 71709ce7daf..3e4308f6af2 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -38,8 +38,11 @@ import java.io.File; import java.io.IOException; import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; +import mage.cards.decks.Deck; +import mage.cards.decks.generator.DeckGenerator; import mage.client.MageFrame; -import mage.client.deck.generator.DeckGenerator; +import mage.client.deck.generator.DeckGeneratorDialog; +import mage.client.dialog.PreferencesDialog; import mage.client.util.Config; /** @@ -49,7 +52,7 @@ import mage.client.util.Config; public class NewPlayerPanel extends javax.swing.JPanel { private final JFileChooser fcSelectDeck; - + /** Creates new form NewPlayerPanel */ public NewPlayerPanel() { initComponents(); @@ -94,7 +97,14 @@ public class NewPlayerPanel extends javax.swing.JPanel { } protected void generateDeck() { - String path = DeckGenerator.generateDeck(); + + DeckGeneratorDialog genDialog = new DeckGeneratorDialog(); + Deck deck = DeckGenerator.generateDeck(genDialog.getOptions()); + String path = genDialog.saveDeck(deck); + // If the deck couldn't be generated or the user cancelled, repopulate the deck selection with its cached value + if (path == null) { + path = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_DECK_FILE, null); + } if (path != null) { this.txtPlayerDeck.setText(path); MageFrame.getPreferences().put("defaultDeckPath", path); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java index 4467e0ab2af..89a3a0a10fd 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPictures.java @@ -47,7 +47,7 @@ import javax.swing.JProgressBar; import mage.cards.repository.CardInfo; import mage.client.constants.Constants; import mage.client.dialog.PreferencesDialog; -import mage.client.util.sets.ConstructedFormats; +import mage.cards.ConstructedFormats; import mage.remote.Connection; import static mage.remote.Connection.ProxyType.HTTP; import static mage.remote.Connection.ProxyType.SOCKS; diff --git a/Mage.Client/src/main/java/mage/client/util/sets/ConstructedFormats.java b/Mage/src/mage/cards/ConstructedFormats.java similarity index 99% rename from Mage.Client/src/main/java/mage/client/util/sets/ConstructedFormats.java rename to Mage/src/mage/cards/ConstructedFormats.java index 81bc28210b6..95f9d3e1802 100644 --- a/Mage.Client/src/main/java/mage/client/util/sets/ConstructedFormats.java +++ b/Mage/src/mage/cards/ConstructedFormats.java @@ -1,4 +1,4 @@ -package mage.client.util.sets; +package mage.cards; import java.util.ArrayList; import java.util.Calendar; diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java b/Mage/src/mage/cards/decks/generator/DeckGenerator.java similarity index 92% rename from Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java rename to Mage/src/mage/cards/decks/generator/DeckGenerator.java index 2982c97a126..f0b7f12607b 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java +++ b/Mage/src/mage/cards/decks/generator/DeckGenerator.java @@ -1,404 +1,407 @@ -/* - * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and should not be interpreted as representing official policies, either expressed - * or implied, of BetaSteward_at_googlemail.com. - */ -package mage.client.deck.generator; - -import java.util.*; - -import mage.cards.Card; -import mage.cards.decks.Deck; -import mage.cards.repository.CardCriteria; -import mage.cards.repository.CardInfo; -import mage.cards.repository.CardRepository; -import mage.cards.repository.ExpansionInfo; -import mage.cards.repository.ExpansionRepository; -import mage.client.dialog.PreferencesDialog; -import mage.client.util.sets.ConstructedFormats; -import mage.constants.CardType; -import mage.constants.ColoredManaSymbol; -import mage.constants.Rarity; - - -/** - * Generates random card pool and builds a deck. - * - * @author nantuko - * @author Simown - */ -public class DeckGenerator { - - private static final int MAX_TRIES = 8196; - private static DeckGeneratorDialog genDialog; - private static DeckGeneratorPool genPool; - - /** - * Builds a deck out of the selected block/set/format. - * @return a path to the generated deck. - */ - public static String generateDeck() { - - genDialog = new DeckGeneratorDialog(); - if (genDialog.getSelectedColors() != null) { - Deck deck = buildDeck(); - return genDialog.saveDeck(deck); - } - // If the deck couldn't be generated or the user cancelled, repopulate the deck selection with its cached value - return PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_DECK_FILE, null); - } - - - protected static Deck buildDeck() { - - String selectedColors = genDialog.getSelectedColors(); - List allowedColors = new ArrayList<>(); - selectedColors = selectedColors != null ? selectedColors.toUpperCase() : getRandomColors("X"); - String format = genDialog.getSelectedFormat(); - - List setsToUse = ConstructedFormats.getSetsByFormat(format); - if (setsToUse.isEmpty()) { - // Default to using all sets - setsToUse = ExpansionRepository.instance.getSetCodes(); - } - - int deckSize = genDialog.getDeckSize(); - - if (selectedColors.contains("X")) { - selectedColors = getRandomColors(selectedColors); - } - - for (int i = 0; i < selectedColors.length(); i++) { - char c = selectedColors.charAt(i); - allowedColors.add(ColoredManaSymbol.lookup(c)); - } - - return generateDeck(deckSize, allowedColors, setsToUse); - } - - /** - * If the user has selected random colors, pick them randomly for the user. - * @param selectedColors a string of the colors selected. - * @return a String representation of the new colors chosen. - */ - private static String getRandomColors(String selectedColors) { - - Random random = new Random(); - List availableColors = new ArrayList<>(); - for (ColoredManaSymbol cms : ColoredManaSymbol.values()) { - availableColors.add(cms.toString().charAt(0)); - } - - StringBuilder generatedColors = new StringBuilder(); - int randomColors = 0; - for (int i = 0; i < selectedColors.length(); i++) { - char currentColor = selectedColors.charAt(i); - if (currentColor != 'X') { - generatedColors.append(currentColor); - availableColors.remove(new Character(currentColor)); - } else { - randomColors++; - } - } - for (int i = 0; i < randomColors && !availableColors.isEmpty(); i++) { - int index = random.nextInt(availableColors.size()); - generatedColors.append(availableColors.remove(index)); - } - return generatedColors.toString(); - } - - /** - * Generates all the cards to use in the deck. - * Adds creatures, non-creatures, lands (including non-basic). - * Fixes the deck, adjusting for size and color of the cards retrieved. - * @param deckSize how big the deck is to generate. - * @param allowedColors which colors are allowed in the deck. - * @param setsToUse which sets to use to retrieve cards for this deck. - * @return the final deck to use. - */ - private static Deck generateDeck(int deckSize, List allowedColors, List setsToUse) { - genPool = new DeckGeneratorPool(deckSize, allowedColors, genDialog.isSingleton()); - - final String[] sets = setsToUse.toArray(new String[setsToUse.size()]); - - // Creatures - final CardCriteria creatureCriteria = new CardCriteria(); - creatureCriteria.setCodes(sets); - creatureCriteria.notTypes(CardType.LAND); - creatureCriteria.types(CardType.CREATURE); - if (!(genDialog.useArtifacts())) - creatureCriteria.notTypes(CardType.ARTIFACT); - - // Non-creatures (sorcery, instant, enchantment, artifact etc.) - final CardCriteria nonCreatureCriteria = new CardCriteria(); - nonCreatureCriteria.setCodes(sets); - nonCreatureCriteria.notTypes(CardType.LAND); - nonCreatureCriteria.notTypes(CardType.CREATURE); - if (!(genDialog.useArtifacts())) - nonCreatureCriteria.notTypes(CardType.ARTIFACT); - - // Non-basic land - final CardCriteria nonBasicLandCriteria = new CardCriteria(); - nonBasicLandCriteria.setCodes(sets); - nonBasicLandCriteria.types(CardType.LAND); - nonBasicLandCriteria.notSupertypes("Basic"); - - // Generate basic land cards - Map> basicLands = generateBasicLands(setsToUse); - - generateSpells(creatureCriteria, genPool.getCreatureCount()); - generateSpells(nonCreatureCriteria, genPool.getNonCreatureCount()); - generateLands(nonBasicLandCriteria, genPool.getLandCount(), basicLands); - - // Reconstructs the final deck and adjusts for Math rounding and/or missing cards - return genPool.getDeck(); - } - - - /** - * Generates all spells for the deck. - * Each card is retrieved from the database and checked against the converted mana cost (CMC) needed for the current card pool. - * If a card's CMC matches the CMC range required by the pool, it is added to the deck. - * This ensures that the majority of cards fit a fixed mana curve for the deck, and it is playable. - * Creatures and non-creatures are retrieved separately to ensure the deck contains a reasonable mix of both. - * @param criteria the criteria to search for in the database. - * @param spellCount the number of spells that match the criteria needed in the deck. - */ - private static void generateSpells(CardCriteria criteria, int spellCount) { - List cardPool = CardRepository.instance.findCards(criteria); - int retrievedCount = cardPool.size(); - List deckCMCs = genPool.getCMCsForSpellCount(spellCount); - Random random = new Random(); - int count = 0; - int reservesAdded = 0; - boolean added; - if (retrievedCount > 0 && retrievedCount >= spellCount) { - int tries = 0; - while (count < spellCount) { - Card card = cardPool.get(random.nextInt(retrievedCount)).getMockCard(); - if (genPool.isValidSpellCard(card)) { - int cardCMC = card.getManaCost().convertedManaCost(); - for (DeckGeneratorCMC deckCMC : deckCMCs) { - if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) { - int currentAmount = deckCMC.getAmount(); - if (currentAmount > 0) { - deckCMC.setAmount(currentAmount - 1); - genPool.addCard(card.copy()); - count++; - } - } else { - if (reservesAdded < (genPool.getDeckSize() / 2)) { - added = genPool.tryAddReserve(card, cardCMC); - if(added) - reservesAdded++; - } - } - } - } - tries++; - if (tries > MAX_TRIES) { - // Break here, we'll fill in random missing ones later - break; - } - } - } else { - throw new IllegalStateException("Not enough cards to generate deck."); - } - - } - - /** - * Generates all the lands for the deck. - * Generates non-basic if selected by the user and if the deck isn't monocolored. - * Will fetch non-basic lands if required and then fill up the remaining space with basic lands. - * Basic lands are adjusted according to the mana symbols seen in the cards used in this deck. - * Usually the lands will be well balanced relative to the color of cards. - * @param criteria the criteria of the lands to search for in the database. - * @param landsCount the amount of lands required for this deck. - * @param basicLands information about the basic lands from the sets used. - */ - private static void generateLands(CardCriteria criteria, int landsCount, Map> basicLands) { - - int tries = 0; - int countNonBasic = 0; - // Store the nonbasic lands (if any) we'll add - List deckLands = new ArrayList<>(); - - // Calculates the percentage of colored mana symbols over all spells in the deck - Map percentage = genPool.calculateSpellColorPercentages(); - - // Only dual/tri color lands are generated for now, and not non-basic lands that only produce colorless mana. - if (!genPool.isMonoColoredDeck() && genDialog.useNonBasicLand()) { - List landCards = genPool.filterLands(CardRepository.instance.findCards(criteria)); - int allCount = landCards.size(); - Random random = new Random(); - if (allCount > 0) { - while (countNonBasic < landsCount / 2) { - Card card = landCards.get(random.nextInt(allCount)); - if (genPool.isValidLandCard(card)) { - Card addedCard = card.copy(); - deckLands.add(addedCard); - genPool.addCard(addedCard); - countNonBasic++; - } - tries++; - // to avoid infinite loop - if (tries > MAX_TRIES) { - // Not a problem, just use what we have - break; - } - } - } - } - // Calculate the amount of colored mana already can be produced by the non-basic lands - Map count = genPool.countManaProduced(deckLands); - // Fill up the rest of the land quota with basic lands adjusted to fit the deck's mana costs - addBasicLands(landsCount - countNonBasic, percentage, count, basicLands); - } - - /** - * Returns a map of colored mana symbol to basic land cards of that color. - * @param setsToUse which sets to retrieve basic lands from. - * @return a map of color to basic lands. - */ - private static Map> generateBasicLands(List setsToUse) { - - List landSets = new LinkedList<>(); - - // decide from which sets basic lands are taken from - for (String setCode :setsToUse) { - ExpansionInfo expansionInfo = ExpansionRepository.instance.getSetByCode(setCode); - if (expansionInfo.hasBasicLands()) { - landSets.add(expansionInfo.getCode()); - } - } - - // if sets have no basic land, take land from block - if (landSets.isEmpty()) { - for (String setCode :setsToUse) { - ExpansionInfo expansionInfo = ExpansionRepository.instance.getSetByCode(setCode); - List blockSets = ExpansionRepository.instance.getSetsFromBlock(expansionInfo.getBlockName()); - for (ExpansionInfo blockSet: blockSets) { - if (blockSet.hasBasicLands()) { - landSets.add(blockSet.getCode()); - } - } - } - } - // if still no set with lands found, take one by random - if (landSets.isEmpty()) { - // if sets have no basic lands and also it has no parent or parent has no lands get last set with lands - // select a set with basic lands by random - Random generator = new Random(); - List basicLandSets = ExpansionRepository.instance.getSetsWithBasicLandsByReleaseDate(); - if (basicLandSets.size() > 0) { - landSets.add(basicLandSets.get(generator.nextInt(basicLandSets.size())).getCode()); - } - } - - if (landSets.isEmpty()) { - throw new IllegalArgumentException("No set with basic land was found"); - } - - CardCriteria criteria = new CardCriteria(); - if (!landSets.isEmpty()) { - criteria.setCodes(landSets.toArray(new String[landSets.size()])); - } - - Map> basicLandMap = new HashMap<>(); - - for(ColoredManaSymbol c: ColoredManaSymbol.values()) { - String landName = DeckGeneratorPool.getBasicLandName(c.toString()); - criteria.rarities(Rarity.LAND).name(landName); - List cards = CardRepository.instance.findCards(criteria); - basicLandMap.put(landName, cards); - } - return basicLandMap; - } - - /** - * Once any non-basic lands are added, add basic lands until the deck is filled. - * @param landsNeeded how many remaining lands are needed. - * @param percentage the percentage needed for each color in the final deck. - * @param count how many of each color can be produced by non-basic lands. - * @param basicLands list of information about basic lands from the database. - */ - private static void addBasicLands(int landsNeeded, Map percentage, Map count, Map> basicLands) { - int colorTotal = 0; - ColoredManaSymbol colorToAdd = null; - - // Add up the totals for all colors, to keep track of the percentage a color is. - for (Map.Entry c : count.entrySet()) { - colorTotal += c.getValue(); - } - - // Keep adding basic lands until we fill the deck - while (landsNeeded > 0) { - - double minPercentage = Integer.MIN_VALUE; - - for (ColoredManaSymbol color : ColoredManaSymbol.values()) { - // What percentage of this color is requested - double neededPercentage = percentage.get(color.toString()); - // If there is a 0% need for basic lands of this color, skip it - if (neededPercentage <= 0) { - continue; - } - int currentCount = count.get(color.toString()); - double thisPercentage = 0.0; - // Calculate the percentage of lands so far that produce this color - if (currentCount > 0) - thisPercentage = (currentCount / (double) colorTotal) * 100.0; - // Check if the color is the most "needed" (highest percentage) we have seen so far - if (neededPercentage - thisPercentage > minPercentage) { - // Put this color land forward to be added - colorToAdd = color; - minPercentage = (neededPercentage - thisPercentage); - } - } - if(colorToAdd != null) { - genPool.addCard(getBasicLand(colorToAdd, basicLands)); - count.put(colorToAdd.toString(), count.get(colorToAdd.toString()) + 1); - colorTotal++; - landsNeeded--; - } - } - } - - /** - * Return a random basic land of the chosen color. - * @param color the color the basic land should produce. - * @param basicLands list of information about basic lands from the database. - * @return a single basic land that produces the color needed. - */ - private static Card getBasicLand(ColoredManaSymbol color, Map> basicLands) { - Random random = new Random(); - String landName = DeckGeneratorPool.getBasicLandName(color.toString()); - List basicLandsInfo = basicLands.get(landName); - return basicLandsInfo.get(random.nextInt(basicLandsInfo.size() - 1)).getMockCard().copy(); - } - - -} +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.decks.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import mage.cards.Card; +import mage.cards.ConstructedFormats; +import mage.cards.decks.Deck; +import mage.cards.repository.CardCriteria; +import mage.cards.repository.CardInfo; +import mage.cards.repository.CardRepository; +import mage.cards.repository.ExpansionInfo; +import mage.cards.repository.ExpansionRepository; +import mage.constants.CardType; +import mage.constants.ColoredManaSymbol; +import mage.constants.Rarity; + + +/** + * Generates random card pool and builds a deck. + * + * @author nantuko + * @author Simown + */ +public class DeckGenerator { + + private static final int MAX_TRIES = 8196; + private static DeckOptions options; + private static DeckGeneratorPool genPool; + + /** + * Builds a deck out of the selected block/set/format. + * @param deckOptions + * @return the generated deck. + */ + public static Deck generateDeck(DeckOptions deckOptions) { + + options = deckOptions; + if (options.getSelectedColors()!= null) { + Deck deck = buildDeck(); + return deck; + } + return null; + } + + + protected static Deck buildDeck() { + + String selectedColors = options.getSelectedColors(); + List allowedColors = new ArrayList<>(); + selectedColors = selectedColors != null ? selectedColors.toUpperCase() : getRandomColors("X"); + String format = options.getSelectedFormat(); + + List setsToUse = ConstructedFormats.getSetsByFormat(format); + if (setsToUse.isEmpty()) { + // Default to using all sets + setsToUse = ExpansionRepository.instance.getSetCodes(); + } + + int deckSize = options.getDeckSize(); + + if (selectedColors.contains("X")) { + selectedColors = getRandomColors(selectedColors); + } + + for (int i = 0; i < selectedColors.length(); i++) { + char c = selectedColors.charAt(i); + allowedColors.add(ColoredManaSymbol.lookup(c)); + } + + return generateDeck(deckSize, allowedColors, setsToUse); + } + + /** + * If the user has selected random colors, pick them randomly for the user. + * @param selectedColors a string of the colors selected. + * @return a String representation of the new colors chosen. + */ + private static String getRandomColors(String selectedColors) { + + Random random = new Random(); + List availableColors = new ArrayList<>(); + for (ColoredManaSymbol cms : ColoredManaSymbol.values()) { + availableColors.add(cms.toString().charAt(0)); + } + + StringBuilder generatedColors = new StringBuilder(); + int randomColors = 0; + for (int i = 0; i < selectedColors.length(); i++) { + char currentColor = selectedColors.charAt(i); + if (currentColor != 'X') { + generatedColors.append(currentColor); + availableColors.remove(new Character(currentColor)); + } else { + randomColors++; + } + } + for (int i = 0; i < randomColors && !availableColors.isEmpty(); i++) { + int index = random.nextInt(availableColors.size()); + generatedColors.append(availableColors.remove(index)); + } + return generatedColors.toString(); + } + + /** + * Generates all the cards to use in the deck. + * Adds creatures, non-creatures, lands (including non-basic). + * Fixes the deck, adjusting for size and color of the cards retrieved. + * @param deckSize how big the deck is to generate. + * @param allowedColors which colors are allowed in the deck. + * @param setsToUse which sets to use to retrieve cards for this deck. + * @return the final deck to use. + */ + private static Deck generateDeck(int deckSize, List allowedColors, List setsToUse) { + genPool = new DeckGeneratorPool(deckSize, allowedColors, options.isSingleton()); + + final String[] sets = setsToUse.toArray(new String[setsToUse.size()]); + + // Creatures + final CardCriteria creatureCriteria = new CardCriteria(); + creatureCriteria.setCodes(sets); + creatureCriteria.notTypes(CardType.LAND); + creatureCriteria.types(CardType.CREATURE); + if (!(options.useArtifacts())) + creatureCriteria.notTypes(CardType.ARTIFACT); + + // Non-creatures (sorcery, instant, enchantment, artifact etc.) + final CardCriteria nonCreatureCriteria = new CardCriteria(); + nonCreatureCriteria.setCodes(sets); + nonCreatureCriteria.notTypes(CardType.LAND); + nonCreatureCriteria.notTypes(CardType.CREATURE); + if (!(options.useArtifacts())) + nonCreatureCriteria.notTypes(CardType.ARTIFACT); + + // Non-basic land + final CardCriteria nonBasicLandCriteria = new CardCriteria(); + nonBasicLandCriteria.setCodes(sets); + nonBasicLandCriteria.types(CardType.LAND); + nonBasicLandCriteria.notSupertypes("Basic"); + + // Generate basic land cards + Map> basicLands = generateBasicLands(setsToUse); + + generateSpells(creatureCriteria, genPool.getCreatureCount()); + generateSpells(nonCreatureCriteria, genPool.getNonCreatureCount()); + generateLands(nonBasicLandCriteria, genPool.getLandCount(), basicLands); + + // Reconstructs the final deck and adjusts for Math rounding and/or missing cards + return genPool.getDeck(); + } + + + /** + * Generates all spells for the deck. + * Each card is retrieved from the database and checked against the converted mana cost (CMC) needed for the current card pool. + * If a card's CMC matches the CMC range required by the pool, it is added to the deck. + * This ensures that the majority of cards fit a fixed mana curve for the deck, and it is playable. + * Creatures and non-creatures are retrieved separately to ensure the deck contains a reasonable mix of both. + * @param criteria the criteria to search for in the database. + * @param spellCount the number of spells that match the criteria needed in the deck. + */ + private static void generateSpells(CardCriteria criteria, int spellCount) { + List cardPool = CardRepository.instance.findCards(criteria); + int retrievedCount = cardPool.size(); + List deckCMCs = genPool.getCMCsForSpellCount(spellCount); + Random random = new Random(); + int count = 0; + int reservesAdded = 0; + boolean added; + if (retrievedCount > 0 && retrievedCount >= spellCount) { + int tries = 0; + while (count < spellCount) { + Card card = cardPool.get(random.nextInt(retrievedCount)).getMockCard(); + if (genPool.isValidSpellCard(card)) { + int cardCMC = card.getManaCost().convertedManaCost(); + for (DeckGeneratorCMC deckCMC : deckCMCs) { + if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) { + int currentAmount = deckCMC.getAmount(); + if (currentAmount > 0) { + deckCMC.setAmount(currentAmount - 1); + genPool.addCard(card.copy()); + count++; + } + } else { + if (reservesAdded < (genPool.getDeckSize() / 2)) { + added = genPool.tryAddReserve(card, cardCMC); + if(added) + reservesAdded++; + } + } + } + } + tries++; + if (tries > MAX_TRIES) { + // Break here, we'll fill in random missing ones later + break; + } + } + } else { + throw new IllegalStateException("Not enough cards to generate deck."); + } + + } + + /** + * Generates all the lands for the deck. + * Generates non-basic if selected by the user and if the deck isn't monocolored. + * Will fetch non-basic lands if required and then fill up the remaining space with basic lands. + * Basic lands are adjusted according to the mana symbols seen in the cards used in this deck. + * Usually the lands will be well balanced relative to the color of cards. + * @param criteria the criteria of the lands to search for in the database. + * @param landsCount the amount of lands required for this deck. + * @param basicLands information about the basic lands from the sets used. + */ + private static void generateLands(CardCriteria criteria, int landsCount, Map> basicLands) { + + int tries = 0; + int countNonBasic = 0; + // Store the nonbasic lands (if any) we'll add + List deckLands = new ArrayList<>(); + + // Calculates the percentage of colored mana symbols over all spells in the deck + Map percentage = genPool.calculateSpellColorPercentages(); + + // Only dual/tri color lands are generated for now, and not non-basic lands that only produce colorless mana. + if (!genPool.isMonoColoredDeck() && options.useNonBasicLand()) { + List landCards = genPool.filterLands(CardRepository.instance.findCards(criteria)); + int allCount = landCards.size(); + Random random = new Random(); + if (allCount > 0) { + while (countNonBasic < landsCount / 2) { + Card card = landCards.get(random.nextInt(allCount)); + if (genPool.isValidLandCard(card)) { + Card addedCard = card.copy(); + deckLands.add(addedCard); + genPool.addCard(addedCard); + countNonBasic++; + } + tries++; + // to avoid infinite loop + if (tries > MAX_TRIES) { + // Not a problem, just use what we have + break; + } + } + } + } + // Calculate the amount of colored mana already can be produced by the non-basic lands + Map count = genPool.countManaProduced(deckLands); + // Fill up the rest of the land quota with basic lands adjusted to fit the deck's mana costs + addBasicLands(landsCount - countNonBasic, percentage, count, basicLands); + } + + /** + * Returns a map of colored mana symbol to basic land cards of that color. + * @param setsToUse which sets to retrieve basic lands from. + * @return a map of color to basic lands. + */ + private static Map> generateBasicLands(List setsToUse) { + + List landSets = new LinkedList<>(); + + // decide from which sets basic lands are taken from + for (String setCode :setsToUse) { + ExpansionInfo expansionInfo = ExpansionRepository.instance.getSetByCode(setCode); + if (expansionInfo.hasBasicLands()) { + landSets.add(expansionInfo.getCode()); + } + } + + // if sets have no basic land, take land from block + if (landSets.isEmpty()) { + for (String setCode :setsToUse) { + ExpansionInfo expansionInfo = ExpansionRepository.instance.getSetByCode(setCode); + List blockSets = ExpansionRepository.instance.getSetsFromBlock(expansionInfo.getBlockName()); + for (ExpansionInfo blockSet: blockSets) { + if (blockSet.hasBasicLands()) { + landSets.add(blockSet.getCode()); + } + } + } + } + // if still no set with lands found, take one by random + if (landSets.isEmpty()) { + // if sets have no basic lands and also it has no parent or parent has no lands get last set with lands + // select a set with basic lands by random + Random generator = new Random(); + List basicLandSets = ExpansionRepository.instance.getSetsWithBasicLandsByReleaseDate(); + if (basicLandSets.size() > 0) { + landSets.add(basicLandSets.get(generator.nextInt(basicLandSets.size())).getCode()); + } + } + + if (landSets.isEmpty()) { + throw new IllegalArgumentException("No set with basic land was found"); + } + + CardCriteria criteria = new CardCriteria(); + if (!landSets.isEmpty()) { + criteria.setCodes(landSets.toArray(new String[landSets.size()])); + } + + Map> basicLandMap = new HashMap<>(); + + for(ColoredManaSymbol c: ColoredManaSymbol.values()) { + String landName = DeckGeneratorPool.getBasicLandName(c.toString()); + criteria.rarities(Rarity.LAND).name(landName); + List cards = CardRepository.instance.findCards(criteria); + basicLandMap.put(landName, cards); + } + return basicLandMap; + } + + /** + * Once any non-basic lands are added, add basic lands until the deck is filled. + * @param landsNeeded how many remaining lands are needed. + * @param percentage the percentage needed for each color in the final deck. + * @param count how many of each color can be produced by non-basic lands. + * @param basicLands list of information about basic lands from the database. + */ + private static void addBasicLands(int landsNeeded, Map percentage, Map count, Map> basicLands) { + int colorTotal = 0; + ColoredManaSymbol colorToAdd = null; + + // Add up the totals for all colors, to keep track of the percentage a color is. + for (Map.Entry c : count.entrySet()) { + colorTotal += c.getValue(); + } + + // Keep adding basic lands until we fill the deck + while (landsNeeded > 0) { + + double minPercentage = Integer.MIN_VALUE; + + for (ColoredManaSymbol color : ColoredManaSymbol.values()) { + // What percentage of this color is requested + double neededPercentage = percentage.get(color.toString()); + // If there is a 0% need for basic lands of this color, skip it + if (neededPercentage <= 0) { + continue; + } + int currentCount = count.get(color.toString()); + double thisPercentage = 0.0; + // Calculate the percentage of lands so far that produce this color + if (currentCount > 0) + thisPercentage = (currentCount / (double) colorTotal) * 100.0; + // Check if the color is the most "needed" (highest percentage) we have seen so far + if (neededPercentage - thisPercentage > minPercentage) { + // Put this color land forward to be added + colorToAdd = color; + minPercentage = (neededPercentage - thisPercentage); + } + } + if(colorToAdd != null) { + genPool.addCard(getBasicLand(colorToAdd, basicLands)); + count.put(colorToAdd.toString(), count.get(colorToAdd.toString()) + 1); + colorTotal++; + landsNeeded--; + } + } + } + + /** + * Return a random basic land of the chosen color. + * @param color the color the basic land should produce. + * @param basicLands list of information about basic lands from the database. + * @return a single basic land that produces the color needed. + */ + private static Card getBasicLand(ColoredManaSymbol color, Map> basicLands) { + Random random = new Random(); + String landName = DeckGeneratorPool.getBasicLandName(color.toString()); + List basicLandsInfo = basicLands.get(landName); + return basicLandsInfo.get(random.nextInt(basicLandsInfo.size() - 1)).getMockCard().copy(); + } + + +} diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java b/Mage/src/mage/cards/decks/generator/DeckGeneratorCMC.java similarity index 98% rename from Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java rename to Mage/src/mage/cards/decks/generator/DeckGeneratorCMC.java index 06ceac14c42..55a9373efbe 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java +++ b/Mage/src/mage/cards/decks/generator/DeckGeneratorCMC.java @@ -25,7 +25,7 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ -package mage.client.deck.generator; +package mage.cards.decks.generator; /** * Stores a range of converted mana costs (CMC) for use in deck generation. diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java b/Mage/src/mage/cards/decks/generator/DeckGeneratorPool.java similarity index 99% rename from Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java rename to Mage/src/mage/cards/decks/generator/DeckGeneratorPool.java index 3e92fef086e..66e8f42567e 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java +++ b/Mage/src/mage/cards/decks/generator/DeckGeneratorPool.java @@ -25,7 +25,7 @@ * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ -package mage.client.deck.generator; +package mage.cards.decks.generator; import mage.abilities.Ability; import mage.cards.Card; @@ -34,6 +34,7 @@ import mage.cards.repository.CardInfo; import mage.constants.ColoredManaSymbol; import java.util.*; +import mage.cards.decks.Deck; /** * diff --git a/Mage/src/mage/cards/decks/generator/DeckOptions.java b/Mage/src/mage/cards/decks/generator/DeckOptions.java new file mode 100644 index 00000000000..7a4e3d5e265 --- /dev/null +++ b/Mage/src/mage/cards/decks/generator/DeckOptions.java @@ -0,0 +1,49 @@ +package mage.cards.decks.generator; + +/** + * + * @author BetaSteward + */ +public class DeckOptions { + + private String selectedColors; + private String selectedFormat; + private int deckSize; + private boolean singleton; + private boolean useArtifacts; + private boolean useNonBasicLand; + + public DeckOptions(String selectedColors, String selectedFormat, int deckSize, boolean singleton, boolean useArtifacts, boolean useNonBasicLand) { + this.selectedColors = selectedColors; + this.selectedFormat = selectedFormat; + this.deckSize = deckSize; + this.singleton = singleton; + this.useArtifacts = useArtifacts; + this.useNonBasicLand = useNonBasicLand; + } + + public String getSelectedColors() { + return selectedColors; + } + + public String getSelectedFormat() { + return selectedFormat; + } + + public int getDeckSize() { + return deckSize; + } + + public boolean isSingleton() { + return singleton; + } + + public boolean useArtifacts() { + return useArtifacts; + } + + public boolean useNonBasicLand() { + return useNonBasicLand; + } + +}