From e54916e8378c26d77aefadb3f706f054ab0d51a5 Mon Sep 17 00:00:00 2001 From: leriomaggio Date: Thu, 12 Aug 2021 16:29:46 +0100 Subject: [PATCH] Extended API with new methods to gather Pool Statistics and the Pivot CardEdition CardPool API has been extended by including utility methods to gather specific statistics about a cardPool. These statistics include: - Distribution (card count frequency) of the Card Edition included in the Pool - Distribution of the Card Edition Type included in the Pool - Retrieving the most common Card Edition Type among those included in the Pool - Determine whether or not the Pool "isModern" (i.e. the majority of cards is gathered from Modern Sets) - get the PivotCardEdition: this is the cornerstone of card art optimisation for decks The PivotCardEdition is the edition that will be considered the threshold boundary for cards in the pool. Any decision of card arts for other cards will be considered based on the PivotEdition, that is "alternativeCardPrint" released BEFORE or AFTER (depending on the current Card Art Preference) the PIVOT EDITION RELEASE DATE. Also, this commit includes an optimisation in add method implementation, getting rid of lots of duplicated code! --- .../src/main/java/forge/deck/CardPool.java | 241 +++++++++++++----- 1 file changed, 174 insertions(+), 67 deletions(-) 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