diff --git a/forge-gui/src/main/java/forge/deck/CommanderDeckGenerator.java b/forge-gui/src/main/java/forge/deck/CommanderDeckGenerator.java index 54404e17bf2..9689222914e 100644 --- a/forge-gui/src/main/java/forge/deck/CommanderDeckGenerator.java +++ b/forge-gui/src/main/java/forge/deck/CommanderDeckGenerator.java @@ -63,10 +63,16 @@ public static List getBrawlDecks(final DeckFormat format, boolean isF return uniqueCards.toFlatList().stream() .filter(format.isLegalCardPredicate()) .filter(PaperCardPredicates.fromRules(CardRulesPredicates.CAN_BE_BRAWL_COMMANDER.and(canPlay))) + .filter(CommanderDeckGenerator::canGenerateBrawlDeck) .map(legend -> new CommanderDeckGenerator(legend, format, isForAi, isCardGen)) .collect(Collectors.toList()); } + private static boolean canGenerateBrawlDeck(final PaperCard legend) { + // The current random Brawl generator cannot reliably build a legal mana base for colorless commanders. + return !legend.getRules().getColorIdentity().isColorless(); + } + private final PaperCard legend; private final int index; private final DeckFormat format; diff --git a/forge-gui/src/main/java/forge/deck/DeckgenUtil.java b/forge-gui/src/main/java/forge/deck/DeckgenUtil.java index 7f0b4bf7b4d..cec3fecc8c4 100644 --- a/forge-gui/src/main/java/forge/deck/DeckgenUtil.java +++ b/forge-gui/src/main/java/forge/deck/DeckgenUtil.java @@ -678,8 +678,7 @@ public static Deck generateRandomCommanderDeck(PaperCard commander, DeckFormat f }else { String matrixKey = (format.equals(DeckFormat.TinyLeaders) ? DeckFormat.Commander : format).toString(); //use Commander for Tiny Leaders List> potentialCards = new ArrayList<>(CardRelationMatrixGenerator.cardPools.get(matrixKey).get(commander.getName())); - Collections.shuffle(potentialCards, MyRandom.getRandom()); - for(Map.Entry pair:potentialCards){ + for(Map.Entry pair:getWeightedRandomizedCardPool(potentialCards)){ if(format.isLegalCard(pair.getKey())) { preSelectedCards.add(pair.getKey()); } @@ -826,6 +825,19 @@ private static PaperCard getRandomCard(final List cards) { return cards.isEmpty() ? null : cards.get(MyRandom.getRandom().nextInt(cards.size())); } + private static List> getWeightedRandomizedCardPool(final List> potentialCards) { + final Map, Double> sortKeys = new IdentityHashMap<>(); + for (final Map.Entry cardEntry : potentialCards) { + sortKeys.put(cardEntry, getWeightedRandomSortKey(cardEntry)); + } + potentialCards.sort(Comparator.comparingDouble(sortKeys::get).reversed()); + return potentialCards; + } + private static double getWeightedRandomSortKey(final Map.Entry cardEntry) { + final int weight = Math.max(1, cardEntry.getValue()); + return Math.log(MyRandom.getRandom().nextDouble()) / weight; + } + public static Map suggestBasicLandCount(Deck d) { int W=0, U=0, R=0, B=0, G=0, total=0; List cards = d.getOrCreate(DeckSection.Main).toFlatList(); diff --git a/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedCommanderDeckBuilder.java b/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedCommanderDeckBuilder.java index 8063ffa59cb..d8f69c010ac 100644 --- a/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedCommanderDeckBuilder.java +++ b/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedCommanderDeckBuilder.java @@ -1,6 +1,9 @@ package forge.gamemodes.limited; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import com.google.common.collect.Lists; @@ -20,7 +23,7 @@ public class CardThemedCommanderDeckBuilder extends CardThemedDeckBuilder { public CardThemedCommanderDeckBuilder(PaperCard commanderCard0, PaperCard partner0, final List dList, boolean isForAI, DeckFormat format) { super(new DeckGenPool(FModel.getMagicDb().getCommonCards().getUniqueCards()), format); - this.availableList = dList; + this.availableList = new ArrayList<>(dList); keyCard = commanderCard0; secondKeyCard = partner0; // remove Unplayables @@ -32,9 +35,10 @@ public CardThemedCommanderDeckBuilder(PaperCard commanderCard0, PaperCard partne this.aiPlayables = Lists.newArrayList(availableList); } this.availableList.removeAll(aiPlayables); + this.aiPlayables = uniqueCardNamesForSingletonDeck(aiPlayables); + this.availableList = uniqueCardNamesForSingletonDeck(availableList); targetSize=format.getMainRange().getMinimum(); colors = keyCard.getRules().getColorIdentity(); - colors = ColorSet.combine(colors, keyCard.getRules().getColorIdentity()); if (secondKeyCard != null && !format.equals(DeckFormat.Oathbreaker)) { colors = ColorSet.combine(colors, secondKeyCard.getRules().getColorIdentity()); targetSize--; @@ -59,6 +63,13 @@ protected void addLandKeyCards(){ //do nothing as keycards are commander/partner and are added by the DeckGenUtils } + @Override + protected void extendPlaysets(int numSpellsNeeded) { + // Commander-family formats are singleton except for basic lands and cards + // with explicit deckbuilding exceptions. Do not fill gaps by duplicating + // cards already selected for the main deck. + } + @Override protected void addThirdColorCards(int num) { //do nothing as we cannot add extra colours beyond commanders @@ -78,4 +89,25 @@ protected String generateName() { return keyCard.getName() +" based commander deck"; } + private List uniqueCardNamesForSingletonDeck(final List cards) { + final List result = new ArrayList<>(); + final Map countsByName = new HashMap<>(); + countsByName.put(keyCard.getName(), 1); + if (secondKeyCard != null) { + countsByName.put(secondKeyCard.getName(), 1); + } + + for (final PaperCard card : cards) { + final int maxCopies = format.getMaxCardCopies(card); + final String name = card.getName(); + final int currentCount = countsByName.getOrDefault(name, 0); + if (currentCount < maxCopies) { + result.add(card); + countsByName.put(name, currentCount + 1); + } + } + + return result; + } + } diff --git a/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedDeckBuilder.java b/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedDeckBuilder.java index 98aaafab838..ba15b3d0856 100644 --- a/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedDeckBuilder.java +++ b/forge-gui/src/main/java/forge/gamemodes/limited/CardThemedDeckBuilder.java @@ -72,7 +72,6 @@ protected final float getSpellPercentage() { protected static final boolean logToConsole = false; protected static final boolean logColorsToConsole = false; - protected Iterable keyCards; protected Map targetCMCs; public CardThemedDeckBuilder(IDeckGenPool pool, DeckFormat format){ @@ -326,8 +325,9 @@ public Deck buildDeck() { //Extend to playsets for non land cards to fill out deck for when no other suitable cards are available protected void extendPlaysets(int numSpellsNeeded){ - Map currentCounts = new HashMap<>(); - List cardsToAdd = new ArrayList<>(); + final Map currentCounts = new HashMap<>(); + final List cardsToAdd = new ArrayList<>(); + final Map countsByName = getDeckListCountsByName(); int i=0; for(PaperCard card: deckList){ if(card.getRules().getType().isLand()){ @@ -336,8 +336,8 @@ protected void extendPlaysets(int numSpellsNeeded){ currentCounts.merge(card, 1, Integer::sum); } for(PaperCard card: currentCounts.keySet()){ - if(currentCounts.get(card)==2 || currentCounts.get(card)==3){ - cardsToAdd.add(card); + if((currentCounts.get(card)==2 || currentCounts.get(card)==3) + && addCardForGeneration(card, cardsToAdd, countsByName)){ ++i; if(i >= numSpellsNeeded ){ break; @@ -375,45 +375,35 @@ private int sumMapValues(Map integerMap){ protected void addKeyCards(){ // Add the first keycard if not land if(!keyCard.getRules().getMainPart().getType().isLand()) { - keyCards = IterableUtil.filter(aiPlayables, PaperCardPredicates.name(keyCard.getName())); - final List keyCardList = Lists.newArrayList(keyCards); - deckList.addAll(keyCardList); - aiPlayables.removeAll(keyCardList); - rankedColorList.removeAll(keyCardList); + addKeyCardCopies(keyCard); } // Add the second keycard if not land if(secondKeyCard!=null && !secondKeyCard.getRules().getMainPart().getType().isLand()) { - final List keyCardList = aiPlayables.stream() - .filter(PaperCardPredicates.name(secondKeyCard.getName())) - .collect(Collectors.toList()); - deckList.addAll(keyCardList); - aiPlayables.removeAll(keyCardList); - rankedColorList.removeAll(keyCardList); + addKeyCardCopies(secondKeyCard); } } protected void addLandKeyCards(){ // Add the deck card if(keyCard.getRules().getMainPart().getType().isLand()) { - keyCards = IterableUtil.filter(aiPlayables, PaperCardPredicates.name(keyCard.getName())); - final List keyCardList = Lists.newArrayList(keyCards); - deckList.addAll(keyCardList); - aiPlayables.removeAll(keyCardList); - rankedColorList.removeAll(keyCardList); - landsNeeded--; + landsNeeded -= addKeyCardCopies(keyCard).size(); } // Add the deck card if(secondKeyCard!=null && secondKeyCard.getRules().getMainPart().getType().isLand()) { - final List keyCardList = aiPlayables.stream() - .filter(PaperCardPredicates.name(secondKeyCard.getName())) - .collect(Collectors.toList()); - deckList.addAll(keyCardList); - aiPlayables.removeAll(keyCardList); - rankedColorList.removeAll(keyCardList); - landsNeeded--; + landsNeeded -= addKeyCardCopies(secondKeyCard).size(); } } + private List addKeyCardCopies(final PaperCard card) { + final List keyCardList = limitCopiesForGeneration(aiPlayables.stream() + .filter(PaperCardPredicates.name(card.getName())) + .collect(Collectors.toList())); + deckList.addAll(keyCardList); + aiPlayables.removeAll(keyCardList); + rankedColorList.removeAll(keyCardList); + return keyCardList; + } + public static class MatchColorIdentity implements Predicate { private final ColorSet allowedColor; @@ -451,7 +441,7 @@ protected void addThirdColorCards(int num) { // We haven't yet ranked the off-color cards. // Compare them to the cards already in the deckList. //List rankedOthers = CardRanker.rankCardsInPack(others, deckList, colors, true); - List toAdd = new ArrayList<>(); + final List toAdd = new ArrayList<>(); for (final PaperCard card : others) { // Want a card that has just one "off" color. final ColorSet off = colors.getOffColors(card.getRules().getColor()); @@ -465,16 +455,17 @@ protected void addThirdColorCards(int num) { .or(DeckGeneratorBase.COLORLESS_CARDS)); final Iterable threeColorList = IterableUtil.filter(aiPlayables, PaperCardPredicates.fromRules(hasColor)); + final Map countsByName = getDeckListCountsByName(); for (final PaperCard card : threeColorList) { - if (num > 0) { - toAdd.add(card); + if (num <= 0) { + break; + } + if (addCardForGeneration(card, toAdd, countsByName)) { num--; if (logToConsole) { System.out.println("Third Color[" + num + "]:" + card.getName() + "(" + card.getRules().getManaCost() + ")"); } - } else { - break; } } deckList.addAll(toAdd); @@ -483,8 +474,10 @@ protected void addThirdColorCards(int num) { } protected void addLowCMCCard(){ + final Map countsByName = getDeckListCountsByName(); final PaperCard card = rankedColorList.stream() .filter(PaperCardPredicates.IS_NON_LAND) + .filter(cardToAdd -> canAddCardForGeneration(cardToAdd, countsByName)) .findFirst().orElse(null); if (card != null) { deckList.add(card); @@ -604,7 +597,7 @@ public boolean test(PaperCard card) { && !card.getRules().getMainPart().getType().isLand(); } }; - List possibleList = Lists.newArrayList(pool.getAllCards(possibleFromFullPool)); + List possibleList = limitCopiesForGeneration(Lists.newArrayList(pool.getAllCards(possibleFromFullPool))); //ensure we do not add more keycards in case they are commanders if (keyCard != null) { possibleList.removeAll(StaticData.instance().getCommonCards().getAllCards(keyCard)); @@ -787,7 +780,8 @@ private boolean containsTronLands(Iterable cards){ */ private void addNonBasicLands() { Iterable lands = IterableUtil.filter(aiPlayables, PaperCardPredicates.IS_NONBASIC_LAND); - List landsToAdd = new ArrayList<>(); + final List landsToAdd = new ArrayList<>(); + final Map countsByName = getDeckListCountsByName(); int minBasics;//Keep a minimum number of basics to ensure playable decks if(colors.isColorless()) { minBasics = 0; @@ -802,8 +796,8 @@ private void addNonBasicLands() { for (final PaperCard card : lands) { if (landsNeeded > minBasics) { // Use only lands that are within our colors - if (card.getRules().getDeckbuildingColors().hasNoColorsExcept(colors)) { - landsToAdd.add(card); + if (card.getRules().getDeckbuildingColors().hasNoColorsExcept(colors) + && addCardForGeneration(card, landsToAdd, countsByName)) { landsNeeded--; } else if (logToConsole) { System.out.println("Excluding NonBasicLand: " + card.getName()); @@ -830,7 +824,7 @@ private void addRandomCards(int num) { && !card.getRules().getAiHints().getRemRandomDecks() && !card.getRules().getMainPart().getType().isLand(); - List possibleList = Lists.newArrayList(pool.getAllCards(possibleFromFullPool)); + List possibleList = limitCopiesForGeneration(Lists.newArrayList(pool.getAllCards(possibleFromFullPool))); //ensure we do not add more keycards in case they are commanders if (keyCard != null) { possibleList.removeAll(StaticData.instance().getCommonCards().getAllCards(keyCard)); @@ -844,6 +838,47 @@ private void addRandomCards(int num) { addManaCurveCards(possibleList, num, "Random Card"); } + protected List limitCopiesForGeneration(final List cards) { + final Map countsByName = getDeckListCountsByName(); + + final List result = new ArrayList<>(); + for (final PaperCard card : cards) { + addCardForGeneration(card, result, countsByName); + } + return result; + } + + private Map getDeckListCountsByName() { + final Map countsByName = new HashMap<>(); + for (final PaperCard card : deckList) { + addCardToGenerationCounts(card, countsByName); + } + return countsByName; + } + + private boolean canAddCardForGeneration(final PaperCard card, final Map countsByName) { + return countsByName.getOrDefault(card.getName(), 0) < getMaxCopiesForGeneration(card); + } + + private boolean addCardForGeneration(final PaperCard card, final List cards, + final Map countsByName) { + if (!canAddCardForGeneration(card, countsByName)) { + return false; + } + cards.add(card); + addCardToGenerationCounts(card, countsByName); + return true; + } + + private void addCardToGenerationCounts(final PaperCard card, final Map countsByName) { + countsByName.merge(card.getName(), 1, Integer::sum); + } + + private int getMaxCopiesForGeneration(final PaperCard card) { + final int formatMax = format.getMaxCardCopies(card); + return formatMax == format.getMaxCardCopies() ? maxDuplicates : formatMax; + } + /** * Add creatures to the deck. * @@ -853,17 +888,19 @@ private void addRandomCards(int num) { * number to add */ private void addCards(final Iterable cards, int num) { - List cardsToAdd = new ArrayList<>(); + final List cardsToAdd = new ArrayList<>(); + final Map countsByName = getDeckListCountsByName(); for (final PaperCard card : cards) { if(card.getRules().getMainPart().getType().isLand()){ continue; } if (num > 0) { - cardsToAdd.add(card); - if (logToConsole) { - System.out.println("Extra needed[" + num + "]:" + card.getName() + " (" + card.getRules().getManaCost() + ")"); + if (addCardForGeneration(card, cardsToAdd, countsByName)) { + if (logToConsole) { + System.out.println("Extra needed[" + num + "]:" + card.getName() + " (" + card.getRules().getManaCost() + ")"); + } + num--; } - num--; } else { break; } @@ -896,12 +933,17 @@ private void addManaCurveCards(final Iterable creatures, int num, Str final Map creatureCosts = deckList.stream().filter(PaperCardPredicates.IS_CREATURE) .collect(Collectors.groupingBy(c -> Ints.constrainToRange(c.getRules().getManaCost().getCMC(), 1, 6), Collectors.counting())); - List creaturesToAdd = new ArrayList<>(); + final List creaturesToAdd = new ArrayList<>(); + final Map countsByName = getDeckListCountsByName(); for (final PaperCard card : creatures) { int cmc = Ints.constrainToRange(card.getRules().getManaCost().getCMC(), 1, 6); + if (!canAddCardForGeneration(card, countsByName)) { + continue; + } + if (creatureCosts.getOrDefault(cmc, 0l) < targetCMCs.get(cmc)) { - creaturesToAdd.add(card); + addCardForGeneration(card, creaturesToAdd, countsByName); num--; creatureCosts.merge(cmc, 1l, Long::sum); if (logToConsole) {