diff --git a/forge-core/src/main/java/forge/deck/CardPool.java b/forge-core/src/main/java/forge/deck/CardPool.java index 361163422c6..72fe0a22d4a 100644 --- a/forge-core/src/main/java/forge/deck/CardPool.java +++ b/forge-core/src/main/java/forge/deck/CardPool.java @@ -18,12 +18,15 @@ package forge.deck; import com.google.common.base.Predicate; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Multimaps; import forge.StaticData; import forge.card.CardDb; import forge.card.CardEdition; import forge.item.IPaperCard; import forge.item.PaperCard; +import forge.util.CollectionSuppliers; import forge.util.ItemPool; import forge.util.ItemPoolSorter; import forge.util.MyRandom; @@ -49,18 +52,8 @@ public class CardPool extends ItemPool { } public void add(final String cardRequest, final int amount) { - Map dbs = StaticData.instance().getAvailableDatabases(); - PaperCard paperCard = null; - for (CardDb db: dbs.values()){ - paperCard = db.getCard(cardRequest); - if (paperCard != null) - break; - } - if (paperCard == null){ - System.err.print("An unsupported card was requested: \"" + cardRequest + "\". "); - paperCard = StaticData.instance().getCommonCards().createUnsupportedCard(cardRequest); - } - this.add(paperCard, amount); + CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest); + this.add(request.cardName, request.edition, request.artIndex, amount); } public void add(final String cardName, final String setCode) { @@ -92,7 +85,7 @@ public class CardPool extends ItemPool { if (paperCard == null && loadAttempt < 2) { /* Attempt to load the card first, and then try again all the three available DBs as we simply don't know which db the card has been added to (in case). */ - StaticData.instance().attemptToLoadCard(cardName); + StaticData.instance().attemptToLoadCard(cardName, setCode); artIndex = IPaperCard.DEFAULT_ART_INDEX; // Reset Any artIndex passed in, at this point } } @@ -167,9 +160,9 @@ public class CardPool extends ItemPool { * @param includeBasicLands determines whether or not basic lands should be counted in or * not when gathering statistics * @return Map - * An HashMap mapping each CardEdition to its corresponding frequency count + * An HashMap structure mapping each CardEdition in Pool to its corresponding frequency count */ - public Map getCardEditionFrequencyMap(boolean includeBasicLands) { + public Map getCardEditionStatistics(boolean includeBasicLands) { Map editionStatistics = new HashMap<>(); for(Entry cp : this.items.entrySet()) { PaperCard card = cp.getKey(); @@ -185,74 +178,188 @@ public class CardPool extends ItemPool { return editionStatistics; } - /** Determines the Pivot Edition for cards in the Pool, according to default - * Card Art Preference settings (@see forge.card.CardDb.CardArtPreference). + /** + * Returns the map of card frequency indexed by frequency value, rather than single card edition. + * Therefore, all editions with the same card count frequency will be grouped together. * - * For a more thorough explanation of Pivot Edition, please - * @see CardPool#getPivotEdition(boolean) + * Note: This method returns the reverse map generated by getCardEditionStatistics * - * @return CardEdition representing the reference edition for the card pool. + * @param includeBasicLands Decide to include or not basic lands in gathered statistics + * + * @return a ListMultimap structure matching each unique frequency value to its corresponding list + * of CardEditions + * + * @see CardPool#getCardEditionStatistics(boolean) */ - public CardEdition getPivotEdition(){ - boolean isLatestCardArtPreference = StaticData.instance().cardArtPreferenceIsLatest(); - return getPivotEdition(isLatestCardArtPreference); + public ListMultimap getCardEditionsGroupedByNumberOfCards(boolean includeBasicLands){ + Map editionsFrequencyMap = this.getCardEditionStatistics(includeBasicLands); + ListMultimap reverseMap = Multimaps.newListMultimap(new HashMap<>(), CollectionSuppliers.arrayLists()); + for (Map.Entry entry : editionsFrequencyMap.entrySet()) + reverseMap.put(entry.getValue(), entry.getKey()); + return reverseMap; + } + + /** + * Gather Statistics per Edition Type from cards included in the CardPool. + * + * @param includeBasicLands Determine whether or not basic lands should be included in gathered statistics + * + * @return an HashMap structure mapping each CardEdition.Type found among + * cards in the Pool, and their corresponding (card) count. + * + * @see CardPool#getCardEditionStatistics(boolean) + */ + public Map getCardEditionTypeStatistics(boolean includeBasicLands){ + Map editionTypeStats = new HashMap<>(); + Map editionStatistics = this.getCardEditionStatistics(includeBasicLands); + for(Entry entry : editionStatistics.entrySet()) { + CardEdition edition = entry.getKey(); + int count = entry.getValue(); + CardEdition.Type key = edition.getType(); + int currentCount = editionTypeStats.getOrDefault(key, 0); + currentCount += count; + editionTypeStats.put(key, currentCount); + } + return editionTypeStats; + } + + /** + * Returns the CardEdition.Type that is the most frequent among cards' editions + * in the pool. In case of more than one candidate, Expansion Type will be preferred (if available). + * + * @return The most frequent CardEdition.Type in the pool, or null if the Pool is empty + */ + public CardEdition.Type getTheMostFrequentEditionType(){ + Map editionTypeStats = this.getCardEditionTypeStatistics(false); + Integer mostFrequentType = 0; + List mostFrequentEditionTypes = new ArrayList<>(); + for (Map.Entry entry : editionTypeStats.entrySet()){ + if (entry.getValue() > mostFrequentType) { + mostFrequentType = entry.getValue(); + mostFrequentEditionTypes.add(entry.getKey()); + } + } + if (mostFrequentEditionTypes.isEmpty()) + return null; + CardEdition.Type mostFrequentEditionType = mostFrequentEditionTypes.get(0); + for (int i=1; i < mostFrequentEditionTypes.size(); i++){ + CardEdition.Type frequentType = mostFrequentEditionTypes.get(i); + if (frequentType == CardEdition.Type.EXPANSION) + return frequentType; + } + return mostFrequentEditionType; + } + + /** + * Determines whether (the majority of the) cards in the Pool are modern framed + * (that is, cards are from Modern Card Edition). + * + * @return True if the majority of cards in Pool are from Modern Edition, false otherwise. + * If the count of Modern and PreModern cards is tied, the return value is determined + * by the preferred Card Art Preference settings, namely True if Latest Art, False otherwise. + */ + public boolean isModern(){ + int modernEditionsCount = 0; + int preModernEditionsCount = 0; + Map editionStats = this.getCardEditionStatistics(false); + for (Map.Entry entry: editionStats.entrySet()){ + CardEdition edition = entry.getKey(); + if (edition.isModern()) + modernEditionsCount += entry.getValue(); + else + preModernEditionsCount += entry.getValue(); + } + if (modernEditionsCount == preModernEditionsCount) + return StaticData.instance().cardArtPreferenceIsLatest(); + return modernEditionsCount > preModernEditionsCount; } /** * Determines the Pivot Edition for cards in the Pool. *

- * The Pivot Edition refers to the Reference Edition considered as the - * most representative for cards in the pool. - * The Pivot Edition simply corresponds to the edition having the majority of cards - * in the pool. + * The Pivot Edition refers to the CardEdition for cards in the pool that sets the + * reference boundary for cards in the pool. + * Therefore, the Pivot Edition will be selected considering the per-edition distribution of + * cards in the Pool. + * If the majority of the cards in the pool corresponds to a single edition, this edition will be the Pivot. + * The majority exists if the highest card frequency accounts for at least a third of the whole Pool + * (i.e. 1 over 3 cards - not including basic lands). *

- * Whenever this Edition cannot be immediately determined - * (i.e. the max is not unique), the Pivot Edition will be chosen - * according to the specified set-preference criterion. + * However, there are cases in which cards in a Pool are gathered from several editions, so that there is + * no clear winner for a single edition of reference. + * In these cases, the Pivot will be selected as the "Median Edition", that is the edition whose frequency + * is the closest to the average. *

- * In other words: - *

    - *
  • isLatestCardArtPreference=true:
  • Pivot Edition will be - * the earliest among the most recent editions (lower bound). - *
  • isLatestCardArtPreference=false:
  • Pivot Edition will be - * the latest among the earliest editions (upper bound). - *
- *

- * Note: Cards in the Pool may have been generated according to the specified CardArtPreference - * but we might be interested in "forcing" a specific selection criterion. + * In cases where multiple candidates could be selected (most likely to occur when the average frequency + * is considered) pivot candidates will be first sorted in ascending (earliest edition first) or + * descending (latest edition first) order depending on whether or not the selected Card Art Preference policy + * and the majority of cards in the Pool are compliant. This is to give preference more likely to + * the best candidate for alternative card art print search. * - * @param isLatestCardArtPreference if true, precedence will be given to most recent editions - * (Latest Card Art) + * @param isLatestCardArtPreference Determines whether the Card Art Preference to consider should + * prefer or not Latest Card Art Editions first. * @return CardEdition instance representing the Pivot Edition + * + * @see #isModern() */ - public CardEdition getPivotEdition(boolean isLatestCardArtPreference) { - CardEdition pivotEdition = null; - int maxCardOccurrence = 0; - Map editionsStatistics = this.getCardEditionFrequencyMap(false); - - for(Entry entry : editionsStatistics.entrySet()) { - Integer cardCount = entry.getValue(); - CardEdition ed = entry.getKey(); - if (cardCount < maxCardOccurrence) - continue; - - if (pivotEdition == null || cardCount > maxCardOccurrence){ - maxCardOccurrence = cardCount; - pivotEdition = ed; + public CardEdition getPivotCardEdition(boolean isLatestCardArtPreference) { + ListMultimap editionsStatistics = this.getCardEditionsGroupedByNumberOfCards(false); + List frequencyValues = new ArrayList<>(editionsStatistics.keySet()); + // Sort in descending order + frequencyValues.sort(new Comparator() { + @Override + public int compare(Integer f1, Integer f2) { + return (f1.compareTo(f2)) * -1; } - else { // i.e. cardCount == maxCardOccurrence - if (isLatestCardArtPreference){ - // update only if older - if (ed.getDate().compareTo(pivotEdition.getDate()) < 0) - pivotEdition = ed; - } else { - // update only if newer - if (ed.getDate().compareTo(pivotEdition.getDate()) > 0) - pivotEdition = ed; - } + }); + float weightedMean = 0; + int sumWeights = 0; + for (Integer freq : frequencyValues) { + int editionsCount = editionsStatistics.get(freq).size(); + int weightedFrequency = freq * editionsCount; + sumWeights += editionsCount; + weightedMean += weightedFrequency; + } + int totalNoCards = (int)weightedMean; + weightedMean /= sumWeights; + + int topFrequency = frequencyValues.get(0); + float ratio = ((float) topFrequency) / totalNoCards; + // determine the Pivot Frequency + int pivotFrequency; + if (ratio >= 0.33) // 1 over 3 cards are from the most frequent edition(s) + pivotFrequency = topFrequency; + else + pivotFrequency = getMedianFrequency(frequencyValues, weightedMean); + + // Now Get editions corresponding to pivot frequency + List pivotCandidates = new ArrayList<>(editionsStatistics.get(pivotFrequency)); + // Now Sort candidates chronologically + pivotCandidates.sort(new Comparator() { + @Override + public int compare(CardEdition ed1, CardEdition ed2) { + return ed1.compareTo(ed2); + } + }); + boolean searchPolicyAndPoolAreCompliant = isLatestCardArtPreference == this.isModern(); + if (!searchPolicyAndPoolAreCompliant) + Collections.reverse(pivotCandidates); // reverse to have latest-first. + return pivotCandidates.get(0); + } + + /* Utility (static) method to return the median value given a target mean. */ + private static int getMedianFrequency(List frequencyValues, float meanFrequency) { + int medianFrequency = frequencyValues.get(0); + float refDelta = Math.abs(meanFrequency - medianFrequency); + for (int i = 1; i < frequencyValues.size(); i++){ + int currentFrequency = frequencyValues.get(i); + float delta = Math.abs(meanFrequency - currentFrequency); + if (delta < refDelta) { + medianFrequency = currentFrequency; + refDelta = delta; } } - return pivotEdition; + return medianFrequency; } @Override