forge.quest.BoosterUtils.java Source code

Java tutorial

Introduction

Here is the source code for forge.quest.BoosterUtils.java

Source

/*
 * Forge: Play Magic: the Gathering.
 * Copyright (C) 2011  Forge Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package forge.quest;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.card.MagicColor;
import forge.card.PrintSheet;
import forge.item.*;
import forge.item.IPaperCard.Predicates.Presets;
import forge.model.FModel;
import forge.quest.data.QuestPreferences.QPref;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.PredicateString.StringOp;

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * <p>
 * QuestBoosterPack class. Generates cards for the Card Pool in Quest Mode
 * </p>
 *
 * @author Forge
 * @version $Id: BoosterUtils.java 29709 2015-06-27 19:24:16Z drdev $
 */
public final class BoosterUtils {

    private static final List<Byte> possibleColors = new ArrayList<>();

    private static final int RARES_PER_MYTHIC = 8;
    private static final int MAX_BIAS = 100; //Bias is a percentage; this is 100%

    private static final int[] COLOR_COUNT_PROBABILITIES = new int[] { 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4,
            5, 6 };

    /**
     * Gets the quest starter deck.
     *
     * @param filter
     *            the filter
     * @param numCommons
     *            the num common
     * @param numUncommons
     *            the num uncommon
     * @param numRares
     *            the num rare
     * @param userPrefs
     *            the starting pool preferences
     * @return the quest starter deck
     */
    public static List<PaperCard> getQuestStarterDeck(final Predicate<PaperCard> filter, final int numCommons,
            final int numUncommons, final int numRares, final StartingPoolPreferences userPrefs) {

        if (possibleColors.isEmpty()) {
            possibleColors.add(MagicColor.BLACK);
            possibleColors.add(MagicColor.BLUE);
            possibleColors.add(MagicColor.GREEN);
            possibleColors.add(MagicColor.RED);
            possibleColors.add(MagicColor.WHITE);
            possibleColors.add(MagicColor.COLORLESS);
        }

        final List<PaperCard> cardPool = Lists
                .newArrayList(Iterables.filter(FModel.getMagicDb().getCommonCards().getAllCards(), filter));
        final List<PaperCard> cards = new ArrayList<>();

        if (userPrefs != null && userPrefs.grantCompleteSet()) {
            for (PaperCard card : cardPool) {
                cards.add(card);
                cards.add(card);
                cards.add(card);
                cards.add(card);
            }
            return cards;
        }

        final boolean allowDuplicates = userPrefs != null && userPrefs.allowDuplicates();
        final boolean mythicsAvailable = Iterables.any(cardPool, Presets.IS_MYTHIC_RARE);
        final int numMythics = mythicsAvailable ? numRares / RARES_PER_MYTHIC : 0;
        final int adjustedRares = numRares - numMythics;

        final List<Predicate<CardRules>> colorFilters = getColorFilters(userPrefs, cardPool);

        cards.addAll(
                BoosterUtils.generateCards(cardPool, Presets.IS_COMMON, numCommons, colorFilters, allowDuplicates));
        cards.addAll(BoosterUtils.generateCards(cardPool, Presets.IS_UNCOMMON, numUncommons, colorFilters,
                allowDuplicates));
        cards.addAll(BoosterUtils.generateCards(cardPool, Presets.IS_RARE, adjustedRares, colorFilters,
                allowDuplicates));

        if (numMythics > 0) {
            cards.addAll(BoosterUtils.generateCards(cardPool, Presets.IS_MYTHIC_RARE, numMythics, colorFilters,
                    allowDuplicates));
        }

        return cards;

    }

    private static List<Predicate<CardRules>> getColorFilters(final StartingPoolPreferences userPrefs,
            final List<PaperCard> cardPool) {

        final List<Predicate<CardRules>> colorFilters = new ArrayList<>();

        if (userPrefs != null) {

            boolean includeArtifacts = userPrefs.includeArtifacts();

            final List<Byte> preferredColors = userPrefs.getPreferredColors();

            switch (userPrefs.getPoolType()) {

            case RANDOM_BALANCED:
                preferredColors.clear();
                int numberOfColors = COLOR_COUNT_PROBABILITIES[(int) (Math.random()
                        * COLOR_COUNT_PROBABILITIES.length)];
                if (numberOfColors < 6) {
                    Collections.shuffle(possibleColors);
                    for (int i = 0; i < numberOfColors; i++) {
                        preferredColors.add(possibleColors.get(i));
                    }
                } else {
                    preferredColors.addAll(possibleColors);
                }
                includeArtifacts = Math.random() < 0.5;
            case BALANCED:
                populateBalancedFilters(colorFilters, preferredColors, cardPool, includeArtifacts);
                break;
            case RANDOM:
                populateRandomFilters(colorFilters);
                break;

            }

        }

        return colorFilters;

    }

    private static void populateRandomFilters(final List<Predicate<CardRules>> colorFilters) {

        for (int i = 0; i < MAX_BIAS; i++) {
            Predicate<CardRules> predicate;
            byte color = possibleColors.get((int) (Math.random() * 6));
            if (Math.random() < 0.6) {
                predicate = CardRulesPredicates.isMonoColor(color);
            } else {
                predicate = CardRulesPredicates.hasColor(color);
            }
            if (Math.random() < 0.1) {
                predicate = Predicates.and(predicate, CardRulesPredicates.Presets.IS_MULTICOLOR);
            }
            colorFilters.add(predicate);
        }

    }

    private static void populateBalancedFilters(final List<Predicate<CardRules>> colorFilters,
            final List<Byte> preferredColors, final List<PaperCard> cardPool, final boolean includeArtifacts) {

        final List<Byte> otherColors = new ArrayList<>(possibleColors);
        otherColors.removeAll(preferredColors);

        int colorBias = FModel.getQuestPreferences().getPrefInt(QPref.STARTING_POOL_COLOR_BIAS);
        double preferredBias = 0;

        if (preferredColors.isEmpty()) {
            colorBias = 0;
        } else {
            preferredBias = (double) colorBias / preferredColors.size();
        }

        int usedMulticolor = 0, usedPhyrexian = 0;

        for (int i = 0; i < MAX_BIAS; i++) {

            if (i < colorBias) {

                int index = (int) ((double) i / preferredBias);
                for (@SuppressWarnings("unused")
                Byte ignored : otherColors) {

                    //Add artifacts here if there's no colorless selection
                    if (i % 8 == 0 && !preferredColors.contains(MagicColor.COLORLESS) && includeArtifacts) {
                        colorFilters.add(CardRulesPredicates.Presets.IS_ARTIFACT);
                    } else if (i % 5 == 0) {

                        //If colorless is the only color selected, add a small chance to get Phyrexian mana cost cards.
                        if (preferredColors.contains(MagicColor.COLORLESS) && preferredColors.size() == 1) {

                            Predicate<CardRules> predicateRules = CardRulesPredicates.cost(StringOp.CONTAINS_IC,
                                    "p/");
                            Predicate<PaperCard> predicateCard = Predicates.compose(predicateRules,
                                    PaperCard.FN_GET_RULES);

                            int size = Iterables.size(Iterables.filter(cardPool, predicateCard));
                            int totalSize = cardPool.size();

                            double phyrexianAmount = (double) size / totalSize;
                            phyrexianAmount *= 125;

                            if (usedPhyrexian < Math.min(1, phyrexianAmount)) {
                                colorFilters.add(predicateRules);
                                usedPhyrexian++;
                                continue;
                            }

                        }

                        //Try to get multicolored cards that fit into the preferred colors.
                        Predicate<CardRules> predicateRules = Predicates.and(
                                CardRulesPredicates.isColor(preferredColors.get(index)),
                                CardRulesPredicates.Presets.IS_MULTICOLOR);
                        Predicate<PaperCard> predicateCard = Predicates.compose(predicateRules,
                                PaperCard.FN_GET_RULES);

                        //Adjust for the number of multicolored possibilities. This prevents flooding of non-selected
                        //colors if multicolored cards aren't in the selected sets. The more multi-colored cards in the
                        //sets, the more that will be selected.
                        if (usedMulticolor / 8 < Iterables.size(Iterables.filter(cardPool, predicateCard))) {
                            colorFilters.add(predicateRules);
                            usedMulticolor++;
                        } else {
                            //Exceeded multicolor-specific ratio, so here we add a more generic filter.
                            colorFilters.add(CardRulesPredicates.isColor(preferredColors.get(index)));
                        }

                    } else {
                        colorFilters.add(CardRulesPredicates.isMonoColor(preferredColors.get(index)));
                    }
                }

            } else {

                for (Byte color : otherColors) {
                    if (i % 6 == 0) {
                        colorFilters.add(Predicates.and(CardRulesPredicates.isColor(color),
                                CardRulesPredicates.Presets.IS_MULTICOLOR));
                    } else {
                        colorFilters.add(CardRulesPredicates.isMonoColor(color));
                    }
                }

            }

        }

    }

    /**
     * Create the list of card names at random from the given pool.
     *
     * @param source
     *            an Iterable<CardPrinted>
     * @param filter
     *            Predicate<CardPrinted>
     * @param cntNeeded
     *            an int
     * @param allowedColors
     *            a List<Predicate<CardRules>>
     * @param allowDuplicates
     *            If true, multiple copies of the same card will be allowed to be generated.
     * @return a list of card names
     */
    private static List<PaperCard> generateCards(final Iterable<PaperCard> source,
            final Predicate<PaperCard> filter, final int cntNeeded, final List<Predicate<CardRules>> allowedColors,
            final boolean allowDuplicates) {

        //If color is null, use colorOrder progression to grab cards
        final List<PaperCard> result = new ArrayList<>();

        final int size = allowedColors == null ? 0 : allowedColors.size();
        if (allowedColors != null) {
            Collections.shuffle(allowedColors);
        }

        int cntMade = 0, iAttempt = 0;

        //This will prevent endless loop @ wh
        int allowedMisses = (size + 4) * cntNeeded;
        int nullMisses = 0;

        while (cntMade < cntNeeded && allowedMisses > 0) {
            PaperCard card = null;

            if (size > 0) {
                final Predicate<CardRules> color2 = allowedColors.get(iAttempt % size);
                int colorMisses = 0;
                //Try a few times to get a card using the available filter. This is important for sets with only a small
                //handful of multi-colored cards.
                do {
                    if (color2 != null) {
                        Predicate<PaperCard> color2c = Predicates.compose(color2, PaperCard.FN_GET_RULES);
                        card = Aggregates.random(Iterables.filter(source, Predicates.and(filter, color2c)));
                    }
                } while (card == null && colorMisses++ < 10);
            }

            if (card == null) {
                //We can't decide on a color. We're going to try very hard to pick a color within the current filters.
                if (nullMisses++ < 10) {
                    iAttempt++;
                    continue;
                }
                nullMisses = 0;
                //Still no luck. We're going to skip generating this card. This will very, very rarely result in fewer
                //cards than expected; however, it will keep unselected colors out of the pool.
            }

            if ((card != null) && (allowDuplicates || !result.contains(card))) {
                result.add(card);
                cntMade++;
            } else {
                allowedMisses--;
            }
            iAttempt++;
        }

        return result;
    }

    /**
     * Parse a limitation for a reward or chosen card.
     * @param input
     *      String, the limitation as text.
     * @return Predicate<CardRules> the text parsed into a CardRules predicate.
     *
     */
    public static Predicate<CardRules> parseRulesLimitation(final String input) {
        if (null == input || "random".equalsIgnoreCase(input)) {
            return Predicates.alwaysTrue();
        }

        if (input.equalsIgnoreCase("black"))
            return CardRulesPredicates.Presets.IS_BLACK;
        if (input.equalsIgnoreCase("blue"))
            return CardRulesPredicates.Presets.IS_BLUE;
        if (input.equalsIgnoreCase("green"))
            return CardRulesPredicates.Presets.IS_GREEN;
        if (input.equalsIgnoreCase("red"))
            return CardRulesPredicates.Presets.IS_RED;
        if (input.equalsIgnoreCase("white"))
            return CardRulesPredicates.Presets.IS_WHITE;
        if (input.equalsIgnoreCase("colorless"))
            return CardRulesPredicates.Presets.IS_COLORLESS;
        if (input.equalsIgnoreCase("multicolor"))
            return CardRulesPredicates.Presets.IS_MULTICOLOR;

        if (input.equalsIgnoreCase("land"))
            return CardRulesPredicates.Presets.IS_LAND;
        if (input.equalsIgnoreCase("creature"))
            return CardRulesPredicates.Presets.IS_CREATURE;
        if (input.equalsIgnoreCase("artifact"))
            return CardRulesPredicates.Presets.IS_ARTIFACT;
        if (input.equalsIgnoreCase("planeswalker"))
            return CardRulesPredicates.Presets.IS_PLANESWALKER;
        if (input.equalsIgnoreCase("instant"))
            return CardRulesPredicates.Presets.IS_INSTANT;
        if (input.equalsIgnoreCase("sorcery"))
            return CardRulesPredicates.Presets.IS_SORCERY;
        if (input.equalsIgnoreCase("enchantment"))
            return CardRulesPredicates.Presets.IS_ENCHANTMENT;

        throw new IllegalArgumentException("No CardRules limitations could be parsed from: " + input);
    }

    /**
     * parseReward - used internally to parse individual items in a challenge reward definition.
     * @param s
     *      String, the reward to parse
     * @return List<CardPrinted>
     */
    private static List<InventoryItem> parseReward(final String s) {

        String[] temp = s.split(" ");
        List<InventoryItem> rewards = new ArrayList<>();

        // last word starts with 'rare' ignore case
        if (temp.length > 1 && temp[temp.length - 1].regionMatches(true, 0, "rare", 0, 4)) {
            // Type 1: 'n [color] rares'
            final int qty = Integer.parseInt(temp[0]);

            List<Predicate<PaperCard>> preds = new ArrayList<>();
            preds.add(IPaperCard.Predicates.Presets.IS_RARE_OR_MYTHIC); // Determine rarity

            if (temp.length > 2) {
                Predicate<CardRules> cr = parseRulesLimitation(temp[1]);
                //noinspection RedundantCast
                if (Predicates.alwaysTrue() != (Object) cr) { // guava has a single instance for always-const predicates
                    preds.add(Predicates.compose(cr, PaperCard.FN_GET_RULES));
                }
            }

            if (FModel.getQuest().getFormat() != null) {
                preds.add(FModel.getQuest().getFormat().getFilterPrinted());
            }

            PrintSheet ps = new PrintSheet("Quest rewards");
            Predicate<PaperCard> predicate = preds.size() == 1 ? preds.get(0) : Predicates.and(preds);
            ps.addAll(Iterables.filter(FModel.getMagicDb().getCommonCards().getAllCards(), predicate));
            rewards.addAll(ps.random(qty, true));
        } else if (temp.length == 2 && temp[0].equalsIgnoreCase("duplicate") && temp[1].equalsIgnoreCase("card")) {
            // Type 2: a duplicate card of the players choice
            rewards.add(new QuestRewardCardDuplicate());
        } else if (temp.length >= 2 && temp[0].equalsIgnoreCase("chosen") && temp[1].equalsIgnoreCase("card")) {
            // Type 3: a duplicate card of the players choice
            rewards.add(new QuestRewardCardFiltered(temp));
        } else if (temp.length >= 3 && temp[0].equalsIgnoreCase("booster") && temp[1].equalsIgnoreCase("pack")) {
            // Type 4: a predetermined extra booster pack
            rewards.add(BoosterPack.FN_FROM_SET.apply(FModel.getMagicDb().getEditions().get(temp[2])));
        } else if (temp.length >= 3 && temp[0].equalsIgnoreCase("tournament") && temp[1].equalsIgnoreCase("pack")) {
            // Type 5: a predetermined extra tournament ("starter") pack
            rewards.add(TournamentPack.FN_FROM_SET.apply(FModel.getMagicDb().getEditions().get(temp[2])));
        } else if (temp.length > 0) {
            // default: assume we are asking for a single copy of a specific card
            final PaperCard specific = FModel.getMagicDb().getCommonCards().getCard(s);
            if (specific != null) {
                rewards.add(specific);
            }
        }
        // Return the duplicate, a specified card, or an empty list
        return rewards;
    }

    /**
     * <p>
     * generateCardRewardList.
     * </p>
     * Takes a reward list string, parses, and returns list of cards rewarded.
     *
     * @param s
     *            Properties string of reward (97 multicolor rares)
     * @return List<CardPrinted>
     */
    public static List<InventoryItem> generateCardRewardList(final String s) {

        if (StringUtils.isBlank(s)) {
            return null;
        }

        final String[] items = s.split(";");
        final List<InventoryItem> rewards = new ArrayList<>();

        for (final String item : items) {

            String input = null;

            if (item.contains("%")) {
                String[] tmp = item.split("%");
                final int chance = Integer.parseInt(tmp[0].trim());
                if (chance > 0 && tmp.length > 1 && MyRandom.percentTrue(chance)) {
                    input = tmp[1].trim();
                }
            } else {
                input = item;
            }
            if (input != null) {
                List<InventoryItem> reward = parseReward(input);

                if (reward != null) {
                    rewards.addAll(reward);
                }
            }
        }

        return rewards;
    }

    public static void sort(List<PaperCard> cards) {
        //sort cards alphabetically so colors appear together and rares appear on top
        Collections.sort(cards, new Comparator<PaperCard>() {
            @Override
            public int compare(PaperCard c1, PaperCard c2) {
                return c1.getName().compareTo(c2.getName());
            }
        });
        Collections.sort(cards, new Comparator<PaperCard>() {
            @Override
            public int compare(PaperCard c1, PaperCard c2) {
                return c1.getRules().getColor().compareTo(c2.getRules().getColor());
            }
        });
        Collections.sort(cards, new Comparator<PaperCard>() {
            @Override
            public int compare(PaperCard c1, PaperCard c2) {
                return c2.getRarity().compareTo(c1.getRarity());
            }
        });
    }
}