/*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package mage.client.deck.generator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import mage.cards.Card;
import mage.cards.decks.Deck;
import mage.cards.repository.CardCriteria;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionRepository;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.sets.ConstructedFormats;
import mage.constants.CardType;
import mage.constants.ColoredManaSymbol;
import mage.constants.Rarity;
import mage.util.RandomUtil;
import mage.util.TournamentUtil;
/**
* Generates random card pool and builds a deck.
*
* @author nantuko
* @author Simown
*/
public final class DeckGenerator {
public static class DeckGeneratorException extends RuntimeException {
public DeckGeneratorException(String message) {
super(message);
}
}
private static final int MAX_TRIES = 8196;
private static DeckGeneratorDialog genDialog;
private static DeckGeneratorPool genPool;
/**
* Builds a deck out of the selected block/set/format.
*
* @return a path to the generated deck.
*/
public static String generateDeck() {
genDialog = new DeckGeneratorDialog();
if (genDialog.getSelectedColors() != null) {
Deck deck = buildDeck();
return genDialog.saveDeck(deck);
}
// If the deck couldn't be generated or the user cancelled, repopulate the deck selection with its cached value
return PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_TABLE_DECK_FILE, null);
}
protected static Deck buildDeck() {
String selectedColors = genDialog.getSelectedColors();
List<ColoredManaSymbol> allowedColors = new ArrayList<>();
selectedColors = selectedColors != null ? selectedColors.toUpperCase() : getRandomColors("X");
String format = genDialog.getSelectedFormat();
List<String> setsToUse = ConstructedFormats.getSetsByFormat(format);
if (setsToUse == null) {
throw new DeckGeneratorException("Deck sets aren't initialized; please connect to a server to update the database.");
}
if (setsToUse.isEmpty()) {
// Default to using all sets
setsToUse = ExpansionRepository.instance.getSetCodes();
}
int deckSize = genDialog.getDeckSize();
if (selectedColors.contains("X")) {
selectedColors = getRandomColors(selectedColors);
}
for (int i = 0; i < selectedColors.length(); i++) {
char c = selectedColors.charAt(i);
allowedColors.add(ColoredManaSymbol.lookup(c));
}
return generateDeck(deckSize, allowedColors, setsToUse);
}
/**
* If the user has selected random colors, pick them randomly for the user.
*
* @param selectedColors a string of the colors selected.
* @return a String representation of the new colors chosen.
*/
private static String getRandomColors(String selectedColors) {
List<Character> availableColors = new ArrayList<>();
for (ColoredManaSymbol cms : ColoredManaSymbol.values()) {
availableColors.add(cms.toString().charAt(0));
}
StringBuilder generatedColors = new StringBuilder();
int randomColors = 0;
for (int i = 0; i < selectedColors.length(); i++) {
char currentColor = selectedColors.charAt(i);
if (currentColor != 'X') {
generatedColors.append(currentColor);
availableColors.remove(new Character(currentColor));
} else {
randomColors++;
}
}
for (int i = 0; i < randomColors && !availableColors.isEmpty(); i++) {
int index = RandomUtil.nextInt(availableColors.size());
generatedColors.append(availableColors.remove(index));
}
return generatedColors.toString();
}
/**
* Generates all the cards to use in the deck. Adds creatures,
* non-creatures, lands (including non-basic). Fixes the deck, adjusting for
* size and color of the cards retrieved.
*
* @param deckSize how big the deck is to generate.
* @param allowedColors which colors are allowed in the deck.
* @param setsToUse which sets to use to retrieve cards for this deck.
* @return the final deck to use.
*/
private static Deck generateDeck(int deckSize, List<ColoredManaSymbol> allowedColors, List<String> setsToUse) {
genPool = new DeckGeneratorPool(deckSize, genDialog.getCreaturePercentage(), genDialog.getNonCreaturePercentage(),
genDialog.getLandPercentage(), allowedColors, genDialog.isSingleton(), genDialog.isColorless(),
genDialog.isAdvanced(), genDialog.getDeckGeneratorCMC());
final String[] sets = setsToUse.toArray(new String[setsToUse.size()]);
// Creatures
final CardCriteria creatureCriteria = new CardCriteria();
creatureCriteria.setCodes(sets);
creatureCriteria.notTypes(CardType.LAND);
creatureCriteria.types(CardType.CREATURE);
if (!(genDialog.useArtifacts())) {
creatureCriteria.notTypes(CardType.ARTIFACT);
}
// Non-creatures (sorcery, instant, enchantment, artifact etc.)
final CardCriteria nonCreatureCriteria = new CardCriteria();
nonCreatureCriteria.setCodes(sets);
nonCreatureCriteria.notTypes(CardType.LAND);
nonCreatureCriteria.notTypes(CardType.CREATURE);
if (!(genDialog.useArtifacts())) {
nonCreatureCriteria.notTypes(CardType.ARTIFACT);
}
// Non-basic land
final CardCriteria nonBasicLandCriteria = new CardCriteria();
nonBasicLandCriteria.setCodes(sets);
nonBasicLandCriteria.types(CardType.LAND);
nonBasicLandCriteria.notSupertypes("Basic");
// Generate basic land cards
Map<String, List<CardInfo>> basicLands = generateBasicLands(setsToUse);
generateSpells(creatureCriteria, genPool.getCreatureCount());
generateSpells(nonCreatureCriteria, genPool.getNonCreatureCount());
generateLands(nonBasicLandCriteria, genPool.getLandCount(), basicLands);
// Reconstructs the final deck and adjusts for Math rounding and/or missing cards
return genPool.getDeck();
}
/**
* Generates all spells for the deck. Each card is retrieved from the
* database and checked against the converted mana cost (CMC) needed for the
* current card pool. If a card's CMC matches the CMC range required by the
* pool, it is added to the deck. This ensures that the majority of cards
* fit a fixed mana curve for the deck, and it is playable. Creatures and
* non-creatures are retrieved separately to ensure the deck contains a
* reasonable mix of both.
*
* @param criteria the criteria to search for in the database.
* @param spellCount the number of spells that match the criteria needed in
* the deck.
*/
private static void generateSpells(CardCriteria criteria, int spellCount) {
List<CardInfo> cardPool = CardRepository.instance.findCards(criteria);
int retrievedCount = cardPool.size();
List<DeckGeneratorCMC.CMC> deckCMCs = genPool.getCMCsForSpellCount(spellCount);
int count = 0;
int reservesAdded = 0;
boolean added;
if (retrievedCount > 0 && retrievedCount >= spellCount) {
int tries = 0;
while (count < spellCount) {
Card card = cardPool.get(RandomUtil.nextInt(retrievedCount)).getMockCard();
if (genPool.isValidSpellCard(card)) {
int cardCMC = card.getManaCost().convertedManaCost();
for (DeckGeneratorCMC.CMC deckCMC : deckCMCs) {
if (cardCMC >= deckCMC.min && cardCMC <= deckCMC.max) {
int currentAmount = deckCMC.getAmount();
if (currentAmount > 0) {
deckCMC.setAmount(currentAmount - 1);
genPool.addCard(card.copy());
count++;
}
} else if (reservesAdded < (genPool.getDeckSize() / 2)) {
added = genPool.tryAddReserve(card, cardCMC);
if (added) {
reservesAdded++;
}
}
}
}
tries++;
if (tries > MAX_TRIES) {
// Break here, we'll fill in random missing ones later
break;
}
}
} else {
throw new IllegalStateException("Not enough cards to generate deck.");
}
}
/**
* Generates all the lands for the deck. Generates non-basic if selected by
* the user and if the deck isn't monocolored. Will fetch non-basic lands if
* required and then fill up the remaining space with basic lands. Basic
* lands are adjusted according to the mana symbols seen in the cards used
* in this deck. Usually the lands will be well balanced relative to the
* color of cards.
*
* @param criteria the criteria of the lands to search for in the database.
* @param landsCount the amount of lands required for this deck.
* @param basicLands information about the basic lands from the sets used.
*/
private static void generateLands(CardCriteria criteria, int landsCount, Map<String, List<CardInfo>> basicLands) {
int tries = 0;
int countNonBasic = 0;
// Store the nonbasic lands (if any) we'll add
List<Card> deckLands = new ArrayList<>();
// Calculates the percentage of colored mana symbols over all spells in the deck
Map<String, Double> percentage = genPool.calculateSpellColorPercentages();
// Only dual/tri color lands are generated for now, and not non-basic lands that only produce colorless mana.
if (!genPool.isMonoColoredDeck() && genDialog.useNonBasicLand()) {
List<Card> landCards = genPool.filterLands(CardRepository.instance.findCards(criteria));
int allCount = landCards.size();
if (allCount > 0) {
while (countNonBasic < landsCount / 2) {
Card card = landCards.get(RandomUtil.nextInt(allCount));
if (genPool.isValidLandCard(card)) {
Card addedCard = card.copy();
deckLands.add(addedCard);
genPool.addCard(addedCard);
countNonBasic++;
}
tries++;
// to avoid infinite loop
if (tries > MAX_TRIES) {
// Not a problem, just use what we have
break;
}
}
}
}
// Calculate the amount of colored mana already can be produced by the non-basic lands
Map<String, Integer> count = genPool.countManaProduced(deckLands);
// Fill up the rest of the land quota with basic lands adjusted to fit the deck's mana costs
addBasicLands(landsCount - countNonBasic, percentage, count, basicLands);
}
/**
* Returns a map of colored mana symbol to basic land cards of that color.
*
* @param setsToUse which sets to retrieve basic lands from.
* @return a map of color to basic lands.
*/
private static Map<String, List<CardInfo>> generateBasicLands(List<String> setsToUse) {
Set<String> landSets = TournamentUtil.getLandSetCodeForDeckSets(setsToUse);
CardCriteria criteria = new CardCriteria();
if (!landSets.isEmpty()) {
criteria.setCodes(landSets.toArray(new String[landSets.size()]));
}
Map<String, List<CardInfo>> basicLandMap = new HashMap<>();
for (ColoredManaSymbol c : ColoredManaSymbol.values()) {
String landName = DeckGeneratorPool.getBasicLandName(c.toString());
criteria.rarities(Rarity.LAND).name(landName);
List<CardInfo> cards = CardRepository.instance.findCards(criteria);
if (cards.isEmpty()) { // Workaround to get basic lands if lands are not available for the given sets
criteria.setCodes("ORI");
cards = CardRepository.instance.findCards(criteria);
}
basicLandMap.put(landName, cards);
}
return basicLandMap;
}
/**
* Once any non-basic lands are added, add basic lands until the deck is
* filled.
*
* @param landsNeeded how many remaining lands are needed.
* @param percentage the percentage needed for each color in the final deck.
* @param count how many of each color can be produced by non-basic lands.
* @param basicLands list of information about basic lands from the
* database.
*/
private static void addBasicLands(int landsNeeded, Map<String, Double> percentage, Map<String, Integer> count, Map<String, List<CardInfo>> basicLands) {
int colorTotal = 0;
ColoredManaSymbol colorToAdd = ColoredManaSymbol.U;
// Add up the totals for all colors, to keep track of the percentage a color is.
for (Map.Entry<String, Integer> c : count.entrySet()) {
colorTotal += c.getValue();
}
// Keep adding basic lands until we fill the deck
while (landsNeeded > 0) {
double minPercentage = Integer.MIN_VALUE;
for (ColoredManaSymbol color : ColoredManaSymbol.values()) {
// What percentage of this color is requested
double neededPercentage = percentage.get(color.toString());
// If there is a 0% need for basic lands of this color, skip it
if (neededPercentage <= 0) {
continue;
}
int currentCount = count.get(color.toString());
double thisPercentage = 0.0;
// Calculate the percentage of lands so far that produce this color
if (currentCount > 0) {
thisPercentage = (currentCount / (double) colorTotal) * 100.0;
}
// Check if the color is the most "needed" (highest percentage) we have seen so far
if (neededPercentage - thisPercentage > minPercentage) {
// Put this color land forward to be added
colorToAdd = color;
minPercentage = (neededPercentage - thisPercentage);
}
}
genPool.addCard(getBasicLand(colorToAdd, basicLands));
count.put(colorToAdd.toString(), count.get(colorToAdd.toString()) + 1);
colorTotal++;
landsNeeded--;
}
}
/**
* Return a random basic land of the chosen color.
*
* @param color the color the basic land should produce.
* @param basicLands list of information about basic lands from the
* database.
* @return a single basic land that produces the color needed.
*/
private static Card getBasicLand(ColoredManaSymbol color, Map<String, List<CardInfo>> basicLands) {
String landName = DeckGeneratorPool.getBasicLandName(color.toString());
List<CardInfo> basicLandsInfo = basicLands.get(landName);
return basicLandsInfo.get(RandomUtil.nextInt(basicLandsInfo.size() - 1)).getMockCard().copy();
}
}