diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java index f1644cb1b86..b5b5e59857b 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGenerator.java @@ -5,20 +5,21 @@ import mage.cards.decks.Deck; import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; 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.SuperType; import mage.util.RandomUtil; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Generates random card pool and builds a deck. * - * @author nantuko - * @author Simown + * @author nantuko, Simown, JayDi85 */ public final class DeckGenerator { @@ -124,7 +125,7 @@ public final class DeckGenerator { genPool = new DeckGeneratorPool(deckSize, genDialog.getCreaturePercentage(), genDialog.getNonCreaturePercentage(), genDialog.getLandPercentage(), allowedColors, genDialog.isSingleton(), genDialog.isColorless(), - genDialog.isAdvanced(), genDialog.getDeckGeneratorCMC()); + genDialog.isCommander(), genDialog.isAdvanced(), genDialog.getDeckGeneratorCMC()); final String[] sets = setsToUse.toArray(new String[setsToUse.size()]); @@ -155,8 +156,8 @@ public final class DeckGenerator { // Generate basic land cards Map> basicLands = DeckGeneratorPool.generateBasicLands(setsToUse); - DeckGeneratorPool.generateSpells(creatureCriteria, genPool.getCreatureCount()); - DeckGeneratorPool.generateSpells(nonCreatureCriteria, genPool.getNonCreatureCount()); + DeckGeneratorPool.generateSpells(creatureCriteria, genPool.getCreatureCount(), genPool.getCommandersCount()); + DeckGeneratorPool.generateSpells(nonCreatureCriteria, genPool.getNonCreatureCount(), 0); DeckGeneratorPool.generateLands(genDialog.useNonBasicLand(), nonBasicLandCriteria, basicLands); // Reconstructs the final deck and adjusts for Math rounding and/or missing cards diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java index 7ed336c88e5..217a0d81bed 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorCMC.java @@ -1,46 +1,76 @@ - package mage.client.deck.generator; import com.google.common.collect.ImmutableList; import java.util.List; +/** + * Mana value distribution between cards in diff deck sizes + */ public enum DeckGeneratorCMC { - Low(ImmutableList.builder() - .add(new CMC(0, 2, 0.60f)) - .add(new CMC(3, 4, 0.30f)) - .add(new CMC(5, 6, 0.10f)).build(), + Low( + // 100 + ImmutableList.builder() + .add(new CMC(0, 2, 0.55f)) + .add(new CMC(3, 4, 0.30f)) + .add(new CMC(5, 6, 0.15f)).build(), + // 60 + ImmutableList.builder() + .add(new CMC(0, 2, 0.60f)) + .add(new CMC(3, 4, 0.30f)) + .add(new CMC(5, 6, 0.10f)).build(), + // 40 ImmutableList.builder() .add(new CMC(0, 2, 0.65f)) .add(new CMC(3, 4, 0.30f)) .add(new CMC(5, 5, 0.05f)).build()), - Default(ImmutableList.builder() - .add(new CMC(0, 2, 0.20f)) - .add(new CMC(3, 5, 0.50f)) - .add(new CMC(6, 7, 0.25f)) - .add(new CMC(8, 100, 0.05f)).build(), + Default( + // 100 + ImmutableList.builder() + .add(new CMC(0, 2, 0.15f)) + .add(new CMC(3, 5, 0.50f)) + .add(new CMC(6, 7, 0.30f)) + .add(new CMC(8, 100, 0.05f)).build(), + // 60 + ImmutableList.builder() + .add(new CMC(0, 2, 0.20f)) + .add(new CMC(3, 5, 0.50f)) + .add(new CMC(6, 7, 0.25f)) + .add(new CMC(8, 100, 0.05f)).build(), + // 40 ImmutableList.builder() .add(new CMC(0, 2, 0.30f)) .add(new CMC(3, 4, 0.45f)) .add(new CMC(5, 6, 0.20f)) .add(new CMC(7, 100, 0.05f)).build()), - High(ImmutableList.builder(). - add(new CMC(0, 2, 0.05f)) - .add(new CMC(3, 5, 0.35f)) - .add(new CMC(6, 7, 0.40f)) - .add(new CMC(8, 100, 0.15f)).build(), + High( + // 100 + ImmutableList.builder(). + add(new CMC(0, 2, 0.05f)) + .add(new CMC(3, 5, 0.40f)) + .add(new CMC(6, 7, 0.40f)) + .add(new CMC(8, 100, 0.15f)).build(), + // 60 + ImmutableList.builder(). + add(new CMC(0, 2, 0.05f)) + .add(new CMC(3, 5, 0.35f)) + .add(new CMC(6, 7, 0.40f)) + .add(new CMC(8, 100, 0.15f)).build(), + // 40 ImmutableList.builder(). add(new CMC(0, 2, 0.10f)) .add(new CMC(3, 4, 0.30f)) .add(new CMC(5, 6, 0.45f)) .add(new CMC(7, 100, 0.15f)).build()); + private final List poolCMCs100; private final List poolCMCs60; private final List poolCMCs40; - DeckGeneratorCMC(List CMCs60, List CMCs40) { + DeckGeneratorCMC(List CMCs100, List CMCs60, List CMCs40) { + this.poolCMCs100 = CMCs100; this.poolCMCs60 = CMCs60; this.poolCMCs40 = CMCs40; } @@ -53,6 +83,10 @@ public enum DeckGeneratorCMC { return this.poolCMCs60; } + public List get100CardPoolCMC() { + return this.poolCMCs100; + } + static class CMC { public final int min; public final int max; diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java index d445663c0c5..761fc57568e 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java @@ -32,7 +32,7 @@ public class DeckGeneratorDialog { private static String selectedColors; private static JComboBox cbSets, cbDeckSize, cbCMC; private static JButton btnGenerate, btnCancel, btnReset; - private static JCheckBox cArtifacts, cSingleton, cNonBasicLands, cColorless, cAdvanced; + private static JCheckBox cArtifacts, cSingleton, cNonBasicLands, cColorless, cAdvanced, cCommander; private static JLabel averageCMCLabel; private static SimpleDateFormat dateFormat; private static RatioAdjustingSliderPanel adjustingSliderPanel; @@ -63,7 +63,7 @@ public class DeckGeneratorDialog { c.insets = new Insets(5, 10, 0, 10); c.gridx = 1; c.gridy = 0; - String chosen = MageFrame.getPreferences().get("genDeckColor", "u"); + String chosen = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_COLORS, "u"); final ColorsChooser colorsChooser = new ColorsChooser(chosen); mainPanel.add(colorsChooser, c); @@ -135,7 +135,7 @@ public class DeckGeneratorDialog { c.ipadx = 30; c.insets = new Insets(5, 10, 0, 10); c.weightx = 0.90; - cbDeckSize = new JComboBox<>(new String[]{"40", "60"}); + cbDeckSize = new JComboBox<>(new String[]{"40", "60", "100"}); cbDeckSize.setSelectedIndex(0); cbDeckSize.setAlignmentX(Component.LEFT_ALIGNMENT); mainPanel.add(cbDeckSize, c); @@ -148,31 +148,33 @@ public class DeckGeneratorDialog { JPanel jCheckBoxes = new JPanel(new FlowLayout(FlowLayout.LEFT)); // Singletons + boolean commanderEnabled = Boolean.parseBoolean(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_COMMANDER, "false")); cSingleton = new JCheckBox("Singleton", false); cSingleton.setToolTipText("Allow only a single copy of each non-land card in your deck."); String singletonEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_SINGLETON, "false"); - cSingleton.setSelected(Boolean.valueOf(singletonEnabled)); + cSingleton.setSelected(Boolean.parseBoolean(singletonEnabled)); jCheckBoxes.add(cSingleton); + cSingleton.setEnabled(!commanderEnabled); // Artifacts cArtifacts = new JCheckBox("Artifacts", false); cArtifacts.setToolTipText("Use artifacts and artifact creatures in your deck."); String artifactEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_ARTIFACTS, "false"); - cArtifacts.setSelected(Boolean.valueOf(artifactEnabled)); + cArtifacts.setSelected(Boolean.parseBoolean(artifactEnabled)); jCheckBoxes.add(cArtifacts); // Non-basic lands cNonBasicLands = new JCheckBox("Non-basic Lands", false); cNonBasicLands.setToolTipText("Use non-basic lands in your deck (if applicable)."); String nonBasicEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_NON_BASIC_LANDS, "false"); - cNonBasicLands.setSelected(Boolean.valueOf(nonBasicEnabled)); + cNonBasicLands.setSelected(Boolean.parseBoolean(nonBasicEnabled)); jCheckBoxes.add(cNonBasicLands); // Colorless mana cColorless = new JCheckBox("Colorless mana", false); cColorless.setToolTipText("Allow cards with colorless mana cost."); String colorlessEnabled = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_COLORLESS, "false"); - cColorless.setSelected(Boolean.valueOf(colorlessEnabled)); + cColorless.setSelected(Boolean.parseBoolean(colorlessEnabled)); jCheckBoxes.add(cColorless); c.ipadx = 0; c.gridx = 0; @@ -181,12 +183,28 @@ public class DeckGeneratorDialog { c.gridwidth = 3; mainPanel.add(jCheckBoxes, c); + // Commander + cCommander = new JCheckBox("Commander", false); + cCommander.setToolTipText("Add legendary creature as commander"); + cCommander.setSelected(commanderEnabled); + jCheckBoxes.add(cCommander); + c.ipadx = 0; + c.gridx = 0; + c.gridy = 3; + c.weightx = 1; + c.gridwidth = 3; + mainPanel.add(jCheckBoxes, c); + cCommander.addItemListener(itemEvent -> { + // commander require singletone mode + cSingleton.setEnabled(!cCommander.isSelected()); + }); + // Create the advanced configuration panel JPanel advancedPanel = createAdvancedPanel(); // Advanced checkbox (enable/disable advanced configuration) - cAdvanced = new JCheckBox("Advanced"); - cAdvanced.setToolTipText("Enable advanced configuration options"); + cAdvanced = new JCheckBox("Customize distribution"); + cAdvanced.setToolTipText("Customize cards distribution due mana values and types"); cAdvanced.addItemListener(itemEvent -> { boolean enable = cAdvanced.isSelected(); enableAdvancedPanel(enable); @@ -194,7 +212,7 @@ public class DeckGeneratorDialog { // Advanced Checkbox String advancedSavedValue = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_ADVANCED, "false"); - boolean advancedEnabled = Boolean.valueOf(advancedSavedValue); + boolean advancedEnabled = Boolean.parseBoolean(advancedSavedValue); enableAdvancedPanel(advancedEnabled); cAdvanced.setSelected(advancedEnabled); c.gridy = 4; @@ -212,7 +230,7 @@ public class DeckGeneratorDialog { colorsChooser.setEnabled(false); selectedColors = (String) colorsChooser.getSelectedItem(); dlg.setVisible(false); - MageFrame.getPreferences().put("genDeckColor", selectedColors); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_COLORS, selectedColors); }); btnCancel = new JButton("Cancel"); btnCancel.addActionListener(e -> { @@ -297,7 +315,7 @@ public class DeckGeneratorDialog { c.gridwidth = 1; c.gridy = 2; btnReset = new JButton("Reset"); - btnReset.setToolTipText("Reset advanced dialog to default values"); + btnReset.setToolTipText("Reset custom cards distribution to default values"); btnReset.addActionListener(actionEvent -> { cbCMC.setSelectedItem(DeckGeneratorCMC.Default); adjustingSliderPanel.resetValues(); @@ -374,6 +392,12 @@ public class DeckGeneratorDialog { return selected; } + public boolean isCommander() { + boolean selected = cCommander.isSelected(); + PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_COMMANDER, Boolean.toString(selected)); + return selected; + } + public boolean isAdvanced() { boolean selected = cAdvanced.isSelected(); PreferencesDialog.saveValue(PreferencesDialog.KEY_NEW_DECK_GENERATOR_ADVANCED, Boolean.toString(selected)); diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java index 5c60b989043..8711bb4a88c 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorPool.java @@ -1,6 +1,6 @@ - package mage.client.deck.generator; +import mage.ObjectColor; import mage.abilities.Ability; import mage.cards.Card; import mage.cards.decks.Deck; @@ -11,16 +11,17 @@ import mage.constants.ColoredManaSymbol; import mage.constants.Rarity; import mage.util.RandomUtil; import mage.util.TournamentUtil; +import org.apache.log4j.Logger; import java.util.*; +import java.util.stream.Collectors; /** - * - * @author Simown + * @author Simown, JayDi85 */ -public class DeckGeneratorPool -{ +public class DeckGeneratorPool { + private static final Logger logger = Logger.getLogger(DeckGeneratorPool.class); public static final int DEFAULT_CREATURE_PERCENTAGE = 38; public static final int DEFAULT_NON_CREATURE_PERCENTAGE = 21; @@ -31,6 +32,7 @@ public class DeckGeneratorPool private final List poolCMCs; private final int creatureCount; private final int nonCreatureCount; + private final int commandersCount; private final int landCount; private final boolean isSingleton; private final int deckSize; @@ -48,65 +50,83 @@ public class DeckGeneratorPool /** * Creates a card pool with specified criterea used when generating a deck. * - * @param deckSize the size of the complete deck - * @param creaturePercentage what percentage of creatures to use when generating the deck. + * @param deckSize the size of the complete deck + * @param creaturePercentage what percentage of creatures to use when generating the deck. * @param nonCreaturePercentage percentage of non-creatures to use when generating the deck. - * @param landPercentage percentage of lands to use when generating the deck. - * @param allowedColors which card colors are allowed in the generated deck. - * @param isSingleton if the deck only has 1 copy of each non-land card. - * @param colorlessAllowed if colourless mana symbols are allowed in costs in the deck. - * @param isAdvanced if the user has provided advanced options to generate the deck. - * @param deckGeneratorCMC the CMC curve to use for this deck + * @param landPercentage percentage of lands to use when generating the deck. + * @param allowedColors which card colors are allowed in the generated deck. + * @param isSingleton if the deck only has 1 copy of each non-land card. + * @param colorlessAllowed if colourless mana symbols are allowed in costs in the deck. + * @param isAdvanced if the user has provided advanced options to generate the deck. + * @param isCommander reserve commander card + * @param deckGeneratorCMC the CMC curve to use for this deck */ public DeckGeneratorPool(final int deckSize, final int creaturePercentage, final int nonCreaturePercentage, final int landPercentage, - final List allowedColors, boolean isSingleton, boolean colorlessAllowed, boolean isAdvanced, DeckGeneratorCMC deckGeneratorCMC) - { + final List allowedColors, boolean isSingleton, boolean colorlessAllowed, boolean isCommander, + boolean isAdvanced, DeckGeneratorCMC deckGeneratorCMC) { this.deckSize = deckSize; this.allowedColors = allowedColors; - this.isSingleton = isSingleton; this.colorlessAllowed = colorlessAllowed; + this.commandersCount = isCommander ? 1 : 0; + this.isSingleton = isSingleton || isCommander; // commander must use singleton mode only this.deck = new Deck(); // Advanced (CMC Slider panel and curve drop-down in the dialog) - if(isAdvanced) { - this.creatureCount = (int)Math.ceil((deckSize / 100.0) * creaturePercentage); - this.nonCreatureCount = (int)Math.ceil((deckSize / 100.0)* nonCreaturePercentage); - this.landCount = (int)Math.ceil((deckSize / 100.0)* landPercentage); - if(this.deckSize == 60) { - this.poolCMCs = deckGeneratorCMC.get60CardPoolCMC(); - } else { - this.poolCMCs = deckGeneratorCMC.get40CardPoolCMC(); + if (isAdvanced) { + this.creatureCount = (int) Math.ceil((deckSize / 100.0) * creaturePercentage); + this.nonCreatureCount = (int) Math.ceil((deckSize / 100.0) * nonCreaturePercentage); + this.landCount = (int) Math.ceil((deckSize / 100.0) * landPercentage); + switch (this.deckSize) { + case 100: + this.poolCMCs = deckGeneratorCMC.get100CardPoolCMC(); + break; + case 60: + this.poolCMCs = deckGeneratorCMC.get60CardPoolCMC(); + break; + case 40: + this.poolCMCs = deckGeneratorCMC.get40CardPoolCMC(); + break; + default: + throw new IllegalArgumentException("Unsupported deck size: " + this.deckSize); } } else { // Ignore the advanced group, just use defaults - this.creatureCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_CREATURE_PERCENTAGE); - this.nonCreatureCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_NON_CREATURE_PERCENTAGE); - this.landCount = (int)Math.ceil((deckSize / 100.0) * DEFAULT_LAND_PERCENTAGE); - if(this.deckSize == 60) { - this.poolCMCs = DeckGeneratorCMC.Default.get60CardPoolCMC(); - } else { - this.poolCMCs = DeckGeneratorCMC.Default.get40CardPoolCMC(); + this.creatureCount = (int) Math.ceil((deckSize / 100.0) * DEFAULT_CREATURE_PERCENTAGE); + this.nonCreatureCount = (int) Math.ceil((deckSize / 100.0) * DEFAULT_NON_CREATURE_PERCENTAGE); + this.landCount = (int) Math.ceil((deckSize / 100.0) * DEFAULT_LAND_PERCENTAGE); + switch (this.deckSize) { + case 100: + this.poolCMCs = DeckGeneratorCMC.Default.get100CardPoolCMC(); + break; + case 60: + this.poolCMCs = DeckGeneratorCMC.Default.get60CardPoolCMC(); + break; + case 40: + this.poolCMCs = DeckGeneratorCMC.Default.get40CardPoolCMC(); + break; + default: + throw new IllegalArgumentException("Unsupported deck size: " + this.deckSize); } } - if(allowedColors.size() == 1) { + if (allowedColors.size() == 1) { monoColored = true; } - } /** * Adjusts the number of spell cards that should be in a converted mana cost (CMC) range, given the amount of cards total. + * * @param cardsCount the number of total cards. * @return a list of CMC ranges, with the amount of cards for each CMC range */ public List getCMCsForSpellCount(int cardsCount) { List adjustedCMCs = new ArrayList<>(this.poolCMCs); // For each CMC calculate how many spell cards are needed, given the total amount of cards - for(DeckGeneratorCMC.CMC deckCMC : adjustedCMCs) { - deckCMC.setAmount((int)Math.ceil(deckCMC.percentage * cardsCount)); + for (DeckGeneratorCMC.CMC deckCMC : adjustedCMCs) { + deckCMC.setAmount((int) Math.ceil(deckCMC.percentage * cardsCount)); } return adjustedCMCs; } @@ -115,15 +135,15 @@ public class DeckGeneratorPool * Verifies if the spell card supplied is valid for this pool of cards. * Checks that there isn't too many copies of this card in the deck. * Checks that the card fits the chosen colors for this pool. + * * @param card the spell card * @return if the spell card is valid for this pool. */ - public boolean isValidSpellCard(Card card) - { + public boolean isValidSpellCard(Card card) { int cardCount = getCardCount((card.getName())); // Check it hasn't already got the maximum number of copies in a deck - if(cardCount < (isSingleton ? 1 : 4)) { - if(cardFitsChosenColors(card)) { + if (cardCount < (isSingleton ? 1 : 4)) { + if (cardFitsChosenColors(card)) { return true; } } @@ -132,11 +152,11 @@ public class DeckGeneratorPool /** * Verifies if the non-basic land card supplied is valid for this pool of cards. + * * @param card the non-basic land card * @return if the land card generates the allowed colors for this pool. */ - public boolean isValidLandCard(Card card) - { + public boolean isValidLandCard(Card card) { int cardCount = getCardCount((card.getName())); // No need to check if the land is valid for the colors chosen // They are all filtered before searching for lands to include in the deck. @@ -146,60 +166,56 @@ public class DeckGeneratorPool /** * Adds a card to the pool and updates the count of this card. + * * @param card the card to add. */ - public void addCard(Card card) - { + public void addCard(Card card) { Object cnt = cardCounts.get((card.getName())); - if(cnt == null) + if (cnt == null) cardCounts.put(card.getName(), 0); int existingCount = cardCounts.get((card.getName())); - cardCounts.put(card.getName(), existingCount+1); + cardCounts.put(card.getName(), existingCount + 1); deckCards.add(card); } + public void clearCards(boolean isClearReserve) { + cardCounts.clear(); + deckCards.clear(); + if (isClearReserve) { + reserveSpells.clear(); + } + } + /** * Adds a card to the reserve pool. * Reserve pool is used when the deck generation fails to build a complete deck, or * a partially complete deck (e.g. if there are no cards found that match a CMC) - * @param card the card to add + * + * @param card the card to add * @param cardCMC the converted mana cost of the card */ public boolean tryAddReserve(Card card, int cardCMC) { // Only cards with CMC < 7 and don't already exist in the deck // can be added to our reserve pool as not to overwhelm the curve // with high CMC cards and duplicates. - if(cardCMC < 7 && getCardCount(card.getName()) == 0) { + if (cardCMC < 7 && getCardCount(card.getName()) == 0) { this.reserveSpells.add(card); return true; } return false; } - /** - * Checks if the mana symbols in the card all match the allowed colors for this pool. - * @param card the spell card to check. - * @return if all the mana symbols fit the chosen colors. - */ private boolean cardFitsChosenColors(Card card) { - for (String symbol : card.getManaCostSymbols()) { - boolean found = false; - symbol = symbol.replace("{", "").replace("}", ""); - if (isColoredManaSymbol(symbol)) { - for (ColoredManaSymbol allowed : allowedColors) { - if (symbol.contains(allowed.toString())) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - if (symbol.equals("C") && !colorlessAllowed) { + Set needColors = allowedColors.stream().map(ColoredManaSymbol::toString).collect(Collectors.toSet()); + List cardColors = card.getColorIdentity().getColors(); + for (ObjectColor cardColor : cardColors) { + if (!needColors.contains(cardColor.toString())) { return false; } } + if (cardColors.isEmpty() && !colorlessAllowed) { + return false; + } return true; } @@ -208,6 +224,7 @@ public class DeckGeneratorPool * Calculates the percentage of colored mana symbols over all spell cards in the deck. * Used to balance the generation of basic lands so the amount of lands matches the * cards mana costs. + * * @return a list of colored mana symbols and the percentage of symbols seen in cards mana costs. */ public Map calculateSpellColorPercentages() { @@ -221,14 +238,14 @@ public class DeckGeneratorPool int totalCount = 0; List fixedSpells = getFixedSpells(); - for(Card spell: fixedSpells) { + for (Card spell : fixedSpells) { for (String symbol : spell.getManaCostSymbols()) { symbol = symbol.replace("{", "").replace("}", ""); if (isColoredManaSymbol(symbol)) { for (ColoredManaSymbol allowed : allowedColors) { if (symbol.contains(allowed.toString())) { int cnt = colorCount.get(allowed.toString()); - colorCount.put(allowed.toString(), cnt+1); + colorCount.put(allowed.toString(), cnt + 1); totalCount++; } } @@ -236,7 +253,7 @@ public class DeckGeneratorPool } } final Map percentages = new HashMap<>(); - for(Map.Entry singleCount: colorCount.entrySet()) { + for (Map.Entry singleCount : colorCount.entrySet()) { String color = singleCount.getKey(); int count = singleCount.getValue(); // Calculate the percentage this color has out of the total color counts @@ -248,20 +265,20 @@ public class DeckGeneratorPool /** * Calculates how many of each mana the non-basic lands produce. + * * @param deckLands the non-basic lands which will be used in the deck. * @return a mapping of colored mana symbol to the amount that can be produced. */ - public Map countManaProduced(List deckLands) - { + public Map countManaProduced(List deckLands) { Map manaCounts = new HashMap<>(); for (final ColoredManaSymbol color : ColoredManaSymbol.values()) { manaCounts.put(color.toString(), 0); } - for(Card land: deckLands) { - for(Ability landAbility: land.getAbilities()) { + for (Card land : deckLands) { + for (Ability landAbility : land.getAbilities()) { for (ColoredManaSymbol symbol : allowedColors) { String abilityString = landAbility.getRule(); - if(landTapsForAllowedColor(abilityString, symbol.toString())) { + if (landTapsForAllowedColor(abilityString, symbol.toString())) { Integer count = manaCounts.get(symbol.toString()); manaCounts.put(symbol.toString(), count + 1); } @@ -271,15 +288,17 @@ public class DeckGeneratorPool return manaCounts; } - /** Filter all the non-basic lands retrieved from the database. + /** + * Filter all the non-basic lands retrieved from the database. + * * @param landCardsInfo information about all the cards. * @return a list of cards that produce the allowed colors for this pool. */ public List filterLands(List landCardsInfo) { List matchingLandList = new ArrayList<>(); - for(CardInfo landCardInfo: landCardsInfo) { + for (CardInfo landCardInfo : landCardsInfo) { Card landCard = landCardInfo.createMockCard(); - if(landProducesChosenColors(landCard)) { + if (landProducesChosenColors(landCard)) { matchingLandList.add(landCard); } } @@ -288,11 +307,12 @@ public class DeckGeneratorPool /** * Returns the card name that represents the basic land for this color. + * * @param symbolString the colored mana symbol. * @return the name of a basic land card. */ public static String getBasicLandName(String symbolString) { - switch(symbolString) { + switch (symbolString) { case "B": return "Swamp"; case "G": @@ -311,25 +331,50 @@ public class DeckGeneratorPool /** * Returns a complete deck. + * * @return the deck. */ public Deck getDeck() { - Set actualDeck = deck.getCards(); - actualDeck.addAll(deckCards); + deck.getCards().clear(); + deck.getSideboard().clear(); + + List useCards = new ArrayList<>(deckCards); + List useCommanders = new ArrayList<>(); + + // take random commanders + if (commandersCount > 0) { + List possibleCommanders = deckCards.stream() + .filter(this::isValidCommander) + .collect(Collectors.toList()); + Card nextCommander = RandomUtil.randomFromCollection(possibleCommanders); + while (nextCommander != null && useCommanders.size() < commandersCount) { + useCards.remove(nextCommander); + useCommanders.add(nextCommander); + } + } + + deck.getCards().addAll(useCards); + deck.getSideboard().addAll(useCommanders); return deck; } /** * Returns the number of creatures needed in this pool. + * * @return the number of creatures. */ public int getCreatureCount() { return creatureCount; } + public int getCommandersCount() { + return commandersCount; + } + /** * Returns the number of non-creatures needed in this pool. + * * @return the number of non-creatures. */ public int getNonCreatureCount() { @@ -338,6 +383,7 @@ public class DeckGeneratorPool /** * Returns the number of lands (basic + non-basic) needed in this pool. + * * @return the number of lands. */ public int getLandCount() { @@ -346,6 +392,7 @@ public class DeckGeneratorPool /** * Returns if this pool only uses one color. + * * @return if this pool is monocolored. */ public boolean isMonoColoredDeck() { @@ -354,6 +401,7 @@ public class DeckGeneratorPool /** * Returns the size of the deck to generate from this pool. + * * @return the deck size. */ public int getDeckSize() { @@ -364,20 +412,20 @@ public class DeckGeneratorPool * Fixes undersized or oversized decks that have been generated. * Removes random cards from an oversized deck until it is the correct size. * Uses the reserve pool to fill up and undersized deck with cards. + * * @return a fixed list of cards for this deck. */ - private List getFixedSpells() - { + private List getFixedSpells() { int spellSize = deckCards.size(); int nonLandSize = (deckSize - landCount); // Less spells than needed - if(spellSize < nonLandSize) { + if (spellSize < nonLandSize) { - int spellsNeeded = nonLandSize-spellSize; + int spellsNeeded = nonLandSize - spellSize; // If we haven't got enough spells in reserve to fulfil the amount we need, skip adding any. - if(reserveSpells.size() >= spellsNeeded) { + if (reserveSpells.size() >= spellsNeeded) { List spellsToAdd = new ArrayList<>(spellsNeeded); @@ -398,15 +446,15 @@ public class DeckGeneratorPool } // More spells than needed - else if(spellSize > (deckSize - landCount)) { - int spellsRemoved = (spellSize)-(deckSize-landCount); - for(int i = 0; i < spellsRemoved; ++i) { + else if (spellSize > (deckSize - landCount)) { + int spellsRemoved = (spellSize) - (deckSize - landCount); + for (int i = 0; i < spellsRemoved; ++i) { deckCards.remove(RandomUtil.nextInt(deckCards.size())); } } // Check we have exactly the right amount of cards for a deck. - if(deckCards.size() != nonLandSize) { + if (deckCards.size() != nonLandSize) { throw new IllegalStateException("Not enough cards found to generate deck."); } // Return the fixed amount @@ -416,16 +464,18 @@ public class DeckGeneratorPool /** * Returns if this land taps for the given color. * Basic string matching to check the ability adds one of the chosen mana when tapped. + * * @param ability MockAbility of the land card - * @param symbol colored mana symbol. + * @param symbol colored mana symbol. * @return if the ability is tapping to produce the mana the symbol represents. */ - private boolean landTapsForAllowedColor(String ability, String symbol) { + private boolean landTapsForAllowedColor(String ability, String symbol) { return ability.matches(".*Add \\{" + symbol + "\\}."); } /** * Returns if this land will produce the chosen colors for this pool. + * * @param card a non-basic land card. * @return if this land card taps to produces the colors chosen. */ @@ -434,32 +484,49 @@ public class DeckGeneratorPool // and other Abilities so we have to do some basic string matching on land cards for now. List landAbilities = card.getAbilities(); int count = 0; - for(Ability ability : landAbilities) { + for (Ability ability : landAbilities) { String abilityString = ability.getRule(); // Lands that tap to produce mana of the chosen colors - for(ColoredManaSymbol symbol : allowedColors) { - if(landTapsForAllowedColor(abilityString, symbol.toString())) { + for (ColoredManaSymbol symbol : allowedColors) { + if (landTapsForAllowedColor(abilityString, symbol.toString())) { count++; } } - if(count > 1) { + if (count > 1) { return true; } } return false; } + private boolean isValidCommander(Card card) { + // commander must be legendary creature + if (!card.isCreature() || !card.isLegendary()) { + return false; + } + + // commander must have all chosen colors + for (ColoredManaSymbol symbol : allowedColors) { + if (!card.getColor().contains(new ObjectColor(symbol.toString()))) { + return false; + } + } + + return true; + } + /** * Returns if the symbol is a colored mana symbol. + * * @param symbol the symbol to check. * @return If it is a basic mana symbol or a hybrid mana symbol. */ private static boolean isColoredManaSymbol(String symbol) { // Hybrid mana - if(symbol.contains("/")) { + if (symbol.contains("/")) { return true; } - for(ColoredManaSymbol c: ColoredManaSymbol.values()) { + for (ColoredManaSymbol c : ColoredManaSymbol.values()) { if (symbol.charAt(0) == (c.toString().charAt(0))) { return true; } @@ -470,14 +537,15 @@ public class DeckGeneratorPool /** * Returns how many of this card is in the pool. * If there are none in the pool it will initalise the card count. + * * @param cardName the name of the card to check. * @return the number of cards in the pool of this name. */ private int getCardCount(String cardName) { Object cC = cardCounts.get((cardName)); - if(cC == null) + if (cC == null) cardCounts.put(cardName, 0); - return cardCounts.get((cardName)); + return cardCounts.get((cardName)); } @@ -490,8 +558,8 @@ public class DeckGeneratorPool * color of cards. * * @param useNonBasicLand - * @param criteria the criteria of the lands to search for in the database. - * @param basicLands information about the basic lands from the sets used. + * @param criteria the criteria of the lands to search for in the database. + * @param basicLands information about the basic lands from the sets used. */ protected static void generateLands(boolean useNonBasicLand, CardCriteria criteria, Map> basicLands) { DeckGeneratorPool genPool = DeckGenerator.genPool; @@ -541,45 +609,69 @@ public class DeckGeneratorPool * 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. + * @param criteria the criteria to search for in the database. + * @param needCardsCount the number of spells that match the criteria needed in + * the deck. + * @param needCommandersCount make sure it contains commander creature (must be uses on first generateSpells only) */ - protected static void generateSpells(CardCriteria criteria, int spellCount) { + protected static void generateSpells(CardCriteria criteria, int needCardsCount, int needCommandersCount) { DeckGeneratorPool genPool = DeckGenerator.genPool; + if (needCommandersCount > 0 && !genPool.cardCounts.isEmpty()) { + throw new IllegalArgumentException("Wrong code usage: generateSpells with creatures and commanders must be called as first"); + } List cardPool = CardRepository.instance.findCards(criteria); - int retrievedCount = cardPool.size(); - List deckCMCs = genPool.getCMCsForSpellCount(spellCount); + List deckCMCs = genPool.getCMCsForSpellCount(needCardsCount); int count = 0; + int validCommanders = 0; int reservesAdded = 0; - boolean added; - if (retrievedCount > 0 && retrievedCount >= spellCount) { + if (cardPool.size() > 0 && cardPool.size() >= needCardsCount) { int tries = 0; - while (count < spellCount) { - Card card = cardPool.get(RandomUtil.nextInt(retrievedCount)).createMockCard(); - if (genPool.isValidSpellCard(card)) { - int cardCMC = card.getManaValue(); - 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++; + while (true) { + tries++; + + // can't finish deck, stop and use reserved cards later + if (tries > DeckGenerator.MAX_TRIES) { + logger.info("Can't generate full deck for selected settings - try again or choose more sets and less colors"); + break; + } + + // can finish deck - but make sure it has commander + if (count >= needCardsCount) { + if (validCommanders < needCommandersCount) { + // reset deck search from scratch (except reserved cards) + count = 0; + validCommanders = 0; + deckCMCs = genPool.getCMCsForSpellCount(needCardsCount); + genPool.clearCards(false); + continue; + } + break; + } + + Card card = cardPool.get(RandomUtil.nextInt(cardPool.size())).createMockCard(); + if (!genPool.isValidSpellCard(card)) { + continue; + } + + int cardCMC = card.getManaValue(); + 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++; + // make sure it has compatible commanders + if (genPool.isValidCommander(card)) { + validCommanders++; } } + } else if (reservesAdded < (genPool.getDeckSize() / 2)) { + if (genPool.tryAddReserve(card, cardCMC)) { + reservesAdded++; + } } } - tries++; - if (tries > DeckGenerator.MAX_TRIES) { - // Break here, we'll fill in random missing ones later - break; - } } } else { throw new IllegalStateException("Not enough cards to generate deck."); diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 4e8581be71f..a4598bd2796 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -268,12 +268,14 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_AUTO_TARGET_LEVEL = "autoTargetLevel"; // pref setting for deck generator + public static final String KEY_NEW_DECK_GENERATOR_COLORS = "newDeckGeneratorDeckColors"; public static final String KEY_NEW_DECK_GENERATOR_DECK_SIZE = "newDeckGeneratorDeckSize"; public static final String KEY_NEW_DECK_GENERATOR_SET = "newDeckGeneratorSet"; public static final String KEY_NEW_DECK_GENERATOR_SINGLETON = "newDeckGeneratorSingleton"; public static final String KEY_NEW_DECK_GENERATOR_ARTIFACTS = "newDeckGeneratorArtifacts"; public static final String KEY_NEW_DECK_GENERATOR_NON_BASIC_LANDS = "newDeckGeneratorNonBasicLands"; public static final String KEY_NEW_DECK_GENERATOR_COLORLESS = "newDeckGeneratorColorless"; + public static final String KEY_NEW_DECK_GENERATOR_COMMANDER = "newDeckGeneratorCommander"; public static final String KEY_NEW_DECK_GENERATOR_ADVANCED = "newDeckGeneratorAdvanced"; public static final String KEY_NEW_DECK_GENERATOR_CREATURE_PERCENTAGE = "newDeckGeneratorCreaturePercentage"; public static final String KEY_NEW_DECK_GENERATOR_NON_CREATURE_PERCENTAGE = "newDeckGeneratorNonCreaturePercentage";