diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java index 55fba392e7d..bda82863dcc 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java @@ -1,200 +1,91 @@ +/* + * 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.awt.Component; -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import mage.Mana; +import java.util.*; + import mage.cards.Card; -import mage.cards.Sets; 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.MageFrame; import mage.client.dialog.PreferencesDialog; -import mage.client.util.gui.ColorsChooser; import mage.client.util.sets.ConstructedFormats; import mage.constants.CardType; import mage.constants.ColoredManaSymbol; import mage.constants.Rarity; -import mage.interfaces.rate.RateCallback; -import mage.utils.DeckBuilder; /** * Generates random card pool and builds a deck. * * @author nantuko + * @author Simown */ public class DeckGenerator { - private static JDialog dlg; - private static String selectedColors; - private static JComboBox cbSets; - private static JComboBox cbDeckSize; - - private static final int SPELL_CARD_POOL_SIZE = 180; - - private static final int DECK_LANDS = 17; - private static final int MAX_NON_BASIC_SOURCE = DECK_LANDS / 2; - - private static final int MAX_TRIES = 4096; - - private static Deck deck = new Deck(); - private static final int ADDITIONAL_CARDS_FOR_3_COLOR_DECKS = 20; + private static final int MAX_TRIES = 8196; + private static DeckGeneratorDialog genDialog; + private static DeckGeneratorPool genPool; /** - * Opens color chooser dialog. Generates deck. - * Saves generated deck and use it as selected deck to play. - * - * @return + * Builds a deck out of the selected block/set/format. + * @return a path to the generated deck. */ public static String generateDeck() { - JPanel p0 = new JPanel(); - p0.setLayout(new BoxLayout(p0, BoxLayout.Y_AXIS)); - JLabel text = new JLabel("Choose color for your deck: "); - text.setAlignmentX(Component.CENTER_ALIGNMENT); - p0.add(text); - - p0.add(Box.createVerticalStrut(5)); - String chosen = MageFrame.getPreferences().get("genDeckColor", "u"); - final ColorsChooser colorsChooser = new ColorsChooser(chosen); - p0.add(colorsChooser); - - p0.add(Box.createVerticalStrut(5)); - JLabel text2 = new JLabel("(X - random color)"); - text2.setAlignmentX(Component.CENTER_ALIGNMENT); - p0.add(text2); - - p0.add(Box.createVerticalStrut(5)); - JPanel jPanel = new JPanel(); - JLabel text3 = new JLabel("Choose sets:"); - cbSets = new JComboBox(ConstructedFormats.getTypes()); - cbSets.setSelectedIndex(0); - cbSets.setPreferredSize(new Dimension(300, 25)); - cbSets.setMaximumSize(new Dimension(300, 25)); - cbSets.setAlignmentX(Component.LEFT_ALIGNMENT); - jPanel.add(text3); - jPanel.add(cbSets); - p0.add(jPanel); - - String prefSet = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SET, null); - if (prefSet != null) { - cbSets.setSelectedItem(prefSet); + genDialog = new DeckGeneratorDialog(); + if (genDialog.getSelectedColors() != null) { + Deck deck = buildDeck(); + return genDialog.saveDeck(deck); } - - p0.add(Box.createVerticalStrut(5)); - JPanel jPanel2 = new JPanel(); - JLabel textDeckSize = new JLabel("Deck size:"); - cbDeckSize = new JComboBox(new String[]{"40","60"}); - cbDeckSize.setSelectedIndex(0); - cbDeckSize.setPreferredSize(new Dimension(300, 25)); - cbDeckSize.setMaximumSize(new Dimension(300, 25)); - cbDeckSize.setAlignmentX(Component.LEFT_ALIGNMENT); - jPanel2.add(textDeckSize); - jPanel2.add(cbDeckSize); - p0.add(jPanel2); - - String prefSize = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_DECK_SIZE, "60"); - if (prefSet != null) { - cbDeckSize.setSelectedItem(prefSize); - } - - final JButton btnGenerate = new JButton("Ok"); - btnGenerate.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - btnGenerate.setEnabled(false); - colorsChooser.setEnabled(false); - selectedColors = (String) colorsChooser.getSelectedItem(); - dlg.setVisible(false); - MageFrame.getPreferences().put("genDeckColor", selectedColors); - } - }); - final JButton btnCancel = new JButton("Cancel"); - btnCancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - dlg.setVisible(false); - selectedColors = null; - } - }); - Object[] options = {btnGenerate, btnCancel}; - JOptionPane optionPane = new JOptionPane(p0, JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[1]); - dlg = optionPane.createDialog("Generating deck"); - dlg.setVisible(true); - dlg.dispose(); - - if (selectedColors != null) { - // save values to prefs - PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_DECK_SIZE, cbDeckSize.getSelectedItem().toString()); - PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SET, cbSets.getSelectedItem().toString()); - - // build deck - buildDeck(); - try { - File tmp = File.createTempFile("tempDeck" + UUID.randomUUID().toString(), ".dck"); - tmp.createNewFile(); - deck.setName("Generated-Deck-" + UUID.randomUUID()); - Sets.saveDeck(tmp.getAbsolutePath(), deck.getDeckCardLists()); - deck = null; - //JOptionPane.showMessageDialog(null, "Deck has been generated."); - DeckGenerator.cleanUp(btnGenerate, btnCancel); - return tmp.getAbsolutePath(); - } catch (Exception e) { - JOptionPane.showMessageDialog(null, "Couldn't generate deck. Try again."); - } - } - DeckGenerator.cleanUp(btnGenerate, btnCancel); - return selectedColors; + // 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); } - private static void cleanUp(JButton btnGenerate, JButton btnCancel) { - for (ActionListener al: btnGenerate.getActionListeners()) { - btnGenerate.removeActionListener(al); - } - for (ActionListener al: btnCancel.getActionListeners()) { - btnCancel.removeActionListener(al); - } - deck = null; - } - - /** - * Generates card pool - */ - protected static void buildDeck() { - List allowedColors = new ArrayList(); + + protected static Deck buildDeck() { + + String selectedColors = genDialog.getSelectedColors(); + List allowedColors = new ArrayList<>(); selectedColors = selectedColors != null ? selectedColors.toUpperCase() : getRandomColors("X"); + String format = genDialog.getSelectedFormat(); - String format = (String) cbSets.getSelectedItem(); List setsToUse = ConstructedFormats.getSetsByFormat(format); if (setsToUse.isEmpty()) { - // use all + // Default to using all sets setsToUse = ExpansionRepository.instance.getSetCodes(); } - int deckSize = Integer.parseInt(cbDeckSize.getSelectedItem().toString()); - if (deckSize < 40) { - deckSize = 40; - } + int deckSize = genDialog.getDeckSize(); + if (selectedColors.contains("X")) { selectedColors = getRandomColors(selectedColors); } @@ -204,43 +95,26 @@ public class DeckGenerator { allowedColors.add(ColoredManaSymbol.lookup(c)); } - int cardPoolSize = SPELL_CARD_POOL_SIZE; - if (selectedColors.length() > 2) { - cardPoolSize += ADDITIONAL_CARDS_FOR_3_COLOR_DECKS; - } - List spellCardPool = generateSpellCardPool(cardPoolSize, allowedColors, setsToUse); - List landCardPool = generateNonBasicLandCardPool(MAX_NON_BASIC_SOURCE, allowedColors, setsToUse); - - // System.out.println("deck generator card pool: spells=" + spellCardPool.size() + ", lands=" + landCardPool.size()); - - final List setsToUseFinal = setsToUse; - - deck = DeckBuilder.buildDeck(spellCardPool, allowedColors, setsToUseFinal, landCardPool, deckSize, new RateCallback() { - @Override - public int rateCard(Card card) { - return 6; - } - - @Override - public Card getBestBasicLand(ColoredManaSymbol color, List setsToUse) { - return DeckGenerator.getBestBasicLand(color, setsToUseFinal); - } - }); + return generateDeck(deckSize, allowedColors, setsToUse); } - private static String getRandomColors(String _selectedColors) { + /** + * 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(); - availableColors.add('R'); - availableColors.add('G'); - availableColors.add('B'); - availableColors.add('U'); - availableColors.add('W'); + 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); + for (int i = 0; i < selectedColors.length(); i++) { + char currentColor = selectedColors.charAt(i); if (currentColor != 'X') { generatedColors.append(currentColor); availableColors.remove(new Character(currentColor)); @@ -248,165 +122,168 @@ public class DeckGenerator { 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 card pool of cardsCount cards that have manacost of allowed colors. - * - * @param cardsCount - * @param allowedColors - * @return + * 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 List generateSpellCardPool(int cardsCount, List allowedColors, List setsToUse) { - List spellCardPool = new ArrayList(); + private static Deck generateDeck(int deckSize, List allowedColors, List setsToUse) { + genPool = new DeckGeneratorPool(deckSize, allowedColors, genDialog.isSingleton()); - CardCriteria spellCriteria = new CardCriteria(); - spellCriteria.setCodes(setsToUse.toArray(new String[0])); - spellCriteria.notTypes(CardType.LAND); + final String[] sets = setsToUse.toArray(new String[setsToUse.size()]); - List cardPool = CardRepository.instance.findCards(spellCriteria); - int cardPoolCount = cardPool.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(); - if (cardPoolCount > 0) { + int count = 0; + int reservesAdded = 0; + if (retrievedCount > 0 && retrievedCount >= spellCount) { int tries = 0; - int count = 0; - while (count < cardsCount) { - Card card = cardPool.get(random.nextInt(cardPoolCount)).getMockCard(); - if (cardFitsChosenColors(card, allowedColors)) { - spellCardPool.add(card); - count++; + 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) { + genPool.tryAddReserve(card, cardCMC); + reservesAdded++; + } + } + } } tries++; - if (tries > MAX_TRIES) { // to avoid infinite loop - throw new IllegalStateException("Not enough cards for chosen colors to generate deck: " + selectedColors); + 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."); } - return spellCardPool; } /** - * Check that card can be played using chosen (allowed) colors. - * - * @param card - * @param allowedColors - * @return + * 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 boolean cardFitsChosenColors(Card card, List allowedColors) { - for (String symbol : card.getManaCost().getSymbols()) { - boolean found = false; - symbol = symbol.replace("{", "").replace("}", ""); - if (isColoredMana(symbol)) { - for (ColoredManaSymbol allowed : allowedColors) { - if (symbol.contains(allowed.toString())) { - found = true; + 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 coloured mana symbols over all spells in the deck + Map percentage = genPool.calculateSpellColourPercentages(); + + // Only dual/tri colour lands are generated for now, and not non-basic lands that only produce colourless 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; } } - if (!found) { - return false; - } } } - return true; + // Calculate the amount of coloured 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); } /** - * Generates card pool of land cards that can produce allowed colors. - * - * @param landsCount - * @param allowedColors - * @return + * 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 List generateNonBasicLandCardPool(int landsCount, List allowedColors, List setsToUse) { - List nonBasicLandCardPool = new ArrayList(); + private static Map> generateBasicLands(List setsToUse) { - CardCriteria landCriteria = new CardCriteria(); - landCriteria.setCodes(setsToUse.toArray(new String[0])); - landCriteria.types(CardType.LAND); - landCriteria.notSupertypes("Basic"); - List landCards = CardRepository.instance.findCards(landCriteria); - - int allCount = landCards.size(); - Random random = new Random(); - if (allCount > 0) { - int tries = 0; - int count = 0; - while (count < landsCount) { - Card card = landCards.get(random.nextInt(allCount)).getMockCard(); - if (cardCardProduceChosenColors(card, allowedColors)) { - nonBasicLandCardPool.add(card); - count++; - } - tries++; - if (tries > MAX_TRIES) { // to avoid infinite loop - // return what have been found - return nonBasicLandCardPool; - } - } - } - - return nonBasicLandCardPool; - } - - /** - * Checks that chosen card can produce mana of specific color. - * - * @param card - * @param allowedColors - * @return - */ - private static boolean cardCardProduceChosenColors(Card card, List allowedColors) { - int score = 0; - for (Mana mana : card.getMana()) { - for (ColoredManaSymbol color : allowedColors) { - score += mana.getColor(color); - } - } - if (score > 1) { - return true; - } - return false; - } - - /** - * Get random basic land that can produce specified color mana. - * Random here means random set and collector id for the same mana producing land. - * - * @param color - * @return - */ - private static Card getBestBasicLand(ColoredManaSymbol color, List setsToUse) { - String cardName = ""; - switch(color) { - case G: - cardName = "Forest"; - break; - case W: - cardName = "Plains"; - break; - case R: - cardName = "Mountain"; - break; - case B: - cardName = "Swamp"; - break; - case U: - cardName = "Island"; - break; - } - - List landSets = new LinkedList(); + List landSets = new LinkedList<>(); // decide from which sets basic lands are taken from for (String setCode :setsToUse) { @@ -447,19 +324,78 @@ public class DeckGenerator { if (!landSets.isEmpty()) { criteria.setCodes(landSets.toArray(new String[landSets.size()])); } - criteria.rarities(Rarity.LAND).name(cardName); - List cards = CardRepository.instance.findCards(criteria); - if (cards.isEmpty() && !setsToUse.isEmpty()) { - cards = CardRepository.instance.findCards(cardName); + 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 colourToAdd = 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(); } - int randomInt = new Random().nextInt(cards.size()); - return cards.get(randomInt).getMockCard(); + // 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 colour, 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 colour + 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 + colourToAdd = color; + minPercentage = (neededPercentage - thisPercentage); + } + } + if(colourToAdd != null) { + genPool.addCard(getBasicLand(colourToAdd, basicLands)); + count.put(colourToAdd.toString(), count.get(colourToAdd.toString()) + 1); + colorTotal++; + landsNeeded--; + } + } } - protected static boolean isColoredMana(String symbol) { - return symbol.equals("W") || symbol.equals("G") || symbol.equals("U") || symbol.equals("B") || symbol.equals("R") || symbol.contains("/"); + /** + * 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()); + return basicLands.get(landName).get(random.nextInt(basicLands.size() - 1)).getMockCard().copy(); } + + } diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java new file mode 100644 index 00000000000..1f245f00637 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java @@ -0,0 +1,43 @@ +package mage.client.deck.generator; + +/** + * Stores a range of converted mana costs (CMC) for use in deck generation. + */ +public class DeckGeneratorCMC +{ + public final int min; + public final int max; + public final float percentage; + private int amount = 0; + + /** + * Constructs a CMC range given a minimum and maximum, and the percentage of cards that are in this range. + * @param min the minimum CMC a card in this range can be. + * @param max the maximum CMC a card in this range can be. + * @param percentage the percentage of cards in the range (min, max) + */ + DeckGeneratorCMC(int min, int max, float percentage) + { + this.min = min; + this.max = max; + this.percentage = percentage; + } + + /** + * Sets the amount of cards needed in this CMC range. + * @param amount the number of cards needed. + */ + public void setAmount(int amount) + { + this.amount = amount; + } + + /** + * Gets the number of cards needed in this CMC range. + * @return the number of cards needed in this CMC range. + */ + public int getAmount() + { + return this.amount; + } +} 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 new file mode 100644 index 00000000000..009785b124f --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java @@ -0,0 +1,225 @@ +/* + * 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 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.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.UUID; + +/** + * + * @author Simown + */ +public class DeckGeneratorDialog { + + private static JDialog dlg; + private static String selectedColors; + private static JComboBox cbSets; + private static JComboBox cbDeckSize; + private static JButton btnGenerate, btnCancel; + private static JCheckBox cArtifacts, cSingleton, cNonBasicLands; + + public DeckGeneratorDialog() + { + initDialog(); + } + + private void initDialog() { + JPanel p0 = new JPanel(); + p0.setLayout(new BoxLayout(p0, BoxLayout.Y_AXIS)); + + JLabel text = new JLabel("Choose color for your deck: "); + text.setAlignmentX(Component.CENTER_ALIGNMENT); + p0.add(text); + + p0.add(Box.createVerticalStrut(5)); + String chosen = MageFrame.getPreferences().get("genDeckColor", "u"); + final ColorsChooser colorsChooser = new ColorsChooser(chosen); + p0.add(colorsChooser); + + p0.add(Box.createVerticalStrut(5)); + JLabel text2 = new JLabel("(X - random color)"); + text2.setAlignmentX(Component.CENTER_ALIGNMENT); + p0.add(text2); + + p0.add(Box.createVerticalStrut(5)); + JPanel jPanel = new JPanel(); + JLabel text3 = new JLabel("Choose sets:"); + cbSets = new JComboBox(ConstructedFormats.getTypes()); + cbSets.setSelectedIndex(0); + cbSets.setPreferredSize(new Dimension(300, 25)); + cbSets.setMaximumSize(new Dimension(300, 25)); + cbSets.setAlignmentX(Component.LEFT_ALIGNMENT); + jPanel.add(text3); + jPanel.add(cbSets); + p0.add(jPanel); + + String prefSet = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SET, null); + if (prefSet != null) { + cbSets.setSelectedItem(prefSet); + } + + p0.add(Box.createVerticalStrut(5)); + JPanel jPanel2 = new JPanel(); + JLabel textDeckSize = new JLabel("Deck size:"); + cbDeckSize = new JComboBox(new String[]{"40","60"}); + cbDeckSize.setSelectedIndex(0); + cbDeckSize.setPreferredSize(new Dimension(300, 25)); + cbDeckSize.setMaximumSize(new Dimension(300, 25)); + cbDeckSize.setAlignmentX(Component.LEFT_ALIGNMENT); + jPanel2.add(textDeckSize); + jPanel2.add(cbDeckSize); + p0.add(jPanel2); + + String prefSize = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_DECK_SIZE, "60"); + if (prefSet != null) { + cbDeckSize.setSelectedItem(prefSize); + } + + p0.add(Box.createVerticalStrut(5)); + JPanel jCheckBoxes = new JPanel(); + + // Singletons + cSingleton = new JCheckBox("Singleton", false); + cSingleton.setToolTipText("Allow only a single copy of each non-land card in your deck."); + String singletonEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SINGLETON, "false"); + cSingleton.setSelected(Boolean.valueOf(singletonEnabled)); + jCheckBoxes.add(cSingleton); + + // Artifacts + cArtifacts = new JCheckBox("Artifacts", false); + cArtifacts.setToolTipText("Use artifacts and artifact creatures in your deck."); + String artifactEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_ARTIFACTS, "false"); + cArtifacts.setSelected(Boolean.valueOf(artifactEnabled)); + jCheckBoxes.add(cArtifacts); + + // Non-basic lands + cNonBasicLands = new JCheckBox("Non-basic Lands", false); + cNonBasicLands.setToolTipText("Use non-basic lands in your deck (if applicable)."); + String nonBasicEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_NON_BASIC_LANDS, "false"); + cNonBasicLands.setSelected(Boolean.valueOf(nonBasicEnabled)); + jCheckBoxes.add(cNonBasicLands); + + jCheckBoxes.setPreferredSize(new Dimension(300, 25)); + jCheckBoxes.setMaximumSize(new Dimension(300, 25)); + p0.add(jCheckBoxes); + + btnGenerate = new JButton("Ok"); + btnGenerate.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + btnGenerate.setEnabled(false); + colorsChooser.setEnabled(false); + selectedColors = (String) colorsChooser.getSelectedItem(); + dlg.setVisible(false); + MageFrame.getPreferences().put("genDeckColor", selectedColors); + } + }); + btnCancel = new JButton("Cancel"); + btnCancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dlg.setVisible(false); + selectedColors = null; + } + }); + JButton[] options = {btnGenerate, btnCancel}; + JOptionPane optionPane = new JOptionPane(p0, JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION, null, options, options[1]); + dlg = optionPane.createDialog("Generating deck"); + dlg.setVisible(true); + dlg.dispose(); + } + + public static void cleanUp() { + for (ActionListener al: btnGenerate.getActionListeners()) { + btnGenerate.removeActionListener(al); + } + for (ActionListener al: btnCancel.getActionListeners()) { + btnCancel.removeActionListener(al); + } + //deck = null; + } + + public String saveDeck(Deck deck) { + try { + File tmp = File.createTempFile("tempDeck" + UUID.randomUUID().toString(), ".dck"); + tmp.createNewFile(); + deck.setName("Generated-Deck-" + UUID.randomUUID()); + Sets.saveDeck(tmp.getAbsolutePath(), deck.getDeckCardLists()); + DeckGeneratorDialog.cleanUp(); + return tmp.getAbsolutePath(); + } catch (Exception e) { + JOptionPane.showMessageDialog(null, "Couldn't generate deck. Try again."); + } + return null; + } + + public String getSelectedFormat() { + return (String) cbSets.getSelectedItem(); + } + + public boolean isSingleton() { + boolean selected = cSingleton.isSelected(); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SINGLETON, Boolean.toString(selected)); + return selected; + } + + public boolean useArtifacts() { + boolean selected = cArtifacts.isSelected(); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_ARTIFACTS, Boolean.toString(selected)); + return selected; + } + + public boolean useNonBasicLand() { + boolean selected = cNonBasicLands.isSelected(); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_NON_BASIC_LANDS, Boolean.toString(selected)); + return selected; + } + + public int getDeckSize() { + return Integer.parseInt(cbDeckSize.getSelectedItem().toString()); + } + + public String getSelectedColors() { + if (selectedColors != null) { + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_DECK_SIZE, cbDeckSize.getSelectedItem().toString()); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SET, cbSets.getSelectedItem().toString()); + } + return selectedColors; + } +} diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java new file mode 100644 index 00000000000..eaebe852907 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java @@ -0,0 +1,484 @@ +/* + * 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 mage.abilities.Ability; +import mage.cards.Card; +import mage.cards.decks.Deck; +import mage.cards.repository.CardInfo; +import mage.constants.ColoredManaSymbol; + +import java.util.*; + +/** + * + * @author Simown + */ +public class DeckGeneratorPool +{ + // Constants for a 40 card deck + private static final int CREATURE_COUNT_40 = 15; + private static final int LAND_COUNT_40 = 17; + private static final int NONCREATURE_COUNT_40 = 8; + // Constants for a 60 card deck + private static final int CREATURE_COUNT_60 = 23; + private static final int LAND_COUNT_60 = 24; + private static final int NONCREATURE_COUNT_60 = 13; + + private final List allowedColors; + private final List poolCMCs; + private final int creatureCount; + private final int nonCreatureCount; + private final int landCount; + private final boolean isSingleton; + private final int deckSize; + + // Count how many copies of the card exists in the deck to check we don't go over 4 copies (or 1 for singleton) + private Map cardCounts = new HashMap<>(); + // If there is only a single colour selected to generate a deck + private boolean monoColored = false; + // List of cards so far in the deck + private List deckCards = new ArrayList<>(); + // List of reserve cards found to fix up undersized decks + private List reserveSpells = new ArrayList<>(); + private Deck deck; + + public DeckGeneratorPool(final int deckSize, final List allowedColors, boolean isSingleton) + { + this.deckSize = deckSize; + this.allowedColors = allowedColors; + this.isSingleton = isSingleton; + + this.deck = new Deck(); + + if(this.deckSize > 40) { + this.creatureCount = CREATURE_COUNT_60; + this.nonCreatureCount = NONCREATURE_COUNT_60; + this.landCount = LAND_COUNT_60; + poolCMCs = new ArrayList() {{ + add(new DeckGeneratorCMC(0, 2, 0.20f)); + add(new DeckGeneratorCMC(3, 5, 0.50f)); + add(new DeckGeneratorCMC(6, 7, 0.25f)); + add(new DeckGeneratorCMC(8, 100, 0.5f)); + }}; + + } + else { + this.creatureCount = CREATURE_COUNT_40; + this.nonCreatureCount = NONCREATURE_COUNT_40; + this.landCount = LAND_COUNT_40; + poolCMCs = new ArrayList() {{ + add(new DeckGeneratorCMC(0, 2, 0.30f)); + add(new DeckGeneratorCMC(3, 4, 0.45f)); + add(new DeckGeneratorCMC(5, 6, 0.20f)); + add(new DeckGeneratorCMC(7, 100, 0.5f)); + }}; + } + + if(allowedColors.size() == 1) { + monoColored = true; + } + + } + + + /** + * Adjusts the number of spell cards that should be in a converted mana cost (CMC) range, given the amount of cards total. + * @param cardsCount the number of total cards. + * @return a list of CMC ranges, with the amount of cards for each CMC range + */ + public List getCMCsForSpellCount(int cardsCount) { + List adjustedCMCs = new ArrayList<>(this.poolCMCs); + // For each CMC calculate how many spell cards are needed, given the total amount of cards + for(DeckGeneratorCMC deckCMC : adjustedCMCs) { + deckCMC.setAmount((int)Math.ceil(deckCMC.percentage * cardsCount)); + } + return adjustedCMCs; + } + + + /** + * Verifies if the spell card supplied is valid for this pool of cards. + * Checks that there isn't too many copies of this card in the deck. + * Checks that the card fits the chosen colors for this pool. + * @param card the spell card + * @return if the spell card is valid for this pool. + */ + public boolean isValidSpellCard(Card card) + { + int cardCount = getCardCount((card.getName())); + // Check it hasn't already got the maximum number of copies in a deck + if(cardCount < (isSingleton ? 1 : 4)) { + if(cardFitsChosenColors(card)) { + return true; + } + } + return false; + } + + /** + * Verifies if the non-basic land card supplied is valid for this pool of cards. + * @param card the non-basic land card + * @return if the land card generates the allowed colors for this pool. + */ + public boolean isValidLandCard(Card card) + { + int cardCount = getCardCount((card.getName())); + // No need to check if the land is valid for the colors chosen + // They are all filtered before searching for lands to include in the deck. + return (cardCount < 4); + } + + + /** + * Adds a card to the pool and updates the count of this card. + * @param card the card to add. + */ + public void addCard(Card card) + { + Object cnt = cardCounts.get((card.getName())); + if(cnt == null) + cardCounts.put(card.getName(), 0); + int existingCount = cardCounts.get((card.getName())); + cardCounts.put(card.getName(), existingCount+1); + deckCards.add(card); + } + + /** + * Adds a card to the reserve pool. + * Reserve pool is used when the deck generation fails to build a complete deck, or + * a partially complete deck (e.g. if there are no cards found that match a CMC) + * @param card the card to add + * @param cardCMC the converted mana cost of the card + */ + public void tryAddReserve(Card card, int cardCMC) { + // Only cards with CMC < 7 and don't already exist in the deck + // can be added to our reserve pool as not to overwhelm the curve + // with high CMC cards and duplicates. + if(cardCMC < 7 && getCardCount(card.getName()) == 0) { + this.reserveSpells.add(card); + } + } + + /** + * Checks if the mana symbols in the card all match the allowed colors for this pool. + * @param card the spell card to check. + * @return if all the mana symbols fit the chosen colors. + */ + private boolean cardFitsChosenColors(Card card) { + for (String symbol : card.getManaCost().getSymbols()) { + boolean found = false; + symbol = symbol.replace("{", "").replace("}", ""); + if (isColoredManaSymbol(symbol)) { + for (ColoredManaSymbol allowed : allowedColors) { + if (symbol.contains(allowed.toString())) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + return true; + } + + + /** + * Calculates the percentage of colored mana symbols over all spell cards in the deck. + * Used to balance the generation of basic lands so the amount of lands matches the + * cards mana costs. + * @return a list of colored mana symbols and the percentage of symbols seen in cards mana costs. + */ + public Map calculateSpellColourPercentages() { + + final Map colorCount = new HashMap<>(); + for (final ColoredManaSymbol color : ColoredManaSymbol.values()) { + colorCount.put(color.toString(), 0); + } + + // Counts how many colored mana symbols we've seen in total so we can get the percentage of each color + int totalCount = 0; + + List fixedSpells = getFixedSpells(); + for(Card spell: fixedSpells) { + for (String symbol : spell.getManaCost().getSymbols()) { + symbol = symbol.replace("{", "").replace("}", ""); + if (isColoredManaSymbol(symbol)) { + for (ColoredManaSymbol allowed : allowedColors) { + if (symbol.contains(allowed.toString())) { + int cnt = colorCount.get(allowed.toString()); + colorCount.put(allowed.toString(), cnt+1); + totalCount++; + } + } + } + } + } + final Map percentages = new HashMap<>(); + for(Map.Entry singleCount: colorCount.entrySet()) { + String color = singleCount.getKey(); + int count = singleCount.getValue(); + // Calculate the percentage this colour has out of the total colour counts + double percentage = (count / (double) totalCount) * 100; + percentages.put(color, percentage); + } + return percentages; + } + + /** + * Calculates how many of each mana the non-basic lands produce. + * @param deckLands the non-basic lands which will be used in the deck. + * @return a mapping of colored mana symbol to the amount that can be produced. + */ + public Map countManaProduced(List deckLands) + { + Map manaCounts = new HashMap<>(); + for (final ColoredManaSymbol color : ColoredManaSymbol.values()) { + manaCounts.put(color.toString(), 0); + } + for(Card land: deckLands) { + for(Ability landAbility: land.getAbilities()) { + for (ColoredManaSymbol symbol : allowedColors) { + String abilityString = landAbility.getRule(); + if(landTapsForAllowedColor(abilityString, symbol.toString())) { + Integer count = manaCounts.get(symbol.toString()); + manaCounts.put(symbol.toString(), count + 1); + } + } + } + } + return manaCounts; + } + + /** Filter all the non-basic lands retrieved from the database. + * @param landCardsInfo information about all the cards. + * @return a list of cards that produce the allowed colors for this pool. + */ + public List filterLands(List landCardsInfo) { + List matchingLandList = new ArrayList<>(); + for(CardInfo landCardInfo: landCardsInfo) { + Card landCard = landCardInfo.getMockCard(); + if(landProducesChosenColors(landCard)) { + matchingLandList.add(landCard); + } + } + return matchingLandList; + } + + /** + * Returns the card name that represents the basic land for this colour. + * @param symbolString the colored mana symbol. + * @return the name of a basic land card. + */ + public static String getBasicLandName(String symbolString) { + switch(symbolString) { + case "B": + return "Swamp"; + case "G": + return "Forest"; + case "R": + return "Mountain"; + case "U": + return "Island"; + case "W": + return "Plains"; + default: + return ""; + } + } + + + /** + * Returns a complete deck. + * @return the deck. + */ + public Deck getDeck() { + Set actualDeck = deck.getCards(); + for(Card card : deckCards) + actualDeck.add(card); + return deck; + } + + + /** + * Returns the number of creatures needed in this pool. + * @return the number of creatures. + */ + public int getCreatureCount() { + return creatureCount; + } + + /** + * Returns the number of non-creatures needed in this pool. + * @return the number of non-creatures. + */ + public int getNonCreatureCount() { + return nonCreatureCount; + } + + /** + * Returns the number of lands (basic + non-basic) needed in this pool. + * @return the number of lands. + */ + public int getLandCount() { + return landCount; + } + + /** + * Returns if this pool only uses one color. + * @return if this pool is monocolored. + */ + public boolean isMonoColoredDeck() { + return monoColored; + } + + /** + * Returns the size of the deck to generate from this pool. + * @return the deck size. + */ + public int getDeckSize() { + return deckSize; + } + + /** + * Fixes undersized or oversized decks that have been generated. + * Removes random cards from an oversized deck until it is the correct size. + * Uses the reserve pool to fill up and undersized deck with cards. + * @return a fixed list of cards for this deck. + */ + private List getFixedSpells() + { + Random random = new Random(); + int spellSize = deckCards.size(); + int nonLandSize = (deckSize - landCount); + + // Less spells than needed + if(spellSize < nonLandSize) { + + int spellsNeeded = nonLandSize-spellSize; + List spellsToAdd = new ArrayList<>(spellsNeeded); + + // Initial reservoir + for(int i = 0; i < spellsNeeded-1; i++) + spellsToAdd.add(reserveSpells.get(i)); + + for(int j = spellsNeeded+1; j < reserveSpells.size()-1; j++) { + int index = random.nextInt(j); + Card randomCard = reserveSpells.get(index); + if (index < j && isValidSpellCard(randomCard)) { + spellsToAdd.set(j, randomCard); + } + } + // Add randomly selected spells needed + deckCards.addAll(spellsToAdd); + } + + // More spells than needed + else if(spellSize > (deckSize - landCount)) { + + int spellsRemoved = (spellSize)-(deckSize-landCount); + for(int i = 0; i < spellsRemoved; ++i) { + deckCards.remove(random.nextInt(deckCards.size())); + } + } + if(deckCards.size() != nonLandSize) + throw new IllegalStateException("Not enough cards found to generate deck. Please try again"); + + // Return the fixed amount + return deckCards; + } + + /** + * Returns if this land will produce the chosen colors for this pool. + * @param card a non-basic land card. + * @return if this land card taps to produces the colors chosen. + */ + private boolean landProducesChosenColors(Card card) { + // All mock card abilities will be MockAbilities so we can't differentiate between ManaAbilities + // and other Abilities so we have to do some basic string matching on land cards for now. + List landAbilities = card.getAbilities(); + int count = 0; + for(Ability ability : landAbilities) { + String abilityString = ability.getRule(); + // Lands that tap to produce mana of the chosen colors + for(ColoredManaSymbol symbol : allowedColors) { + if(landTapsForAllowedColor(abilityString, symbol.toString())) { + count++; + } + } + if(count > 1) { + return true; + } + } + return false; + } + + /** + * Returns if this land taps for the given color. + * Basic string matching to check the ability adds one of the chosen mana when tapped. + * @param ability MockAbility of the land card + * @param symbol colored mana symbol. + * @return if the ability is tapping to produce the mana the symbol represents. + */ + private boolean landTapsForAllowedColor(String ability, String symbol) { + return ability.matches(".*Add \\{" + symbol + "\\} to your mana pool."); + } + + /** + * Returns if the symbol is a colored mana symbol. + * @param symbol the symbol to check. + * @return If it is a basic mana symbol or a hybrid mana symbol. + */ + private static boolean isColoredManaSymbol(String symbol) { + // Hybrid mana + if(symbol.contains("/")) { + return true; + } + for(ColoredManaSymbol c: ColoredManaSymbol.values()) { + if (symbol.charAt(0) == (c.toString().charAt(0))) { + return true; + } + } + return false; + } + + /** + * Returns how many of this card is in the pool. + * If there are none in the pool it will initalise the card count. + * @param cardName the name of the card to check. + * @return the number of cards in the pool of this name. + */ + private int getCardCount(String cardName) { + Object cC = cardCounts.get((cardName)); + if(cC == null) + cardCounts.put(cardName, 0); + return cardCounts.get((cardName)); + } +} diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index f5a24f64c81..bd764f48ce6 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -210,6 +210,9 @@ public class PreferencesDialog extends javax.swing.JDialog { // pref setting for deck generator public static final String KEY_NEW_DECK_GENERATOR_DECK_SIZE = "newDeckGeneratorDeckSize"; public static final String KEY_NEW_DECK_GENERATOR_SET = "newDeckGeneratorSet"; + public static final String KEY_NEW_DECK_GENERATOR_SINGLETON = "newDeckGeneratorSingleton"; + public static final String KEY_NEW_DECK_GENERATOR_ARTIFACTS = "newDeckGeneratorArtifacts"; + public static final String KEY_NEW_DECK_GENERATOR_NON_BASIC_LANDS = "newDeckGeneratorNonBasicLands"; // used to save and restore the settings for the cardArea (draft, sideboarding, deck builder) public static final String KEY_DRAFT_VIEW = "draftView";