More robust searching and importing of MDFCs, Split, Adventure cards, etc. (#8948)

This commit is contained in:
Alex Vasile 2022-07-08 22:19:54 -04:00 committed by GitHub
parent 484e6c20f1
commit b473300680
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 257 additions and 98 deletions

View file

@ -455,7 +455,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
throw new IllegalStateException("Second side card can't have empty name.");
}
CardInfo secondSideCard = CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode(), false);
CardInfo secondSideCard = CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode());
if (secondSideCard == null) {
throw new IllegalStateException("Can''t find second side card in database: " + card.getSecondSideName());
}

View file

@ -128,7 +128,7 @@ public class ChatManagerImpl implements ChatManager {
Matcher matchPattern = cardNamePattern.matcher(message);
while (matchPattern.find()) {
String cardName = matchPattern.group(1);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName);
if (cardInfo != null) {
String colour = "silver";
if (cardInfo.getCard().getColor(null).isMulticolored()) {
@ -270,7 +270,7 @@ public class ChatManagerImpl implements ChatManager {
Matcher matchPattern = getCardTextPattern.matcher(message.toLowerCase(Locale.ENGLISH));
if (matchPattern.find()) {
String cardName = matchPattern.group(1);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName);
if (cardInfo != null) {
cardInfo.getRules();
message = "<font color=orange>" + cardInfo.getName() + "</font>: Cost:" + cardInfo.getManaCosts(CardInfo.ManaCostSide.ALL).toString() + ", Types:" + cardInfo.getTypes().toString() + ", ";

View file

@ -358,7 +358,7 @@ public final class Battlebond extends ExpansionSet {
//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();
Card card = CardRepository.instance.findCardWPreferredSet(partnerName, sourceCard.getExpansionSetCode()).getCard();
if (i < max) {
booster.add(card);
} else {

View file

@ -3482,7 +3482,7 @@ public class MysteryBooster extends ExpansionSet {
private void populateSlot(int slotNumber, List<String> cardNames) {
final List<CardInfo> cardInfoList = this.possibleCardsPerBoosterSlot.get(slotNumber);
for (String name : cardNames) {
final CardInfo cardWithGivenName = CardRepository.instance.findCardWPreferredSet(name, this.code, false);
final CardInfo cardWithGivenName = CardRepository.instance.findCardWPreferredSet(name, this.code);
cardInfoList.add(cardWithGivenName);
}
}

View file

@ -21,13 +21,13 @@ public class TxtDeckImporterTest {
String[] sideboard = {"Swamp", "Mountain"};
for (String c : cards) {
card = CardRepository.instance.findPreferredCoreExpansionCard(c, true);
card = CardRepository.instance.findPreferredCoreExpansionCard(c);
assert card != null;
deck.getCards().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode()));
}
for (String s : sideboard) {
card = CardRepository.instance.findPreferredCoreExpansionCard(s, true);
card = CardRepository.instance.findPreferredCoreExpansionCard(s);
assert card != null;
deck.getSideboard().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode()));
}

View file

@ -0,0 +1,144 @@
package org.mage.test.utils;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
/**
* Testing of CardRepository functionality.
*
* @author Alex-Vasile
*/
public class CardRepositoryTest {
/**
* Test CardRepository.findCards for the difficult cases when provided with the full card name.
*
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByFullName() {
// Modal double-faced
assertFindCard("Malakir Rebirth // Malakir Mire");
// Transform double-faced
assertFindCard("Brutal Cathar // Moonrage Brute");
// Split
assertFindCard("Alive // Well");
// Flip
assertFindCard("Rune-Tail, Kitsune Ascendant // Rune-Tail's Essence");
// Adventure
assertFindCard("Ardenvale Tactician // Dizzying Swoop");
}
/**
* Test CardRepository.findCards for the difficult cases.
*
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByMainName() {
// Modal double-faced
assertFindCard("Malakir Rebirth");
// Transform double-faced
assertFindCard("Brutal Cathar");
// Split
assertFindCard("Alive");
// Flip
assertFindCard("Rune-Tail, Kitsune Ascendant");
// Adventure
assertFindCard("Ardenvale Tactician");
}
/**
* Test CardRepository.findCards for the difficult cases.
*
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsBySecondName() {
// Modal double-faced
assertFindCard("Malakir Mire");
// Transform double-faced
assertFindCard("Moonrage Brute");
// Split
assertFindCard("Well");
// Flip
assertFindCard("Rune-Tail's Essence");
// Adventure
assertFindCard("Dizzying Swoop");
}
/**
* Test CardRepository.findCardsCaseInsensitive for the difficult cases.
*
* CardRepository.findCardsCaseInsensitive is used for actual game
*/
@Test
public void testFindSplitCardsByFullNameCaseInsensitive() {
// Modal double-faced
assertFindCard("malakIR rebirTH // maLAkir mIRe");
// Transform double-faced
assertFindCard("brutAL cathAR // moonRAge BRUte");
// Split
assertFindCard("aliVE // wELl");
// Flip
assertFindCard("ruNE-taIL, kitsuNE ascendaNT // rUNe-tAIl's essENce");
// Adventure
assertFindCard("ardenvaLE tacticiAN // dizzYIng sWOop");
}
/**
* Test CardRepository.findCards for the difficult cases.
*
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsByMainNameCaseInsensitive() {
// Modal double-faced
assertFindCard("malakIR rebirTH");
// Transform double-faced
assertFindCard("brutAL cathAR");
// Split
assertFindCard("aliVE");
// Flip
assertFindCard("ruNE-taIL, kitsuNE ascendaNT");
// Adventure
assertFindCard("ardenvaLE tacticiAN");
}
/**
* Test CardRepository.findCards for the difficult cases.
*
* CardRepository.findCards is used only for testing purposes.
*/
@Test
public void testFindSplitCardsBySecondNameCaseInsensitive() {
// Modal double-faced
assertFindCard("maLAkir mIRe");
// Transform double-faced
assertFindCard("moonRAge BRUte");
// Split
assertFindCard("wELl");
// Flip
assertFindCard("rUNe-tAIl's essENce");
// Adventure
assertFindCard("dizzYIng sWOop");
}
/**
* Checks if the card with name cardName can be found when searched for
* using the case sensitive approach.
*
* @param cardName The name of the card to search by.
*/
private void assertFindCard(String cardName) {
List<CardInfo> foundCards = CardRepository.instance.findCards(cardName);
Assert.assertFalse(
"Could not find " + "\"" + cardName + "\".",
foundCards.isEmpty()
);
}
}

View file

@ -1923,9 +1923,9 @@ public class VerifyCardDataTest {
// same find code as original cube
CardInfo cardInfo;
if (!cardId.getExtension().isEmpty()) {
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension(), false);
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension());
} else {
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName(), false);
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName());
}
if (cardInfo == null) {
errorsList.add("Error: broken cube, can't find card: " + cube.getClass().getCanonicalName() + " - " + cardId.getName());

View file

@ -12,7 +12,7 @@ public class CardLookup {
public static final CardLookup instance = new CardLookup();
public Optional<CardInfo> lookupCardInfo(String name) {
return Optional.ofNullable(CardRepository.instance.findPreferredCoreExpansionCard(name, true));
return Optional.ofNullable(CardRepository.instance.findPreferredCoreExpansionCard(name));
}
public List<CardInfo> lookupCardInfo(CardCriteria criteria) {

View file

@ -92,7 +92,7 @@ public class DckDeckImporter extends PlainTextDeckImporter {
}
if (!cardName.equals("")) {
foundedCard = CardRepository.instance.findPreferredCoreExpansionCard(cardName, false, setCode);
foundedCard = CardRepository.instance.findPreferredCoreExpansionCard(cardName, setCode);
}
if (foundedCard != null) {

View file

@ -21,7 +21,7 @@ public class DekDeckImporter extends PlainTextDeckImporter {
Integer cardCount = Integer.parseInt(extractAttribute(line, "Quantity"));
String cardName = extractAttribute(line, "Name");
boolean isSideboard = "true".equals(extractAttribute(line, "Sideboard"));
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName, true);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName);
if (cardInfo == null) {
sbMessage.append("Could not find card: '").append(cardName).append("' at line ").append(lineCount).append('\n');
} else {

View file

@ -126,7 +126,7 @@ public class TxtDeckImporter extends PlainTextDeckImporter {
wasCardLines = true;
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(lineName, true);
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(lineName);
if (cardInfo == null) {
sbMessage.append("Could not find card: '").append(lineName).append("' at line ").append(lineCount).append('\n');
} else {

View file

@ -63,8 +63,9 @@ public class MockCard extends CardImpl {
this.flipCard = card.isFlipCard();
this.nightCard = card.isNightCard();
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode(), false));
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode()));
}
if (card.isAdventureCard()) {

View file

@ -38,8 +38,9 @@ public class MockSplitCard extends SplitCard {
this.flipCard = card.isFlipCard();
this.nightCard = card.isNightCard();
if (card.getSecondSideName() != null && !card.getSecondSideName().isEmpty()) {
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode(), false));
this.secondSideCard = new MockCard(CardRepository.instance.findCardWPreferredSet(card.getSecondSideName(), card.getSetCode()));
}
this.flipCardName = card.getFlipCardName();
@ -48,13 +49,13 @@ public class MockSplitCard extends SplitCard {
this.addAbility(textAbilityFromString(ruleText));
}
CardInfo leftHalf = CardRepository.instance.findCardWPreferredSet(getLeftHalfName(card), card.getSetCode(), false);
CardInfo leftHalf = CardRepository.instance.findCardWPreferredSet(getLeftHalfName(card), card.getSetCode());
if (leftHalf != null) {
this.leftHalfCard = new MockSplitCardHalf(leftHalf);
((SplitCardHalf) this.leftHalfCard).setParentCard(this);
}
CardInfo rightHalf = CardRepository.instance.findCardWPreferredSet(getRightHalfName(card), card.getSetCode(), false);
CardInfo rightHalf = CardRepository.instance.findCardWPreferredSet(getRightHalfName(card), card.getSetCode());
if (rightHalf != null) {
this.rightHalfCard = new MockSplitCardHalf(rightHalf);
((SplitCardHalf) this.rightHalfCard).setParentCard(this);

View file

@ -34,12 +34,6 @@ public class CardInfo {
@DatabaseField(indexName = "name_index")
protected String name;
/**
* lower_name exists to speed up importing decks, specifically to provide an indexed column.
* H2 does not support expressions in indices, so we need a physical column.
*/
@DatabaseField(indexName = "lower_name_index")
protected String lower_name;
@DatabaseField(indexName = "setCode_cardNumber_index")
protected String setCode;
@DatabaseField(indexName = "setCode_cardNumber_index")
@ -125,7 +119,6 @@ public class CardInfo {
public CardInfo(Card card) {
this.name = card.getName();
this.lower_name = name.toLowerCase(Locale.ENGLISH);
this.cardNumber = card.getCardNumber();
this.cardNumberAsInt = CardUtil.parseCardNumberAsInt(card.getCardNumber());
this.setCode = card.getExpansionSetCode();

View file

@ -2,7 +2,6 @@ package mage.cards.repository;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.dao.GenericRawResults;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.SelectArg;
@ -30,7 +29,7 @@ public enum CardRepository {
private static final Logger logger = Logger.getLogger(CardRepository.class);
private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE";
private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE";
private static final String VERSION_ENTITY_NAME = "card";
// raise this if db structure was changed
private static final long CARD_DB_VERSION = 54;
@ -352,17 +351,14 @@ public enum CardRepository {
return null;
}
public CardInfo findPreferredCoreExpansionCard(String name, boolean caseInsensitive) {
return findPreferredCoreExpansionCard(name, caseInsensitive, null);
public CardInfo findPreferredCoreExpansionCard(String name) {
return findPreferredCoreExpansionCard(name, null);
}
public CardInfo findPreferredCoreExpansionCard(String name, boolean caseInsensitive, String preferredSetCode) {
public CardInfo findPreferredCoreExpansionCard(String name, String preferredSetCode) {
List<CardInfo> cards;
if (caseInsensitive) {
cards = findCardsCaseInsensitive(name);
} else {
cards = findCards(name);
}
cards = findCards(name);
return findPreferredOrLatestCard(cards, preferredSetCode);
}
@ -399,13 +395,19 @@ public enum CardRepository {
return null;
}
public CardInfo findCardWPreferredSet(String name, String expansion, boolean caseInsensitive) {
/**
* Function to find a card by name from a specific set.
* Used for building cubes, packs, and for ensuring that dual faces and split cards have sides/halves from the same set.
*
* @param name name of the card, or side of the card, to find
* @param expansion the set name from which to find the card
* @return
*/
public CardInfo findCardWPreferredSet(String name, String expansion) {
List<CardInfo> cards;
if (caseInsensitive) {
cards = findCardsCaseInsensitive(name);
} else {
cards = findCards(name);
}
cards = findCards(name);
if (!cards.isEmpty()) {
for (CardInfo cardinfo : cards) {
if (cardinfo.getSetCode() != null && expansion != null && expansion.equalsIgnoreCase(cardinfo.getSetCode())) {
@ -413,7 +415,7 @@ public enum CardRepository {
}
}
}
return findPreferredCoreExpansionCard(name, true);
return findPreferredCoreExpansionCard(name);
}
public List<CardInfo> findCards(String name) {
@ -421,34 +423,84 @@ public enum CardRepository {
}
/**
* Find card's reprints from all sets
* Find a card's reprints from all sets.
* It allows for cards to be searched by their full name, or in the case of multi-name cards of the type "A // B"
* To search for them using "A", "B", or "A // B".
*
* @param name
* @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets)
* @return
* Note of how the function works:
* Out of all card types (Split, MDFC, Adventure, Flip, Transform)
* ONLY Split cards (Fire // Ice) MUST be queried in the DB by the full name when querying by "name".
* Searching for it by either half will return an incorrect result.
* ALL the others MUST be queried for by the first half of their full name (i.e. "A" from "A // B")
* when querying by "name".
*
* @param name the name of the card to search for
* @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets)
* @return A list of the reprints of the card if it was found (up to limitByMaxAmount number), or
* an empty list if the card was not found.
*/
public List<CardInfo> findCards(String name, long limitByMaxAmount) {
List<CardInfo> results;
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
if (limitByMaxAmount > 0) {
queryBuilder.limit(limitByMaxAmount);
}
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
queryBuilder.where().eq("name", new SelectArg(name));
if (limitByMaxAmount > 0) {
queryBuilder.limit(limitByMaxAmount);
if (name.contains(" // ")) { //
// Try to see if it's a split card first. (Split card stored in DB under full card name)
// Could be made faster by searching assuming it's NOT a split card and first searching by the first
// half of the name, but this is easier to understand.
queryBuilder.where().eq("name", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
// Result comes back empty, try to search using the first half (could be Adventure, MDFC, etc.)
if (results.isEmpty()) {
String mainCardName = name.split(" // ", 2)[0];
queryBuilder.where().eq("name", new SelectArg(mainCardName));
results = cardDao.query(queryBuilder.prepare()); // If still empty, then card can't be found
}
} else { // Cannot tell if string represents the full name of a card or only part of it.
// Assume it is the full card name
queryBuilder.where().eq("name", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
if (results.isEmpty()) {
// Nothing found when looking for main name, try looking under the other names
queryBuilder.where()
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
.eq("adventureSpellName", new SelectArg(name)).or()
.eq("modalDoubleFacesSecondSideName", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
} else {
// Check that a full card was found and not a SplitCardHalf
// Can be caused by searching for "Fire" instead of "Fire // Ice"
CardInfo firstCardInfo = results.get(0);
if (firstCardInfo.isSplitCardHalf()) {
// Find the main card by it's setCode and CardNumber
queryBuilder.where()
.eq("setCode", new SelectArg(firstCardInfo.setCode)).and()
.eq("cardNumber", new SelectArg(firstCardInfo.cardNumber));
List<CardInfo> tmpResults = cardDao.query(queryBuilder.prepare());
String fullSplitCardName = null;
for (CardInfo cardInfo : tmpResults) {
if (cardInfo.isSplitCard()) {
fullSplitCardName = cardInfo.name;
break;
}
}
if (fullSplitCardName == null) {
return Collections.emptyList();
}
queryBuilder.where().eq("name", new SelectArg(fullSplitCardName));
results = cardDao.query(queryBuilder.prepare());
}
}
}
List<CardInfo> result = cardDao.query(queryBuilder.prepare());
// Got no results, could be because the name referred to a double-face cards (e.g. Malakir Rebirth // Malakir Mire)
if (result.isEmpty() && name.contains(" // ")) {
// If there IS a " // " then the card could be either a double-face card (e.g. Malakir Rebirth // Malakir Mire)
// OR a split card (e.g. Assault // Battery).
// Since you can't tell based on the name, we split the text based on " // " and try the operation again with
// the string on the left side of " // " (double-faced cards are stored under the name on the left of the " // ").
queryBuilder.where().eq("name", new SelectArg(name.split(" // ", 2)[0]));
result = cardDao.query(queryBuilder.prepare());
}
return result;
return results;
} catch (SQLException ex) {
Logger.getLogger(CardRepository.class).error("Error during execution of raw sql statement", ex);
}
@ -467,38 +519,6 @@ public enum CardRepository {
return Collections.emptyList();
}
public List<CardInfo> findCardsCaseInsensitive(String name) {
try {
String sqlName = name.toLowerCase(Locale.ENGLISH).replaceAll("'", "''");
GenericRawResults<CardInfo> rawResults = cardDao.queryRaw(
"select * from " + CardRepository.VERSION_ENTITY_NAME + " where lower_name = '" + sqlName + '\'',
cardDao.getRawRowMapper());
List<CardInfo> result = rawResults.getResults();
// Got no results, could be because the name referred to a double-face cards (e.g. Malakir Rebirth // Malakir Mire)
if (result.isEmpty() && sqlName.contains(" // ")) {
// If there IS a " // " then the card could be either a double-face card (e.g. Malakir Rebirth // Malakir Mire)
// OR a split card (e.g. Assault // Battery).
// Since you can't tell based on the name, we split the text based on " // " and try the operation again with
// the string on the left side of " // " (double-faced cards are stored under the name on the left of the " // ").
String leftCardName = sqlName.split(" // ", 2)[0];
GenericRawResults<CardInfo> rawResults2 = cardDao.queryRaw(
"select * from " + CardRepository.VERSION_ENTITY_NAME + " where lower_name = '" + leftCardName + '\'',
cardDao.getRawRowMapper());
result = rawResults2.getResults();
}
return result;
} catch (SQLException ex) {
Logger.getLogger(CardRepository.class).error("Error during execution of raw sql statement", ex);
}
return Collections.emptyList();
}
/**
* Warning, don't use db functions in card's code - it generates heavy db loading in AI simulations. If you
* need that feature then check for simulation mode. See https://github.com/magefree/mage/issues/7014

View file

@ -84,9 +84,9 @@ public abstract class DraftCube {
if (!cardId.getName().isEmpty()) {
CardInfo cardInfo = null;
if (!cardId.getExtension().isEmpty()) {
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension(), false);
cardInfo = CardRepository.instance.findCardWPreferredSet(cardId.getName(), cardId.getExtension());
} else {
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName(), false);
cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardId.getName());
}
if (cardInfo != null) {