* Game: fixed wrong booster's cards ratio in sets with various arts (#7333)

This commit is contained in:
Oleg Agafonov 2023-03-19 19:55:21 +04:00
parent 00ebef654f
commit e48f024909
4 changed files with 158 additions and 23 deletions

View file

@ -3515,6 +3515,7 @@ public class MysteryBooster extends ExpansionSet {
@Override @Override
public List<Card> create15CardBooster() { public List<Card> create15CardBooster() {
// ignore special partner generation for 15 booster
return this.createBooster(); return this.createBooster();
} }
} }

View file

@ -12,6 +12,7 @@ import mage.constants.CardType;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SubType; import mage.constants.SubType;
import mage.sets.*; import mage.sets.*;
import mage.util.CardUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
@ -249,6 +250,42 @@ public class BoosterGenerationTest extends MageTestBase {
} }
} }
@Test
public void testFallenEmpires_BoosterMustUseVariousArtsButUnique() {
// Related issue: https://github.com/magefree/mage/issues/7333
// Actual for default boosters without collation
Set<String> cardNumberPosfixes = new HashSet<>();
for (int i = 0; i < 10; i++) {
List<Card> booster = FallenEmpires.getInstance().createBooster();
// must have single version of the card for booster generation
Map<String, Long> stats = FallenEmpires.getInstance().getCardsByRarity(Rarity.COMMON)
.stream()
.collect(Collectors.groupingBy(CardInfo::getName, Collectors.counting()));
String multipleCopies = stats.entrySet()
.stream()
.filter(data -> data.getValue() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.joining(", "));
assertTrue("booster generation must use cards with various arts as one card: " + multipleCopies, multipleCopies.isEmpty());
// must have all reprints
booster.forEach(card -> {
// 123c -> c
String postfix = card.getCardNumber().replace(String.valueOf(CardUtil.parseCardNumberAsInt(card.getCardNumber())), "");
if (!postfix.isEmpty()) {
cardNumberPosfixes.add(postfix);
}
});
}
assertTrue("booster must use cards with various arts",
cardNumberPosfixes.contains("a")
&& cardNumberPosfixes.contains("b")
&& cardNumberPosfixes.contains("c")
&& cardNumberPosfixes.contains("d")
);
}
@Test @Test
public void testZendikarRising_MDFC() { public void testZendikarRising_MDFC() {
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
@ -514,7 +551,7 @@ public class BoosterGenerationTest extends MageTestBase {
@Ignore // debug only: collect info about cards in boosters, see https://github.com/magefree/mage/issues/8081 @Ignore // debug only: collect info about cards in boosters, see https://github.com/magefree/mage/issues/8081
@Test @Test
public void test_CollectBoosterStats() { public void test_CollectBoosterStats() {
ExpansionSet setToAnalyse = Innistrad.getInstance(); ExpansionSet setToAnalyse = FallenEmpires.getInstance();
int openBoosters = 1000; int openBoosters = 1000;
Map<String, Integer> resRatio = new HashMap<>(); Map<String, Integer> resRatio = new HashMap<>();

View file

@ -144,6 +144,7 @@ public abstract class ExpansionSet implements Serializable {
protected final EnumMap<Rarity, List<CardInfo>> savedCards = new EnumMap<>(Rarity.class); protected final EnumMap<Rarity, List<CardInfo>> savedCards = new EnumMap<>(Rarity.class);
protected final EnumMap<Rarity, List<CardInfo>> savedSpecialCards = new EnumMap<>(Rarity.class); protected final EnumMap<Rarity, List<CardInfo>> savedSpecialCards = new EnumMap<>(Rarity.class);
protected Map<String, List<CardInfo>> savedReprints = null;
protected final Map<String, CardInfo> inBoosterMap = new HashMap<>(); protected final Map<String, CardInfo> inBoosterMap = new HashMap<>();
public ExpansionSet(String name, String code, Date releaseDate, SetType setType) { public ExpansionSet(String name, String code, Date releaseDate, SetType setType) {
@ -223,16 +224,19 @@ public abstract class ExpansionSet implements Serializable {
} }
protected void addToBooster(List<Card> booster, List<CardInfo> cards) { protected void addToBooster(List<Card> booster, List<CardInfo> cards) {
if (!cards.isEmpty()) { if (cards.isEmpty()) {
return;
}
CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size())); CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size()));
if (cardInfo != null) {
Card card = cardInfo.getCard(); Card card = cardInfo.getCard();
if (card != null) { if (card == null) {
// card with error
return;
}
booster.add(card); booster.add(card);
} }
}
}
}
public BoosterCollator createCollator() { public BoosterCollator createCollator() {
return null; return null;
@ -247,13 +251,13 @@ public abstract class ExpansionSet implements Serializable {
for (int i = 0; i < 100; i++) {//don't want to somehow loop forever for (int i = 0; i < 100; i++) {//don't want to somehow loop forever
List<Card> booster = tryBooster(); List<Card> booster = tryBooster();
if (boosterIsValid(booster)) { if (boosterIsValid(booster)) {
return booster; return addReprints(booster);
} }
} }
// return random booster if can't do valid // return random booster if can't do valid
logger.error(String.format("Can't generate valid booster for set [%s - %s]", this.getCode(), this.getName())); logger.error(String.format("Can't generate valid booster for set [%s - %s]", this.getCode(), this.getName()));
return tryBooster(); return addReprints(tryBooster());
} }
private List<Card> createBoosterUsingCollator(BoosterCollator collator) { private List<Card> createBoosterUsingCollator(BoosterCollator collator) {
@ -384,7 +388,16 @@ public abstract class ExpansionSet implements Serializable {
return ratioBoosterSpecialMythic > 0 && ratioBoosterSpecialMythic * RandomUtil.nextDouble() <= 1; return ratioBoosterSpecialMythic > 0 && ratioBoosterSpecialMythic * RandomUtil.nextDouble() <= 1;
} }
public List<Card> tryBooster() { /**
* Generates a single booster by rarity ratio in sets without custom collator
*/
protected List<Card> tryBooster() {
// Booster generating proccess must use:
// * unique cards list for ratio calculation (see removeReprints)
// * reprints for final result (see addReprints)
//
// BUT there is possible a card's duplication, see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-general/554944-do-booster-packs-ever-contain-two-of-the-same-card
List<Card> booster = new ArrayList<>(); List<Card> booster = new ArrayList<>();
if (!hasBoosters) { if (!hasBoosters) {
return booster; return booster;
@ -491,10 +504,75 @@ public abstract class ExpansionSet implements Serializable {
return hasBasicLands; return hasBasicLands;
} }
/**
* Keep only unique cards for booster generation and card ratio calculation
*
* @param list all cards list
*/
private List<CardInfo> removeReprints(List<CardInfo> list) {
Map<String, CardInfo> usedNames = new HashMap<>();
List<CardInfo> filteredList = new ArrayList<>();
list.forEach(card -> {
CardInfo foundCard = usedNames.getOrDefault(card.getName(), null);
if (foundCard == null) {
usedNames.put(card.getName(), card);
filteredList.add(card);
}
});
return filteredList;
}
/**
* Fill booster with reprints, used for non collator generation
*
* @param booster booster's cards
* @return
*/
private List<Card> addReprints(List<Card> booster) {
if (booster.stream().noneMatch(Card::getUsesVariousArt)) {
return new ArrayList<>(booster);
}
// generate possible reprints
if (this.savedReprints == null) {
this.savedReprints = new HashMap<>();
List<String> needSets = new ArrayList<>();
needSets.add(this.code);
if (this.parentSet != null) {
needSets.add(this.parentSet.code);
}
List<CardInfo> cardInfos = CardRepository.instance.findCards(new CardCriteria()
.setCodes(needSets)
.variousArt(true)
.maxCardNumber(maxCardNumberInBooster) // ignore bonus/extra reprints
);
cardInfos.forEach(card -> {
this.savedReprints.putIfAbsent(card.getName(), new ArrayList<>());
this.savedReprints.get(card.getName()).add(card);
});
}
// replace normal cards by random reprints
List<Card> finalBooster = new ArrayList<>();
booster.forEach(card -> {
List<CardInfo> reprints = this.savedReprints.getOrDefault(card.getName(), null);
if (reprints != null && reprints.size() > 1) {
Card newCard = reprints.get(RandomUtil.nextInt(reprints.size())).getCard();
if (newCard != null) {
finalBooster.add(newCard);
return;
}
}
finalBooster.add(card);
});
return finalBooster;
}
public final synchronized List<CardInfo> getCardsByRarity(Rarity rarity) { public final synchronized List<CardInfo> getCardsByRarity(Rarity rarity) {
List<CardInfo> savedCardInfos = savedCards.get(rarity); List<CardInfo> savedCardInfos = savedCards.get(rarity);
if (savedCardInfos == null) { if (savedCardInfos == null) {
savedCardInfos = findCardsByRarity(rarity); savedCardInfos = removeReprints(findCardsByRarity(rarity));
savedCards.put(rarity, savedCardInfos); savedCards.put(rarity, savedCardInfos);
} }
// Return a copy of the saved cards information, as not to let modify the original. // Return a copy of the saved cards information, as not to let modify the original.
@ -504,7 +582,7 @@ public abstract class ExpansionSet implements Serializable {
public final synchronized List<CardInfo> getSpecialCardsByRarity(Rarity rarity) { public final synchronized List<CardInfo> getSpecialCardsByRarity(Rarity rarity) {
List<CardInfo> savedCardInfos = savedSpecialCards.get(rarity); List<CardInfo> savedCardInfos = savedSpecialCards.get(rarity);
if (savedCardInfos == null) { if (savedCardInfos == null) {
savedCardInfos = findSpecialCardsByRarity(rarity); savedCardInfos = removeReprints(findSpecialCardsByRarity(rarity));
savedSpecialCards.put(rarity, savedCardInfos); savedSpecialCards.put(rarity, savedCardInfos);
} }
// Return a copy of the saved cards information, as not to let modify the original. // Return a copy of the saved cards information, as not to let modify the original.
@ -540,11 +618,11 @@ public abstract class ExpansionSet implements Serializable {
* "Special cards" are cards that have common/uncommon/rare/mythic rarities * "Special cards" are cards that have common/uncommon/rare/mythic rarities
* but can only appear in a specific slot in boosters. Examples are DFCs in * but can only appear in a specific slot in boosters. Examples are DFCs in
* Innistrad sets and common nonbasic lands in many post-2018 sets. * Innistrad sets and common nonbasic lands in many post-2018 sets.
* * <p>
* Note that Rarity.SPECIAL and Rarity.BONUS cards are not normally treated * Note that Rarity.SPECIAL and Rarity.BONUS cards are not normally treated
* as "special cards" because by default boosters don't even have slots for * as "special cards" because by default boosters don't even have slots for
* those rarities. * those rarities.
* * <p>
* Also note that getCardsByRarity calls getSpecialCardsByRarity to exclude * Also note that getCardsByRarity calls getSpecialCardsByRarity to exclude
* special cards from non-special booster slots, so sets that override this * special cards from non-special booster slots, so sets that override this
* method must not call getCardsByRarity in it or infinite recursion will occur. * method must not call getCardsByRarity in it or infinite recursion will occur.

View file

@ -29,6 +29,7 @@ public class CardCriteria {
private final List<SuperType> notSupertypes; private final List<SuperType> notSupertypes;
private final List<SubType> subtypes; private final List<SubType> subtypes;
private final List<Rarity> rarities; private final List<Rarity> rarities;
private Boolean variousArt;
private Boolean doubleFaced; private Boolean doubleFaced;
private Boolean modalDoubleFaced; private Boolean modalDoubleFaced;
private boolean nightCard; private boolean nightCard;
@ -98,6 +99,11 @@ public class CardCriteria {
return this; return this;
} }
public CardCriteria variousArt(boolean variousArt) {
this.variousArt = variousArt;
return this;
}
public CardCriteria doubleFaced(boolean doubleFaced) { public CardCriteria doubleFaced(boolean doubleFaced) {
this.doubleFaced = doubleFaced; this.doubleFaced = doubleFaced;
return this; return this;
@ -144,7 +150,11 @@ public class CardCriteria {
} }
public CardCriteria setCodes(String... setCodes) { public CardCriteria setCodes(String... setCodes) {
this.setCodes.addAll(Arrays.asList(setCodes)); return setCodes(Arrays.asList(setCodes));
}
public CardCriteria setCodes(List<String> setCodes) {
this.setCodes.addAll(setCodes);
return this; return this;
} }
@ -223,6 +233,11 @@ public class CardCriteria {
clausesCount++; clausesCount++;
} }
if (variousArt != null) {
where.eq("variousArt", variousArt);
clausesCount++;
}
if (doubleFaced != null) { if (doubleFaced != null) {
where.eq("doubleFaced", doubleFaced); where.eq("doubleFaced", doubleFaced);
clausesCount++; clausesCount++;
@ -426,6 +441,10 @@ public class CardCriteria {
return rarities; return rarities;
} }
public Boolean getVariousArt() {
return variousArt;
}
public Boolean getDoubleFaced() { public Boolean getDoubleFaced() {
return doubleFaced; return doubleFaced;
} }