/* * 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 mage.util.RandomUtil; import java.util.*; /** * * @author Simown */ public class DeckGeneratorPool { public static final int DEFAULT_CREATURE_PERCENTAGE = 38; public static final int DEFAULT_NON_CREATURE_PERCENTAGE = 21; public static final int DEFAULT_LAND_PERCENTAGE = 41; private final List<ColoredManaSymbol> allowedColors; private final boolean colorlessAllowed; private final List<DeckGeneratorCMC.CMC> 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 final Map<String, Integer> cardCounts = new HashMap<>(); // If there is only a single color selected to generate a deck private boolean monoColored = false; // List of cards so far in the deck private final List<Card> deckCards = new ArrayList<>(); // List of reserve cards found to fix up undersized decks private final List<Card> reserveSpells = new ArrayList<>(); private final Deck deck; /** * Creates a card pool with specified criterea used when generating a deck. * * @param deckSize the size of the complete deck * @param creaturePercentage what percentage of creatures to use when generating the deck. * @param nonCreaturePercentage percentage of non-creatures to use when generating the deck. * @param landPercentage percentage of lands to use when generating the deck. * @param allowedColors which card colors are allowed in the generated deck. * @param isSingleton if the deck only has 1 copy of each non-land card. * @param colorlessAllowed if colourless mana symbols are allowed in costs in the deck. * @param isAdvanced if the user has provided advanced options to generate the deck. * @param deckGeneratorCMC the CMC curve to use for this deck */ public DeckGeneratorPool(final int deckSize, final int creaturePercentage, final int nonCreaturePercentage, final int landPercentage, final List<ColoredManaSymbol> allowedColors, boolean isSingleton, boolean colorlessAllowed, boolean isAdvanced, DeckGeneratorCMC deckGeneratorCMC) { this.deckSize = deckSize; this.allowedColors = allowedColors; this.isSingleton = isSingleton; this.colorlessAllowed = colorlessAllowed; this.deck = new Deck(); // Advanced (CMC Slider panel and curve drop-down in the dialog) if(isAdvanced) { this.creatureCount = (int)Math.ceil((deckSize / 100.0) * creaturePercentage); this.nonCreatureCount = (int)Math.ceil((deckSize / 100.0)* nonCreaturePercentage); this.landCount = (int)Math.ceil((deckSize / 100.0)* landPercentage); if(this.deckSize == 60) { this.poolCMCs = deckGeneratorCMC.get60CardPoolCMC(); } else { this.poolCMCs = deckGeneratorCMC.get40CardPoolCMC(); } } else { // Ignore the advanced group, just use defaults this.creatureCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_CREATURE_PERCENTAGE); this.nonCreatureCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_NON_CREATURE_PERCENTAGE); this.landCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_LAND_PERCENTAGE); if(this.deckSize == 60) { this.poolCMCs = DeckGeneratorCMC.Default.get60CardPoolCMC(); } else { this.poolCMCs = DeckGeneratorCMC.Default.get40CardPoolCMC(); } } 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<DeckGeneratorCMC.CMC> getCMCsForSpellCount(int cardsCount) { List<DeckGeneratorCMC.CMC> adjustedCMCs = new ArrayList<>(this.poolCMCs); // For each CMC calculate how many spell cards are needed, given the total amount of cards for(DeckGeneratorCMC.CMC 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 boolean 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); return true; } return false; } /** * 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; } } if (symbol.equals("C") && !colorlessAllowed) { 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<String, Double> calculateSpellColorPercentages() { final Map<String, Integer> 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<Card> 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<String, Double> percentages = new HashMap<>(); for(Map.Entry<String, Integer> singleCount: colorCount.entrySet()) { String color = singleCount.getKey(); int count = singleCount.getValue(); // Calculate the percentage this color has out of the total color 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<String,Integer> countManaProduced(List<Card> deckLands) { Map<String, Integer> 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<Card> filterLands(List<CardInfo> landCardsInfo) { List<Card> 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 color. * @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<Card> 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<Card> getFixedSpells() { int spellSize = deckCards.size(); int nonLandSize = (deckSize - landCount); // Less spells than needed if(spellSize < nonLandSize) { int spellsNeeded = nonLandSize-spellSize; // If we haven't got enough spells in reserve to fulfil the amount we need, skip adding any. if(reserveSpells.size() >= spellsNeeded) { List<Card> spellsToAdd = new ArrayList<>(spellsNeeded); // Initial reservoir for (int i = 0; i < spellsNeeded; i++) spellsToAdd.add(reserveSpells.get(i)); for (int i = spellsNeeded + 1; i < reserveSpells.size() - 1; i++) { int j = RandomUtil.nextInt(i); Card randomCard = reserveSpells.get(j); if (isValidSpellCard(randomCard) && j < spellsToAdd.size()) { spellsToAdd.set(j, randomCard); } } // Add randomly selected spells needed 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(RandomUtil.nextInt(deckCards.size())); } } // Check we have exactly the right amount of cards for a deck. if(deckCards.size() != nonLandSize) { throw new IllegalStateException("Not enough cards found to generate deck."); } // Return the fixed amount return deckCards; } /** * 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 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<Ability> 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 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)); } }