forge.deck.generation.DeckGeneratorBase.java Source code

Java tutorial

Introduction

Here is the source code for forge.deck.generation.DeckGeneratorBase.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.deck.generation;

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.*;
import forge.card.mana.ManaCost;
import forge.deck.CardPool;
import forge.deck.DeckFormat;
import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.ItemPool;
import forge.util.MyRandom;

import org.apache.commons.lang3.tuple.ImmutablePair;

import java.util.*;
import java.util.Map.Entry;

/**
 * <p>
 * Generate2ColorDeck class.
 * </p>
 * 
 * @author Forge
 * @version $Id: Generate2ColorDeck.java 14959 2012-03-28 14:03:43Z Chris H. $
 */
public abstract class DeckGeneratorBase {
    protected final Random r = MyRandom.getRandom();
    protected final Map<String, Integer> cardCounts = new HashMap<String, Integer>();
    protected int maxDuplicates = 4;
    protected boolean useArtifacts = true;

    protected ColorSet colors;
    protected final CardPool tDeck = new CardPool();
    protected final IDeckGenPool pool;
    protected final DeckFormat format;

    // 2-colored deck generator has its own constants. The rest works fine with these ones
    protected float getLandsPercentage() {
        return 0.44f;
    }

    protected float getCreatPercentage() {
        return 0.34f;
    }

    protected float getSpellPercentage() {
        return 0.22f;
    }

    StringBuilder tmpDeck = new StringBuilder();

    public DeckGeneratorBase(IDeckGenPool pool0, DeckFormat format0) {
        pool = format0.getCardPool(pool0);
        format = format0;
    }

    public void setSingleton(boolean singleton) {
        maxDuplicates = singleton ? 1 : 4;
    }

    public void setUseArtifacts(boolean value) {
        useArtifacts = value;
    }

    protected void addCreaturesAndSpells(int size, List<ImmutablePair<FilterCMC, Integer>> cmcLevels,
            boolean forAi) {
        tmpDeck.append("Building deck of ").append(size).append("cards\n");

        final Iterable<PaperCard> cards = selectCardsOfMatchingColorForPlayer(forAi);
        // build subsets based on type

        final Iterable<PaperCard> creatures = Iterables.filter(cards,
                Predicates.compose(CardRulesPredicates.Presets.IS_CREATURE, PaperCard.FN_GET_RULES));
        final int creatCnt = (int) Math.ceil(getCreatPercentage() * size);
        tmpDeck.append("Creatures to add:").append(creatCnt).append("\n");
        addCmcAdjusted(creatures, creatCnt, cmcLevels);

        Predicate<PaperCard> preSpells = Predicates
                .compose(CardRulesPredicates.Presets.IS_NONCREATURE_SPELL_FOR_GENERATOR, PaperCard.FN_GET_RULES);
        final Iterable<PaperCard> spells = Iterables.filter(cards, preSpells);
        final int spellCnt = (int) Math.ceil(getSpellPercentage() * size);
        tmpDeck.append("Spells to add:").append(spellCnt).append("\n");
        addCmcAdjusted(spells, spellCnt, cmcLevels);

        tmpDeck.append(String.format("Current deck size: %d... should be %f%n", tDeck.countAll(),
                size * (getCreatPercentage() + getSpellPercentage())));
    }

    public CardPool getDeck(final int size, final boolean forAi) {
        return null; // all but theme deck do override this method
    }

    protected boolean addSome(int cnt, List<PaperCard> source) {
        int srcLen = source.size();
        if (srcLen == 0) {
            return false;
        }

        for (int i = 0; i < cnt; i++) {
            PaperCard cp;
            int lc = 0;
            do {
                cp = source.get(r.nextInt(srcLen));
                lc++;
            } while (cardCounts.get(cp.getName()) > maxDuplicates - 1 && lc <= 100);

            if (lc > 100) {
                return false;
            }

            tDeck.add(pool.getCard(cp.getName(), cp.getEdition()));

            final int n = cardCounts.get(cp.getName());
            cardCounts.put(cp.getName(), n + 1);
            if (n + 1 == maxDuplicates) {
                if (source.remove(cp)) {
                    srcLen--;
                    if (srcLen == 0) {
                        return false;
                    }
                }
            }
            tmpDeck.append(String.format("(%d) %s [%s]%n", cp.getRules().getManaCost().getCMC(), cp.getName(),
                    cp.getRules().getManaCost()));
        }
        return true;
    }

    protected int addSomeStr(int cnt, List<String> source) {
        int res = 0;
        for (int i = 0; i < cnt; i++) {
            String s;
            int lc = 0;
            do {
                s = source.get(r.nextInt(source.size()));
                lc++;
            } while ((cardCounts.get(s) >= maxDuplicates) && (lc <= 50));
            // not an error if looped too much - could play singleton mode, with 6 slots for 3 non-basic lands.

            if (lc > 50) {
                break;
            }

            tDeck.add(pool.getCard(s));

            final int n = cardCounts.get(s);
            cardCounts.put(s, n + 1);
            tmpDeck.append(s + "\n");
            res++;
        }
        return res;
    }

    protected void addBasicLand(int cnt) {
        addBasicLand(cnt, null);
    }

    protected void addBasicLand(int cnt, String edition) {
        tmpDeck.append(cnt).append(" basic lands remain").append("\n");

        // attempt to optimize basic land counts according to colors of picked cards
        final Map<String, Integer> clrCnts = countLands(tDeck);
        // total of all ClrCnts
        float totalColor = 0;
        for (Entry<String, Integer> c : clrCnts.entrySet()) {
            totalColor += c.getValue();
            tmpDeck.append(c.getKey()).append(":").append(c.getValue()).append("\n");
        }

        tmpDeck.append("totalColor:").append(totalColor).append("\n");

        int landsLeft = cnt;
        for (Entry<String, Integer> c : clrCnts.entrySet()) {
            String basicLandName = c.getKey();

            // calculate number of lands for each color
            final int nLand = Math.min(landsLeft, Math.round(cnt * c.getValue() / totalColor));
            tmpDeck.append("nLand-").append(basicLandName).append(":").append(nLand).append("\n");

            // just to prevent a null exception by the deck size fixing code
            cardCounts.put(basicLandName, nLand);

            PaperCard cp;
            if (edition != null) {
                cp = pool.getCard(basicLandName, edition);
            } else {
                cp = pool.getCard(basicLandName);
            }

            String basicLandSet = cp.getEdition();

            for (int i = 0; i < nLand; i++) {
                tDeck.add(pool.getCard(cp.getName(), basicLandSet, -1), 1);
            }

            landsLeft -= nLand;
        }
    }

    protected void adjustDeckSize(int targetSize) {
        // fix under-sized or over-sized decks, due to integer arithmetic
        int actualSize = tDeck.countAll();
        if (actualSize < targetSize) {
            addSome(targetSize - actualSize, tDeck.toFlatList());
        } else if (actualSize > targetSize) {
            Predicate<PaperCard> exceptBasicLand = Predicates
                    .not(Predicates.compose(CardRulesPredicates.Presets.IS_BASIC_LAND, PaperCard.FN_GET_RULES));

            for (int i = 0; i < 3 && actualSize > targetSize; i++) {
                Iterable<PaperCard> matchingCards = Iterables.filter(tDeck.toFlatList(), exceptBasicLand);
                List<PaperCard> toRemove = Aggregates.random(matchingCards, actualSize - targetSize);
                tDeck.removeAllFlat(toRemove);

                for (PaperCard c : toRemove) {
                    tmpDeck.append("Removed:").append(c.getName()).append("\n");
                }
                actualSize = tDeck.countAll();
            }
        }
    }

    protected void addCmcAdjusted(Iterable<PaperCard> source, int cnt,
            List<ImmutablePair<FilterCMC, Integer>> cmcLevels) {
        int totalWeight = 0;
        for (ImmutablePair<FilterCMC, Integer> pair : cmcLevels) {
            totalWeight += pair.getRight();
        }

        float variability = 0.6f; // if set to 1, you'll get minimum cards to choose from
        float desiredWeight = (float) cnt / (maxDuplicates * variability);
        float desiredOverTotal = desiredWeight / totalWeight;
        float requestedOverTotal = (float) cnt / totalWeight;

        for (ImmutablePair<FilterCMC, Integer> pair : cmcLevels) {
            Iterable<PaperCard> matchingCards = Iterables.filter(source,
                    Predicates.compose(pair.getLeft(), PaperCard.FN_GET_RULES));
            int cmcCountForPool = (int) Math.ceil(pair.getRight().intValue() * desiredOverTotal);

            int addOfThisCmc = Math.round(pair.getRight().intValue() * requestedOverTotal);
            tmpDeck.append(String.format("Adding %d cards for cmc range from a pool with %d cards:%n", addOfThisCmc,
                    cmcCountForPool));

            final List<PaperCard> curved = Aggregates.random(matchingCards, cmcCountForPool);
            final List<PaperCard> curvedRandomized = Lists.newArrayList();
            for (PaperCard c : curved) {
                cardCounts.put(c.getName(), 0);
                curvedRandomized.add(pool.getCard(c.getName()));
            }

            addSome(addOfThisCmc, curvedRandomized);
        }
    }

    protected Iterable<PaperCard> selectCardsOfMatchingColorForPlayer(boolean forAi) {

        // start with all cards
        // remove cards that generated decks don't like
        Predicate<CardRules> canPlay = forAi ? AI_CAN_PLAY : HUMAN_CAN_PLAY;
        Predicate<CardRules> hasColor = new MatchColorIdentity(colors);

        if (useArtifacts) {
            hasColor = Predicates.or(hasColor, COLORLESS_CARDS);
        }
        return Iterables.filter(pool.getAllCards(),
                Predicates.compose(Predicates.and(canPlay, hasColor), PaperCard.FN_GET_RULES));
    }

    protected static Map<String, Integer> countLands(ItemPool<PaperCard> outList) {
        // attempt to optimize basic land counts according
        // to color representation

        Map<String, Integer> res = new TreeMap<String, Integer>();
        // count each card color using mana costs
        // TODO: count hybrid mana differently?
        for (Entry<PaperCard, Integer> cpe : outList) {

            int profile = cpe.getKey().getRules().getManaCost().getColorProfile();

            if ((profile & MagicColor.WHITE) != 0) {
                increment(res, MagicColor.Constant.BASIC_LANDS.get(0), cpe.getValue());
            } else if ((profile & MagicColor.BLUE) != 0) {
                increment(res, MagicColor.Constant.BASIC_LANDS.get(1), cpe.getValue());
            } else if ((profile & MagicColor.BLACK) != 0) {
                increment(res, MagicColor.Constant.BASIC_LANDS.get(2), cpe.getValue());
            } else if ((profile & MagicColor.RED) != 0) {
                increment(res, MagicColor.Constant.BASIC_LANDS.get(3), cpe.getValue());
            } else if ((profile & MagicColor.GREEN) != 0) {
                increment(res, MagicColor.Constant.BASIC_LANDS.get(4), cpe.getValue());
            }

        }
        return res;
    }

    protected static void increment(Map<String, Integer> map, String key, int delta) {
        final Integer boxed = map.get(key);
        map.put(key, boxed == null ? delta : boxed.intValue() + delta);
    }

    public static final Predicate<CardRules> AI_CAN_PLAY = new Predicate<CardRules>() {
        @Override
        public boolean apply(CardRules c) {
            return !c.getAiHints().getRemAIDecks() && !c.getAiHints().getRemRandomDecks();
        }
    };

    public static final Predicate<CardRules> HUMAN_CAN_PLAY = new Predicate<CardRules>() {
        @Override
        public boolean apply(CardRules c) {
            return !c.getAiHints().getRemRandomDecks();
        }
    };

    public static final Predicate<CardRules> COLORLESS_CARDS = new Predicate<CardRules>() {
        @Override
        public boolean apply(CardRules c) {
            ManaCost mc = c.getManaCost();
            return c.getColorIdentity().isColorless() && !mc.isNoCost();
        }
    };

    public static class MatchColorIdentity implements Predicate<CardRules> {
        private final ColorSet allowedColor;

        public MatchColorIdentity(ColorSet color) {
            allowedColor = color;
        }

        @Override
        public boolean apply(CardRules subject) {
            ManaCost mc = subject.getManaCost();
            return !mc.isPureGeneric() && allowedColor.containsAllColorsFrom(subject.getColorIdentity().getColor());
            //return  mc.canBePaidWithAvaliable(allowedColor);
            // return allowedColor.containsAllColorsFrom(mc.getColorProfile());
        }
    }

    public static class FilterCMC implements Predicate<CardRules> {
        private final int min;
        private final int max;

        public FilterCMC(int from, int to) {
            min = from;
            max = to;
        }

        @Override
        public boolean apply(CardRules c) {
            ManaCost mc = c.getManaCost();
            int cmc = mc.getCMC();
            return cmc >= min && cmc <= max && !mc.isNoCost();
        }
    }

    private static Map<Integer, String[]> dualLands = new HashMap<Integer, String[]>();
    static {
        dualLands.put(MagicColor.WHITE | MagicColor.BLUE,
                new String[] { "Tundra", "Hallowed Fountain", "Flooded Strand" });
        dualLands.put(MagicColor.BLACK | MagicColor.BLUE,
                new String[] { "Underground Sea", "Watery Grave", "Polluted Delta" });
        dualLands.put(MagicColor.BLACK | MagicColor.RED,
                new String[] { "Badlands", "Blood Crypt", "Bloodstained Mire" });
        dualLands.put(MagicColor.GREEN | MagicColor.RED,
                new String[] { "Taiga", "Stomping Ground", "Wooded Foothills" });
        dualLands.put(MagicColor.GREEN | MagicColor.WHITE,
                new String[] { "Savannah", "Temple Garden", "Windswept Heath" });

        dualLands.put(MagicColor.WHITE | MagicColor.BLACK,
                new String[] { "Scrubland", "Godless Shrine", "Marsh Flats" });
        dualLands.put(MagicColor.BLUE | MagicColor.RED,
                new String[] { "Volcanic Island", "Steam Vents", "Scalding Tarn" });
        dualLands.put(MagicColor.BLACK | MagicColor.GREEN,
                new String[] { "Bayou", "Overgrown Tomb", "Verdant Catacombs" });
        dualLands.put(MagicColor.WHITE | MagicColor.RED, new String[] { "Plateau", "Sacred Foundry", "Arid Mesa" });
        dualLands.put(MagicColor.GREEN | MagicColor.BLUE,
                new String[] { "Tropical Island", "Breeding Pool", "Misty Rainforest" });
    }

    /**
     * Get list of dual lands for this color combo.
     * 
     * @param color
     *            the color
     * @return dual land names
     */
    protected List<String> getDualLandList() {

        final List<String> dLands = new ArrayList<String>();

        if (colors.countColors() > 3) {
            dLands.add("Rupture Spire");
            dLands.add("Undiscovered Paradise");
        }

        if (colors.countColors() > 2) {
            dLands.add("Evolving Wilds");
            dLands.add("Terramorphic Expanse");
        }
        for (Entry<Integer, String[]> dual : dualLands.entrySet()) {
            if (colors.hasAllColors(dual.getKey())) {
                for (String s : dual.getValue()) {
                    dLands.add(s);
                }
            }
        }

        return dLands;
    }

    /**
     * Get all dual lands that do not match this color combo.
     * 
     * @param color
     *            the color
     * @return dual land names
     */
    protected List<String> getInverseDualLandList() {

        final List<String> dLands = new ArrayList<String>();

        for (Entry<Integer, String[]> dual : dualLands.entrySet()) {
            if (!colors.hasAllColors(dual.getKey())) {
                for (String s : dual.getValue()) {
                    dLands.add(s);
                }
            }
        }

        return dLands;
    }
}