Refactor ExpansionSet and its subclasses (#8382)

* Don't purge cards-by-rarity cache between tournaments

* Move DOM and WAR specific checks out of base ExpansionSet class

* Move Battlebond specific methods out of base ExpansionSet class

* Refactor ExpansionSet and subclasses

* Zendikar boosters also only have full-art basic lands

* Fix Eldritch Moon (#8171)
This commit is contained in:
Alex W. Jackson 2021-10-13 11:47:23 -04:00 committed by GitHub
parent 819f4c4c6c
commit 7926193e34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 589 additions and 1067 deletions

View file

@ -1,13 +1,11 @@
package mage.cards;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.keyword.PartnerWithAbility;
import mage.cards.repository.CardCriteria;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.collation.BoosterCollator;
import mage.constants.CardType;
import mage.constants.Rarity;
import mage.constants.SetType;
import mage.util.CardUtil;
@ -118,23 +116,22 @@ public abstract class ExpansionSet implements Serializable {
protected int numBoosterLands;
protected int ratioBoosterSpecialLand = 0; // if > 0 basic lands are replaced with special land with probability ratioBoosterSpecialLandNumerator / ratioBoosterSpecialLand
protected int ratioBoosterSpecialLandNumerator = 1;
protected int ratioBoosterSpecialCommon = 0; // if > 0 one common is replaced with special card with probability 1 / ratioBoosterSpecialCommon
protected int numBoosterCommon;
protected int numBoosterUncommon;
protected int numBoosterRare;
protected int numBoosterDoubleFaced; // -1 = include normally 0 = exclude 1-n = include explicit
protected double ratioBoosterMythic;
protected boolean hasPartnerMechanic = false;
protected boolean needsLegendCreature = false;
protected boolean needsPlaneswalker = false;
protected boolean validateBoosterColors = true;
protected double rejectMissingColorProbability = 0.8;
protected double rejectSameColorUncommonsProbability = 0.8;
protected int maxCardNumberInBooster; // used to omit cards with collector numbers beyond the regular cards in a set for boosters
protected final EnumMap<Rarity, List<CardInfo>> savedCards;
protected final EnumMap<Rarity, List<CardInfo>> savedCards = new EnumMap<>(Rarity.class);
protected final EnumMap<Rarity, List<CardInfo>> savedSpecialCards = new EnumMap<>(Rarity.class);
protected final Map<String, CardInfo> inBoosterMap = new HashMap<>();
public ExpansionSet(String name, String code, Date releaseDate, SetType setType) {
@ -143,7 +140,6 @@ public abstract class ExpansionSet implements Serializable {
this.releaseDate = releaseDate;
this.setType = setType;
this.maxCardNumberInBooster = Integer.MAX_VALUE;
savedCards = new EnumMap<>(Rarity.class);
}
public String getName() {
@ -214,35 +210,6 @@ public abstract class ExpansionSet implements Serializable {
return theBooster;
}
protected int addMissingPartner(List<Card> booster, boolean partnerAllowed, int max, int i) {
Card sourceCard = booster.get(booster.size() - 1);
for (Ability ability : sourceCard.getAbilities()) {
//Check if fetched card has the PartnerWithAbility
if (ability instanceof PartnerWithAbility) {
String partnerName = ((PartnerWithAbility) ability).getPartnerName();
//Check if the pack already contains a partner pair
if (partnerAllowed) {
//Added card always replaces an uncommon card
Card card = CardRepository.instance.findCardWPreferredSet(partnerName, sourceCard.getExpansionSetCode(), false).getCard();
if (i < max) {
booster.add(card);
} else {
booster.set(0, card);
}
//2 return value indicates found partner
return 2;
} else {
//If partner already exists, remove card and loop again
booster.remove(booster.size() - 1);
return 0;
}
}
}
return 1;
}
protected void addToBooster(List<Card> booster, List<CardInfo> cards) {
if (!cards.isEmpty()) {
CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size()));
@ -266,16 +233,7 @@ public abstract class ExpansionSet implements Serializable {
}
for (int i = 0; i < 100; i++) {//don't want to somehow loop forever
List<Card> booster;
if (hasPartnerMechanic) {
// battlebond's partners cards
booster = createPartnerBooster();
} else {
// all other sets
booster = tryBooster();
}
List<Card> booster = tryBooster();
if (boosterIsValid(booster)) {
return booster;
}
@ -324,13 +282,6 @@ public abstract class ExpansionSet implements Serializable {
}
}
if (needsLegendCreature) {
return booster.stream().anyMatch(card -> card.isLegendary() && card.isCreature());
}
if (needsPlaneswalker) {
return booster.stream().filter(MageObject::isPlaneswalker).count() == 1;
}
// TODO: add partner check
// TODO: add booster size check?
return true;
@ -388,88 +339,10 @@ public abstract class ExpansionSet implements Serializable {
return true;
}
private boolean checkMythic() {
protected boolean checkMythic() {
return ratioBoosterMythic > 0 && ratioBoosterMythic * RandomUtil.nextDouble() <= 1;
}
public List<Card> createPartnerBooster() {
List<Card> booster = new ArrayList<>();
boolean partnerAllowed = true;
List<CardInfo> uncommons = getCardsByRarity(Rarity.UNCOMMON);
for (int i = 0; i < numBoosterUncommon; i++) {
while (true) {
addToBooster(booster, uncommons);
int check = addMissingPartner(booster, partnerAllowed, numBoosterUncommon - 1, i);
if (check == 1) {
break;
}
if (check == 2) {
partnerAllowed = false;
//Be sure to account for the added card
if (i != numBoosterUncommon - 1) {
i += 1;
}
break;
}
}
}
int numSpecialCommons = getNumberOfSpecialCommons();
int numCommonsToGenerate = numBoosterCommon - numSpecialCommons;
List<CardInfo> commons = getCardsByRarity(Rarity.COMMON);
for (int i = 0; i < numCommonsToGenerate; i++) {
addToBooster(booster, commons);
}
List<CardInfo> rares = getCardsByRarity(Rarity.RARE);
List<CardInfo> mythics = getCardsByRarity(Rarity.MYTHIC);
for (int i = 0; i < numBoosterRare; i++) {
if (checkMythic()) {
while (true) {
addToBooster(booster, mythics);
int check = addMissingPartner(booster, partnerAllowed, -1, 1);
if (check == 1) {
break;
}
if (check == 2) {
partnerAllowed = false;
break;
}
}
} else {
while (true) {
addToBooster(booster, rares);
int check = addMissingPartner(booster, partnerAllowed, -1, 1);
if (check == 1) {
break;
}
if (check == 2) {
partnerAllowed = false;
break;
}
}
}
}
if (numBoosterLands > 0) {
List<CardInfo> specialLands = getSpecialLand();
List<CardInfo> basicLands = getCardsByRarity(Rarity.LAND);
for (int i = 0; i < numBoosterLands; i++) {
if (ratioBoosterSpecialLand > 0 && RandomUtil.nextInt(ratioBoosterSpecialLand) < ratioBoosterSpecialLandNumerator && specialLands != null) {
addToBooster(booster, specialLands);
} else {
addToBooster(booster, basicLands);
}
}
}
return booster;
}
public List<Card> tryBooster() {
List<Card> booster = new ArrayList<>();
if (!hasBoosters) {
@ -477,28 +350,29 @@ public abstract class ExpansionSet implements Serializable {
}
if (numBoosterLands > 0) {
List<CardInfo> specialLands = getSpecialLand();
List<CardInfo> specialLands = getSpecialCardsByRarity(Rarity.LAND);
List<CardInfo> basicLands = getCardsByRarity(Rarity.LAND);
for (int i = 0; i < numBoosterLands; i++) {
if (ratioBoosterSpecialLand > 0 && RandomUtil.nextInt(ratioBoosterSpecialLand) < ratioBoosterSpecialLandNumerator && specialLands != null) {
if (ratioBoosterSpecialLand > 0 && RandomUtil.nextInt(ratioBoosterSpecialLand) < ratioBoosterSpecialLandNumerator) {
addToBooster(booster, specialLands);
} else {
addToBooster(booster, basicLands);
}
}
}
int numSpecialCommons = getNumberOfSpecialCommons();
int numCommonsToGenerate = numBoosterCommon - numSpecialCommons;
int numCommonsToGenerate = numBoosterCommon;
int numSpecialToGenerate = numBoosterSpecial;
if (ratioBoosterSpecialCommon > 0 && RandomUtil.nextInt(ratioBoosterSpecialCommon) < 1) {
--numCommonsToGenerate;
++numSpecialToGenerate;
}
List<CardInfo> commons = getCardsByRarity(Rarity.COMMON);
for (int i = 0; i < numCommonsToGenerate; i++) {
addToBooster(booster, commons);
}
if (numSpecialCommons > 0) { // e.g. used to conditionally replace common cards in the booster
addSpecialCommon(booster, numSpecialCommons);
}
List<CardInfo> uncommons = getCardsByRarity(Rarity.UNCOMMON);
for (int i = 0; i < numBoosterUncommon; i++) {
addToBooster(booster, uncommons);
@ -518,8 +392,8 @@ public abstract class ExpansionSet implements Serializable {
addDoubleFace(booster);
}
if (numBoosterSpecial > 0) {
addSpecial(booster);
if (numSpecialToGenerate > 0) {
addSpecialCards(booster, numSpecialToGenerate);
}
return booster;
@ -528,21 +402,27 @@ public abstract class ExpansionSet implements Serializable {
/* add double faced card for Innistrad booster
* rarity near as the normal distribution
*/
public void addDoubleFace(List<Card> booster) {
protected void addDoubleFace(List<Card> booster) {
Rarity rarity;
for (int i = 0; i < numBoosterDoubleFaced; i++) {
CardCriteria criteria = new CardCriteria();
criteria.setCodes(this.code).doubleFaced(true);
if (RandomUtil.nextInt(15) < 10) {
criteria.rarities(Rarity.COMMON);
} else if (RandomUtil.nextInt(5) < 4) {
criteria.rarities(Rarity.UNCOMMON);
} else if (RandomUtil.nextInt(8) < 7) {
criteria.rarities(Rarity.RARE);
int rarityKey = RandomUtil.nextInt(121);
if (rarityKey < 66) {
rarity = Rarity.COMMON;
} else if (rarityKey < 108) {
rarity = Rarity.UNCOMMON;
} else if (rarityKey < 120) {
rarity = Rarity.RARE;
} else {
criteria.rarities(Rarity.MYTHIC);
rarity = Rarity.MYTHIC;
}
List<CardInfo> doubleFacedCards = CardRepository.instance.findCards(criteria);
addToBooster(booster, doubleFacedCards);
addToBooster(booster, getSpecialCardsByRarity(rarity));
}
}
protected void addSpecialCards(List<Card> booster, int number) {
List<CardInfo> specialCards = getCardsByRarity(Rarity.SPECIAL);
for (int i = 0; i < number; i++) {
addToBooster(booster, specialCards);
}
}
@ -551,75 +431,6 @@ public abstract class ExpansionSet implements Serializable {
return new GregorianCalendar(year, month - 1, day).getTime();
}
/**
* Can be overwritten if sometimes special cards will be generated instead
* of common slots
*
* @return
*/
public int getNumberOfSpecialCommons() {
return 0;
}
/**
* Can be overwritten to add a replacement for common card in boosters
*
* @param booster
* @param number
*/
public void addSpecialCommon(List<Card> booster, int number) {
}
private void addSpecial(List<Card> booster) {
int specialCards = 0;
List<CardInfo> specialBonus = getSpecialBonus();
specialCards += specialBonus.size();
List<CardInfo> specialMythic = getSpecialMythic();
specialCards += specialMythic.size();
List<CardInfo> specialRare = getSpecialRare();
specialCards += specialRare.size();
List<CardInfo> specialUncommon = getSpecialUncommon();
specialCards += specialUncommon.size();
List<CardInfo> specialCommon = getSpecialCommon();
specialCards += specialCommon.size();
if (specialCards > 0) {
for (int i = 0; i < numBoosterSpecial; i++) {
if (!specialCommon.isEmpty()
&& RandomUtil.nextInt(15) < 10) {
addToBooster(booster, specialCommon);
continue;
}
if (!specialUncommon.isEmpty()
&& RandomUtil.nextInt(4) < 3) {
addToBooster(booster, specialUncommon);
continue;
}
if (!specialRare.isEmpty()
&& RandomUtil.nextInt(8) < 7) {
addToBooster(booster, specialRare);
continue;
}
if (!specialMythic.isEmpty()) {
if (specialBonus.isEmpty() || RandomUtil.nextInt(3) < 2) {
addToBooster(booster, specialMythic);
continue;
}
}
if (!specialBonus.isEmpty()) {
addToBooster(booster, specialBonus);
continue;
}
i--;
}
}
}
public boolean hasBoosters() {
return hasBoosters;
}
@ -628,58 +439,89 @@ public abstract class ExpansionSet implements Serializable {
return hasBasicLands;
}
public List<CardInfo> getCardsByRarity(Rarity rarity) {
List<CardInfo> savedCardsInfos = savedCards.get(rarity);
if (savedCardsInfos == null) {
CardCriteria criteria = new CardCriteria();
if (rarity == Rarity.LAND && !hasBasicLands && parentSet != null) {
// get basic lands from parent set if this set doesn't have them
criteria.setCodes(parentSet.code);
} else {
criteria.setCodes(this.code);
}
criteria.rarities(rarity);
if (numBoosterDoubleFaced > -1) {
criteria.doubleFaced(false);
}
savedCardsInfos = CardRepository.instance.findCards(criteria);
// Workaround after card number is numeric
if (maxCardNumberInBooster != Integer.MAX_VALUE) {
savedCardsInfos.removeIf(next -> next.getCardNumberAsInt() > maxCardNumberInBooster && rarity != Rarity.LAND);
}
savedCards.put(rarity, savedCardsInfos);
public final synchronized List<CardInfo> getCardsByRarity(Rarity rarity) {
List<CardInfo> savedCardInfos = savedCards.get(rarity);
if (savedCardInfos == null) {
savedCardInfos = findCardsByRarity(rarity);
savedCards.put(rarity, savedCardInfos);
}
// Return a copy of the saved cards information, as not to let modify the original.
return new ArrayList<>(savedCardsInfos);
return new ArrayList<>(savedCardInfos);
}
public List<CardInfo> getSpecialCommon() {
return new ArrayList<>();
public final synchronized List<CardInfo> getSpecialCardsByRarity(Rarity rarity) {
List<CardInfo> savedCardInfos = savedSpecialCards.get(rarity);
if (savedCardInfos == null) {
savedCardInfos = findSpecialCardsByRarity(rarity);
savedSpecialCards.put(rarity, savedCardInfos);
}
// Return a copy of the saved cards information, as not to let modify the original.
return new ArrayList<>(savedCardInfos);
}
public List<CardInfo> getSpecialUncommon() {
return new ArrayList<>();
protected List<CardInfo> findCardsByRarity(Rarity rarity) {
// get basic lands from parent set if this set doesn't have them
if (rarity == Rarity.LAND && !hasBasicLands && parentSet != null) {
return parentSet.getCardsByRarity(rarity);
}
List<CardInfo> cardInfos = CardRepository.instance.findCards(new CardCriteria()
.setCodes(this.code)
.rarities(rarity));
cardInfos.removeIf(next -> (
next.getCardNumberAsInt() > maxCardNumberInBooster
|| 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
List<CardInfo> specialCards = getSpecialCardsByRarity(rarity);
if (rarity == Rarity.COMMON && ratioBoosterSpecialLand > 0) {
specialCards.addAll(getSpecialCardsByRarity(Rarity.LAND));
}
cardInfos.removeAll(specialCards);
return cardInfos;
}
public List<CardInfo> getSpecialRare() {
return new ArrayList<>();
}
/**
* "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.
*
* 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.
*
* 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.
*/
protected List<CardInfo> findSpecialCardsByRarity(Rarity rarity) {
List<CardInfo> cardInfos = new ArrayList<>();
public List<CardInfo> getSpecialMythic() {
return new ArrayList<>();
}
// if set has special land slot, assume all common lands are special cards
if (rarity == Rarity.LAND && ratioBoosterSpecialLand > 0) {
cardInfos.addAll(CardRepository.instance.findCards(new CardCriteria()
.setCodes(this.code)
.rarities(Rarity.COMMON)
.types(CardType.LAND)));
}
public List<CardInfo> getSpecialBonus() {
return new ArrayList<>();
}
// if set has special slot(s) for DFCs, they are special cards
if (numBoosterDoubleFaced > 0) {
cardInfos.addAll(CardRepository.instance.findCards(new CardCriteria()
.setCodes(this.code)
.rarities(rarity)
.doubleFaced(true)));
}
public List<CardInfo> getSpecialLand() {
return new ArrayList<>();
}
cardInfos.removeIf(next -> (
next.getCardNumberAsInt() > maxCardNumberInBooster
|| next.getCardNumber().contains("*")
|| next.getCardNumber().contains("+")));
public void removeSavedCards() {
savedCards.clear();
return cardInfos;
}
public int getMaxCardNumberInBooster() {

View file

@ -479,6 +479,16 @@ public class CardInfo {
return modalDoubleFacesSecondSideName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof CardInfo)) return false;
CardInfo other = (CardInfo) o;
return (this.name.equals(other.name)
&& this.setCode.equals(other.setCode)
&& this.cardNumber.equals(other.cardNumber));
}
@Override
public String toString() {
return String.format("%s (%s, %s)", getName(), getSetCode(), getCardNumber());

View file

@ -34,7 +34,6 @@ public class BoosterDraft extends DraftImpl {
}
boosterNum++;
}
resetBufferedCards();
this.fireEndDraftEvent();
}

View file

@ -44,8 +44,6 @@ public interface Draft extends MageItem, Serializable {
void addPlayerQueryEventListener(Listener<PlayerQueryEvent> listener);
void firePickCardEvent(UUID playerId);
void resetBufferedCards();
boolean isAbort();
void setAbort(boolean abort);

View file

@ -305,16 +305,4 @@ public abstract class DraftImpl implements Draft {
started = true;
}
@Override
public void resetBufferedCards() {
Set<ExpansionSet> setsDone = new HashSet<>();
for (ExpansionSet set : sets) {
if (!setsDone.contains(set)) {
set.removeSavedCards();
setsDone.add(set);
}
}
}
}

View file

@ -38,7 +38,6 @@ public class RichManBoosterDraft extends DraftImpl {
}
boosterNum++;
}
resetBufferedCards();
this.fireEndDraftEvent();
}

View file

@ -36,7 +36,6 @@ public class RichManCubeBoosterDraft extends DraftImpl {
}
boosterNum++;
}
resetBufferedCards();
this.fireEndDraftEvent();
}

View file

@ -419,20 +419,9 @@ public abstract class TournamentImpl implements Tournament {
}
}
}
resetBufferedCards();
nextStep();
}
public void resetBufferedCards() {
Set<ExpansionSet> setsDone = new HashSet<>();
for (ExpansionSet set : sets) {
if (!setsDone.contains(set)) {
set.removeSavedCards();
setsDone.add(set);
}
}
}
public void playMatch(TournamentPairing pair) {
options.getMatchOptions().getPlayerTypes().clear();
options.getMatchOptions().getPlayerTypes().add(pair.getPlayer1().getPlayerType());