/* * 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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; 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.ExpansionRepository; import mage.client.dialog.PreferencesDialog; import mage.client.util.sets.ConstructedFormats; import mage.constants.CardType; import mage.constants.ColoredManaSymbol; import mage.constants.Rarity; import mage.util.RandomUtil; import mage.util.TournamentUtil; /** * Generates random card pool and builds a deck. * * @author nantuko * @author Simown */ public final class DeckGenerator { public static class DeckGeneratorException extends RuntimeException { public DeckGeneratorException(String message) { super(message); } } 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<ColoredManaSymbol> allowedColors = new ArrayList<>(); selectedColors = selectedColors != null ? selectedColors.toUpperCase() : getRandomColors("X"); String format = genDialog.getSelectedFormat(); List<String> setsToUse = ConstructedFormats.getSetsByFormat(format); if (setsToUse == null) { throw new DeckGeneratorException("Deck sets aren't initialized; please connect to a server to update the database."); } 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) { List<Character> 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 = RandomUtil.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<ColoredManaSymbol> allowedColors, List<String> setsToUse) { genPool = new DeckGeneratorPool(deckSize, genDialog.getCreaturePercentage(), genDialog.getNonCreaturePercentage(), genDialog.getLandPercentage(), allowedColors, genDialog.isSingleton(), genDialog.isColorless(), genDialog.isAdvanced(), genDialog.getDeckGeneratorCMC()); 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<String, List<CardInfo>> 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<CardInfo> cardPool = CardRepository.instance.findCards(criteria); int retrievedCount = cardPool.size(); List<DeckGeneratorCMC.CMC> deckCMCs = genPool.getCMCsForSpellCount(spellCount); int count = 0; int reservesAdded = 0; boolean added; if (retrievedCount > 0 && retrievedCount >= spellCount) { int tries = 0; while (count < spellCount) { Card card = cardPool.get(RandomUtil.nextInt(retrievedCount)).getMockCard(); if (genPool.isValidSpellCard(card)) { int cardCMC = card.getManaCost().convertedManaCost(); for (DeckGeneratorCMC.CMC 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<String, List<CardInfo>> basicLands) { int tries = 0; int countNonBasic = 0; // Store the nonbasic lands (if any) we'll add List<Card> deckLands = new ArrayList<>(); // Calculates the percentage of colored mana symbols over all spells in the deck Map<String, Double> 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<Card> landCards = genPool.filterLands(CardRepository.instance.findCards(criteria)); int allCount = landCards.size(); if (allCount > 0) { while (countNonBasic < landsCount / 2) { Card card = landCards.get(RandomUtil.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<String, Integer> 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<String, List<CardInfo>> generateBasicLands(List<String> setsToUse) { Set<String> landSets = TournamentUtil.getLandSetCodeForDeckSets(setsToUse); CardCriteria criteria = new CardCriteria(); if (!landSets.isEmpty()) { criteria.setCodes(landSets.toArray(new String[landSets.size()])); } Map<String, List<CardInfo>> basicLandMap = new HashMap<>(); for (ColoredManaSymbol c : ColoredManaSymbol.values()) { String landName = DeckGeneratorPool.getBasicLandName(c.toString()); criteria.rarities(Rarity.LAND).name(landName); List<CardInfo> cards = CardRepository.instance.findCards(criteria); if (cards.isEmpty()) { // Workaround to get basic lands if lands are not available for the given sets criteria.setCodes("ORI"); 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<String, Double> percentage, Map<String, Integer> count, Map<String, List<CardInfo>> basicLands) { int colorTotal = 0; ColoredManaSymbol colorToAdd = ColoredManaSymbol.U; // Add up the totals for all colors, to keep track of the percentage a color is. for (Map.Entry<String, Integer> 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); } } 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<String, List<CardInfo>> basicLands) { String landName = DeckGeneratorPool.getBasicLandName(color.toString()); List<CardInfo> basicLandsInfo = basicLands.get(landName); return basicLandsInfo.get(RandomUtil.nextInt(basicLandsInfo.size() - 1)).getMockCard().copy(); } }