* 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
public List<Card> create15CardBooster() {
// ignore special partner generation for 15 booster
return this.createBooster();
}
}

View file

@ -12,6 +12,7 @@ import mage.constants.CardType;
import mage.constants.Rarity;
import mage.constants.SubType;
import mage.sets.*;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Before;
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
public void testZendikarRising_MDFC() {
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
@Test
public void test_CollectBoosterStats() {
ExpansionSet setToAnalyse = Innistrad.getInstance();
ExpansionSet setToAnalyse = FallenEmpires.getInstance();
int openBoosters = 1000;
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>> savedSpecialCards = new EnumMap<>(Rarity.class);
protected Map<String, List<CardInfo>> savedReprints = null;
protected final Map<String, CardInfo> inBoosterMap = new HashMap<>();
public ExpansionSet(String name, String code, Date releaseDate, SetType setType) {
@ -223,15 +224,18 @@ public abstract class ExpansionSet implements Serializable {
}
protected void addToBooster(List<Card> booster, List<CardInfo> cards) {
if (!cards.isEmpty()) {
CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size()));
if (cardInfo != null) {
Card card = cardInfo.getCard();
if (card != null) {
booster.add(card);
}
}
if (cards.isEmpty()) {
return;
}
CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size()));
Card card = cardInfo.getCard();
if (card == null) {
// card with error
return;
}
booster.add(card);
}
public BoosterCollator createCollator() {
@ -247,13 +251,13 @@ public abstract class ExpansionSet implements Serializable {
for (int i = 0; i < 100; i++) {//don't want to somehow loop forever
List<Card> booster = tryBooster();
if (boosterIsValid(booster)) {
return booster;
return addReprints(booster);
}
}
// 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()));
return tryBooster();
return addReprints(tryBooster());
}
private List<Card> createBoosterUsingCollator(BoosterCollator collator) {
@ -280,10 +284,10 @@ public abstract class ExpansionSet implements Serializable {
if (!hasBasicLands && parentSet != null) {
String parentCode = parentSet.code;
CardRepository
.instance
.findCards(new CardCriteria().setCodes(parentCode).rarities(Rarity.LAND))
.stream()
.forEach(cardInfo -> inBoosterMap.put(parentCode + "_" + cardInfo.getCardNumber(), cardInfo));
.instance
.findCards(new CardCriteria().setCodes(parentCode).rarities(Rarity.LAND))
.stream()
.forEach(cardInfo -> inBoosterMap.put(parentCode + "_" + cardInfo.getCardNumber(), cardInfo));
}
}
@ -384,7 +388,16 @@ public abstract class ExpansionSet implements Serializable {
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<>();
if (!hasBoosters) {
return booster;
@ -491,10 +504,75 @@ public abstract class ExpansionSet implements Serializable {
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) {
List<CardInfo> savedCardInfos = savedCards.get(rarity);
if (savedCardInfos == null) {
savedCardInfos = findCardsByRarity(rarity);
savedCardInfos = removeReprints(findCardsByRarity(rarity));
savedCards.put(rarity, savedCardInfos);
}
// 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) {
List<CardInfo> savedCardInfos = savedSpecialCards.get(rarity);
if (savedCardInfos == null) {
savedCardInfos = findSpecialCardsByRarity(rarity);
savedCardInfos = removeReprints(findSpecialCardsByRarity(rarity));
savedSpecialCards.put(rarity, savedCardInfos);
}
// Return a copy of the saved cards information, as not to let modify the original.
@ -524,7 +602,7 @@ public abstract class ExpansionSet implements Serializable {
cardInfos.removeIf(next -> (
next.getCardNumber().contains("*")
|| next.getCardNumber().contains("+")));
|| next.getCardNumber().contains("+")));
// special slot cards must not also appear in regular slots of their rarity
// special land slot cards must not appear in regular common slots either
@ -540,11 +618,11 @@ public abstract class ExpansionSet implements Serializable {
* "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
* Innistrad sets and common nonbasic lands in many post-2018 sets.
*
* <p>
* 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
* those rarities.
*
* <p>
* Also note that getCardsByRarity calls getSpecialCardsByRarity to exclude
* special cards from non-special booster slots, so sets that override this
* method must not call getCardsByRarity in it or infinite recursion will occur.
@ -572,7 +650,7 @@ public abstract class ExpansionSet implements Serializable {
cardInfos.removeIf(next -> (
next.getCardNumber().contains("*")
|| next.getCardNumber().contains("+")));
|| next.getCardNumber().contains("+")));
return cardInfos;
}

View file

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