nl.spellenclubeindhoven.dominionshuffle.data.CardSelector.java Source code

Java tutorial

Introduction

Here is the source code for nl.spellenclubeindhoven.dominionshuffle.data.CardSelector.java

Source

/*
 * Copyright (C) 2011 Tim Kramp
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package nl.spellenclubeindhoven.dominionshuffle.data;

import android.content.Context;
import android.preference.PreferenceManager;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import nl.spellenclubeindhoven.dominionshuffle.R;
import nl.spellenclubeindhoven.dominionshuffle.SettingsActivity;

public class CardSelector {
    // Static Constants
    private final static int DEFAULT_CARDS_TO_DRAW = 10;
    private final static String EVENT = "Event";
    private final static String LANDMARK = "Landmark";
    private final static String COST_2_Group = "Cost_2";
    private final static String COST_3_Group = "Cost_3";
    private final static String POTION_CARD = "Potion";
    private final static String POTION_COST = "P";
    private final static String PROSPERITY_GROUP = "Prosperity";
    private final static String COLONY_CARD = "Colony";
    private final static String PLATINUM_CARD = "Platinum";
    private final static String SHELTER_CARD = "Shelter";
    private final static String OBELISK_CARD = "Obelisk";
    private final static String YOUNG_WITCH_CARD = "Young_Witch";
    private final static String DARK_AGES_GROUP = "Dark_Ages";
    private final static String LOOTER_GROUP = "Looter";
    private final static String RUINS_CARD = "Ruins";
    private final static String ACTION_TYPE = "Action";

    // Final Class variables
    final private Context context;
    final private Set<Group> includedGroups = new HashSet<>();
    final private Set<Group> excludedGroups = new HashSet<>();
    final private Set<Card> includedCards = new HashSet<>();
    final private Set<Card> excludedCards = new HashSet<>();
    final private Set<Card> requiredCards = new HashSet<>();
    final private Map<Group, Limit> allLimits = new HashMap<>();
    final private Random random = new Random();

    // Dynamic Class variables
    private SortedSet<Limit> minimumLimits;
    private Data data;

    public CardSelector(Context context) {
        this.context = context;
    }

    private static JSONArray createJSONArray(Collection<?> list) {
        LinkedList<String> result = new LinkedList<String>();
        for (Object item : list) {
            if (item instanceof GroupOrCard) {
                result.add(((GroupOrCard) item).getName());
            } else {
                result.add(item.toString());
            }
        }
        return new JSONArray(result);
    }

    public Result generate(Data data) throws SolveError {
        this.data = data;

        Set<Card> availableCards = new HashSet<>();

        // Add all included groups
        for (Group group : includedGroups) {
            availableCards.addAll(group.getCards());
        }

        // Excluded groups take priority over included groups
        for (Group group : excludedGroups) {
            availableCards.removeAll(group.getCards());
        }

        // Specifically excluded Cards have higher priority over groups
        availableCards.removeAll(excludedCards);

        // Specifically included Cards are highest priority
        availableCards.addAll(includedCards);

        // Sort Minimum Limits as we want smallest first
        minimumLimits = new TreeSet<>(new Comparator<Limit>() {
            public int compare(Limit a1, Limit a2) {
                return a1.getGroup().getCards().size() - a2.getGroup().getCards().size();
            }
        });
        for (Limit limit : allLimits.values()) {
            if (limit.getMinimum() > 0) {
                minimumLimits.add(limit);
            }
        }

        Result result = new Result();

        // First add required Cards
        for (Card card : requiredCards) {
            result.getCards().add(card);
        }

        // Generate a Solution
        result = generateSolution(result, availableCards);

        // We have a valid solution in terms of draw cards
        // Now check for and apply rules for specific cards
        //addPotionIfNeeded(result);
        drawColonyPlatinum(result);
        drawShelter(result);
        drawObelisk(result);

        return result;
    }

    private Result generateSolution(final Result existingResult, final Set<Card> existingAvailableCards)
            throws SolveError {
        Log.d(CardSelector.class.getSimpleName(), "Current Card List: " + existingResult);

        // Check to ensure most recent card added hasn't taken us over any maximum limits
        for (final Limit limit : allLimits.values()) {
            if (!limit.maximumSatisified(existingResult.getCards())) {
                throw new SolveError(R.string.solveerror_unsatisfied_rule, "");
            }
        }

        // Check if we have completed the Solution
        if (countDrawCards(existingResult.getCards(), existingResult) == getCardsToDraw()) {
            // We've drawn the correct number of cards, now check our limits
            if (checkLimitsSatisfied(existingResult)) {
                // First add the Bane card if needed.
                addBaneIfNeeded(existingResult, existingAvailableCards);
                // This is a valid Solution!
                return existingResult;
            } else {
                // Not a valid Solution.
                throw new SolveError(R.string.solveerror_unsatisfied_rule, "");
            }
        }

        // Loop trying cards, and seeing if having picked that card we can complete a Solution
        final Set<Card> availableCards = new HashSet<>(existingAvailableCards);
        while (true) { // Loop until we either return, or throw an exception

            // Pick the next card
            final Result result = pickCard(existingResult, availableCards);

            // Try generating more of the Solution with this card
            try {
                return generateSolution(result, availableCards);
            } catch (SolveError solveError) {
                // Nope, continue loop to try next card
            }
        }
    }

    private boolean checkLimitsSatisfied(final Result result) {
        for (final Limit limit : allLimits.values()) {
            if (!limit.isSatisfied(result.getCards())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Pick a Card and remove it from the availableCards List.<br/>
     * If we have any minimumLimit, pick from the smallest (first in list) to satisfy it.
     */
    private Result pickCard(final Result existingResult, final Set<Card> availableCards) throws SolveError {
        Set<Card> pickSource = availableCards;

        // Check to see if there's still a minimumLimit to satisfy
        for (final Limit minimumLimit : minimumLimits) {
            if (!minimumLimit.minimumSatisfied(existingResult.getCards())) {
                pickSource = new HashSet<>(availableCards);
                pickSource.retainAll(minimumLimit.getGroup().getCards());
                break;
            }
        }

        if (pickSource.isEmpty()) {
            throw new SolveError(R.string.solveerror_rules_to_strict,
                    "Can not select cards using given rules, try relaxing them");
        }

        // Randomise selection from the set - Iterating through isn't very efficient, but without writing a combined List Set class ourself is the easiest choice.
        final int cardToFetch = this.random.nextInt(pickSource.size());
        final Iterator<Card> iterator = pickSource.iterator();
        for (int i = 0; i < cardToFetch; i++) {
            iterator.next();
        }
        final Card pickedCard = iterator.next();

        // Remove from the availableList
        availableCards.remove(pickedCard);

        Result result = new Result(existingResult);
        result.addCard(pickedCard);

        return result;
    }

    /**
     * Counts the cards, excluding those that don't count against the draw limit.<br/>
     * E.g. Events, Basic Cards, Non-Supply Cards
     */
    private int countDrawCards(final Collection<Card> cards, final Result result) {
        int count = 0;
        for (final Card card : cards) {
            if (!card.isBasicOrNonSupply() && !card.getTypes().contains(EVENT)
                    && !card.getTypes().contains(LANDMARK) && !(result.getBaneCard() == card)) {
                count++;
            }
        }
        return count;
    }

    /**
     * If either Colony or Platinum have been added, add the other (if not excluded).<br/>
     * If they're not both excluded
     * Checks the number of prosperity cards to the total drawn, and randomly draws the Colony and Platinum based on the ratio
     */
    private void drawColonyPlatinum(final Result result) {
        final Card colony = data.getCard(COLONY_CARD);
        final Card platinum = data.getCard(PLATINUM_CARD);
        final boolean colonyExcluded = this.excludedCards.contains(colony);
        final boolean platinumExcluded = this.excludedCards.contains(platinum);

        // If they have both been explicitly excluded, do nothing
        if (colonyExcluded && platinumExcluded) {
            return;
        }

        final boolean colonyAlreadyAdded = result.getCards().contains(colony);
        final boolean platinumAlreadyAdded = result.getCards().contains(platinum);

        if (colonyAlreadyAdded) {
            if (platinumAlreadyAdded) {
                // We already have both, do nothing
                return;
            }
            if (!platinumExcluded) {
                // If we have Colony, and Platinum isn't excluded, add it.
                result.addCard(platinum);
            }
            return;
        }
        if (platinumAlreadyAdded) {
            if (!colonyExcluded) {
                // If we have Platinum, and Colony isn't excluded, add it.
                result.addCard(colony);
            }
            return;
        }

        // We have neither Colony nor Platinum, do calculation to determine if we should add them

        // Count the number of prosperity cards
        Group prosperityCards = data.getGroup(PROSPERITY_GROUP);
        int count = 0;
        for (Card card : result.getCards()) {
            if (prosperityCards.contains(card)) {
                count++;
            }
        }

        // Randomly determine if prosperity cards should be added
        if (random.nextInt(getCardsToDraw()) < count) {
            // Add the Colony and Platinum, where they're not excluded.
            if (!colonyExcluded) {
                result.addCard(colony);
            }
            if (!platinumExcluded) {
                result.addCard(platinum);
            }
        }
    }

    /**
     * Check the current results to determine if need to pick a Bane
     */
    private void addBaneIfNeeded(final Result result, final Set<Card> availableCards) throws SolveError {
        // Firstly have we already done the bane card
        if (result.getBaneCard() != null) {
            return;
        }

        // Check if Young Witch is in the selection
        boolean youngWitchInSelection = false;
        Card youngWitch = data.getCard(YOUNG_WITCH_CARD);
        for (Card card : result.getCards()) {
            if (card == youngWitch) {
                youngWitchInSelection = true;
                break;
            }
        }
        if (!youngWitchInSelection) {
            // Young Witch isn't in selection, don't do anything.
            return;
        }

        final Group cost2 = data.getGroup(COST_2_Group);
        final Group cost3 = data.getGroup(COST_3_Group);
        final Set<Card> cost2Or3Cards = new HashSet<>(cost2.getCards());
        cost2Or3Cards.addAll(cost3.getCards());
        cost2Or3Cards.retainAll(availableCards);

        // Filter out Basic and Non-Supply Cards
        // While it means looping through again before we find our random card, we have to so we know how many to count from.
        for (Iterator<Card> i = cost2Or3Cards.iterator(); i.hasNext();) {
            if (i.next().isBasicOrNonSupply()) {
                i.remove();
            }
        }

        if (cost2Or3Cards.isEmpty()) {
            throw new SolveError(R.string.solveerror_not_enough_cards,
                    "Not enough cost 2 or 3 cards for selecting a bane card");
        }

        // Randomise selection from the set
        final int cardToFetch = this.random.nextInt(cost2Or3Cards.size());
        final Iterator<Card> iterator = cost2Or3Cards.iterator();
        for (int i = 0; i < cardToFetch; i++) {
            iterator.next();
        }
        final Card baneCard = iterator.next();

        result.setBaneCard(baneCard);
        result.addCard(baneCard);

        availableCards.remove(baneCard);
    }

    /**
     * Shelter is actually a type of Basic cards, but we treat and display it as a single card for convenience.
     */
    private void drawShelter(Result result) {
        final Card shelter = data.getCard(SHELTER_CARD);

        // Check if it was already a required card
        if (result.getCards().contains(shelter)) {
            return;
        }

        // Count the number of dark ages cards
        Group darkAgesCards = data.getGroup(DARK_AGES_GROUP);
        int count = 0;
        for (Card card : result.getCards()) {
            if (darkAgesCards.contains(card)) {
                count++;
            }
        }

        // Randomly determine if shelter cards should be added
        if (random.nextInt(getCardsToDraw()) < count) {
            result.addCard(shelter);
        }
    }

    /**
     * Check if we need to identify a card for the Obelisk Landmark
     */
    private void drawObelisk(Result result) {
        final Card obelisk = data.getCard(OBELISK_CARD);

        // If we don't have obelisk, just return
        if (!result.getCards().contains(obelisk)) {
            return;
        }

        // Create a list of only action cards from the result
        List<Card> actionCards = new ArrayList<>();
        for (Card card : result.getCards()) {
            if (card.getTypes().contains(ACTION_TYPE)) {
                actionCards.add(card);
            }
        }

        if (actionCards.isEmpty()) {
            // Wierd, but technically possible - means they will have the obelisk card in play, but it has no effect on the game (under rules at the time of this).
            return;
        }

        final int cardToFetch = this.random.nextInt(actionCards.size());
        result.setObeliskCard(actionCards.get(cardToFetch));
    }

    private int getCardsToDraw() {
        String cardsToDraw = PreferenceManager.getDefaultSharedPreferences(this.context)
                .getString(SettingsActivity.CARDS_TO_DRAW, null);
        if (cardsToDraw == null) {
            return DEFAULT_CARDS_TO_DRAW;
        }
        return Integer.parseInt(cardsToDraw);
    }

    public void addIncludedGroup(Group group) {
        includedGroups.add(group);
    }

    public void removeIncludedGroup(Group group) {
        includedGroups.remove(group);
    }

    public boolean hasIncludedGroup(Group group) {
        return includedGroups.contains(group);
    }

    public void addExcludedGroup(Group group) {
        excludedGroups.add(group);
    }

    public void removeExcludedGroup(Group group) {
        excludedGroups.remove(group);
    }

    public boolean hasExcludedGroup(Group group) {
        return excludedGroups.contains(group);
    }

    public void addIncludedCard(Card card) {
        includedCards.add(card);
    }

    public void removeIncludedCard(Card card) {
        includedCards.remove(card);
    }

    public boolean hasIncludedCard(Card card) {
        return includedCards.contains(card);
    }

    public void addExcludedCard(Card card) {
        excludedCards.add(card);
    }

    public void removeExcludedCard(Card cards) {
        excludedCards.remove(cards);
    }

    public boolean hasExlcudedCard(Card card) {
        return excludedCards.contains(card);
    }

    public void addRequiredCard(Card card) {
        requiredCards.add(card);
    }

    public void removeRequiredCard(Card card) {
        requiredCards.remove(card);
    }

    public boolean hasRequiredCard(Card card) {
        return requiredCards.contains(card);
    }

    public void removeLimit(Group group) {
        allLimits.remove(group);
    }

    public Limit getLimit(Group group) {
        if (!allLimits.containsKey(group)) {
            allLimits.put(group, new Limit(group));
        }

        return allLimits.get(group);
    }

    public boolean hasLimit(Group group) {
        return allLimits.containsKey(group);
    }

    public void clear() {
        includedGroups.clear();
        excludedGroups.clear();
        includedCards.clear();
        excludedCards.clear();
        requiredCards.clear();
        allLimits.clear();
    }

    public void cycleIncludeExclude(Object cardOrGroup) {
        if (cardOrGroup instanceof Card) {
            cycleIncludeExclude((Card) cardOrGroup);
        } else if (cardOrGroup instanceof Group) {
            cycleIncludeExclude((Group) cardOrGroup);
        }
    }

    private void cycleIncludeExclude(Card card) {
        if (hasIncludedCard(card)) {
            removeIncludedCard(card);
            addExcludedCard(card);
        } else if (hasExlcudedCard(card)) {
            removeExcludedCard(card);
        } else {
            addIncludedCard(card);
        }
    }

    private void cycleIncludeExclude(Group group) {
        if (hasIncludedGroup(group)) {
            removeIncludedGroup(group);
            addExcludedGroup(group);
        } else if (hasExcludedGroup(group)) {
            removeExcludedGroup(group);
        } else {
            addIncludedGroup(group);
        }
    }

    public void cycleRequireExclude(Card card) {
        if (hasRequiredCard(card)) {
            removeRequiredCard(card);
            removeExcludedCard(card);
        } else if (hasExlcudedCard(card)) {
            removeExcludedCard(card);
            addRequiredCard(card);
        } else {
            addExcludedCard(card);
            removeRequiredCard(card);
        }
    }

    public int getLimitMinimum(Group group) {
        if (hasLimit(group)) {
            return getLimit(group).getMinimum();
        } else {
            return 0;
        }
    }

    // Will map MAX_VALUE back to 0
    public int getLimitMaximum(Group group) {
        if (hasLimit(group)) {
            Limit limit = getLimit(group);
            if (limit.getMaximum() == Integer.MAX_VALUE) {
                return 0;
            } else {
                return limit.getMaximum();
            }
        } else {
            return 0;
        }
    }

    public GroupOrCard getCondition(Group group) {
        if (hasLimit(group)) {
            Limit limit = getLimit(group);
            return limit.getCondition();
        } else {
            return null;
        }
    }

    public void setLimitMinimum(Group group, int which) {
        if (which == 0 && !hasLimit(group)) {
            return;
        }
        Limit limit = getLimit(group);
        limit.setMinimum(which);
        removeLimitIfUseless(group);
    }

    // Will map the value 0 to MAX_VALUE
    public void setLimitMaximum(Group group, int which) {
        if (which == 0 && !hasLimit(group)) {
            return;
        }
        if (which == 0) {
            which = Integer.MAX_VALUE;
        }
        Limit limit = getLimit(group);
        limit.setMaximum(which);
        removeLimitIfUseless(group);
    }

    public void removeLimitIfUseless(Group group) {
        if (hasLimit(group)) {
            Limit limit = getLimit(group);
            if (limit.getMinimum() == 0 && limit.getMaximum() == Integer.MAX_VALUE
                    && limit.getCondition() == null) {
                removeLimit(group);
            }
        }
    }

    public void setCondition(Group group, GroupOrCard groupOrCard) {
        if (groupOrCard == null || groupOrCard instanceof Nothing && !hasLimit(group)) {
            return;
        }
        Limit limit = getLimit(group);
        if (groupOrCard instanceof Nothing) {
            limit.setCondition(null);
        } else {
            limit.setCondition(groupOrCard);
        }
        removeLimitIfUseless(group);
    }

    public void fromJson(String json, Data data) throws JSONException {
        JSONObject root = new JSONObject(json);
        JSONArray array;

        includedCards.clear();
        array = root.getJSONArray("includedCards");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                Card card = data.getCard(array.getString(i));
                if (card != null) {
                    includedCards.add(card);
                }
            }
        }

        excludedCards.clear();
        array = root.getJSONArray("excludedCards");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                Card card = data.getCard(array.getString(i));
                if (card != null) {
                    excludedCards.add(card);
                }
            }
        }

        requiredCards.clear();
        array = root.getJSONArray("requiredCards");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                Card card = data.getCard(array.getString(i));
                if (card != null) {
                    requiredCards.add(card);
                }
            }
        }

        includedGroups.clear();
        array = root.getJSONArray("includedGroups");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                Group group = data.getGroup(array.getString(i));
                if (group != null) {
                    includedGroups.add(group);
                }
            }
        }

        excludedGroups.clear();
        array = root.getJSONArray("excludedGroups");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                Group group = data.getGroup(array.getString(i));
                if (group != null) {
                    excludedGroups.add(group);
                }
            }
        }

        allLimits.clear();
        array = root.getJSONArray("rules");
        if (array != null) {
            for (int i = 0; i < array.length(); i++) {
                JSONObject jsonLimit = array.getJSONObject(i);
                if (jsonLimit == null) {
                    continue;
                }
                Group group = data.getGroup(jsonLimit.getString("group"));
                if (group == null) {
                    continue;
                }
                Limit limit = getLimit(group);
                if (jsonLimit.has("min")) {
                    limit.setMinimum(jsonLimit.getInt("min"));
                }
                if (jsonLimit.has("max")) {
                    limit.setMaximum(jsonLimit.getInt("max"));
                }
                if (jsonLimit.has("condition")) {
                    limit.setCondition(data.getGroupOrCard(jsonLimit.getString("condition")));
                }
                allLimits.put(group, limit);
            }
        }
    }

    public String toJson() throws JSONException {
        JSONObject root = new JSONObject();

        root.put("includedCards", createJSONArray(includedCards));
        root.put("excludedCards", createJSONArray(excludedCards));
        root.put("requiredCards", createJSONArray(requiredCards));
        root.put("includedGroups", createJSONArray(includedGroups));
        root.put("excludedGroups", createJSONArray(excludedGroups));

        JSONArray jsonRules = new JSONArray();
        for (Group group : allLimits.keySet()) {
            Limit limit = allLimits.get(group);
            JSONObject jsonLimit = new JSONObject();
            jsonLimit.put("group", group.getName());
            jsonLimit.put("min", limit.getMinimum());
            jsonLimit.put("max", limit.getMaximum());
            if (limit.getCondition() != null) {
                jsonLimit.put("condition", limit.getCondition().getName());
            }
            jsonRules.put(jsonLimit);
        }
        root.put("rules", jsonRules);

        return root.toString();
    }

    public void removeExcludedCard(Collection<Card> cards) {
        for (Card card : cards) {
            removeExcludedCard(card);
        }
    }

    public void removeRequiredCard(Collection<Card> cards) {
        for (Card card : cards) {
            removeRequiredCard(card);
        }
    }

    public void addExcludedCard(Collection<Card> cards) {
        for (Card card : cards) {
            addExcludedCard(card);
        }
    }

    public void addRequiredCard(Collection<Card> cards) {
        for (Card card : cards) {
            addRequiredCard(card);
        }
    }
}