mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Merge 3de8351b8b into df74917cb0
This commit is contained in:
commit
f0ca0183b6
9 changed files with 440 additions and 48 deletions
|
|
@ -45,12 +45,23 @@ bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format)
|
||||
std::optional<int> cardCopyLimitException(const CardInfo &card, const FormatRules &format)
|
||||
{
|
||||
std::optional<int> limit;
|
||||
for (const ExceptionRule &rule : format.exceptions) {
|
||||
if (exceptionAppliesToCard(card, rule)) {
|
||||
return true;
|
||||
if (rule.maxCopies == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (rule.maxCopies >= 0 && (!limit || rule.maxCopies > *limit)) {
|
||||
limit = rule.maxCopies;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format)
|
||||
{
|
||||
return cardCopyLimitException(card, format).has_value();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QList>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
class CardInfo;
|
||||
|
||||
|
|
@ -74,6 +75,9 @@ bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond);
|
|||
|
||||
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule);
|
||||
|
||||
/** @brief Most permissive matching copy-count exception, or no value when none applies. */
|
||||
std::optional<int> cardCopyLimitException(const CardInfo &card, const FormatRules &format);
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format);
|
||||
|
||||
#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "libcockatrice/deck_list/tree/abstract_deck_list_node.h"
|
||||
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QModelIndexList>
|
||||
#include <QSet>
|
||||
#include <QVariant>
|
||||
|
|
@ -327,7 +328,7 @@ void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent)
|
|||
|
||||
QModelIndex topLeft = index(0, 0, parent);
|
||||
QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent);
|
||||
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole});
|
||||
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole, DeckRoles::IsLegalRole});
|
||||
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
QModelIndex child = index(r, 0, parent);
|
||||
|
|
@ -355,7 +356,6 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
|
|||
switch (index.column()) {
|
||||
case DeckListModelColumns::CARD_AMOUNT:
|
||||
node->setNumber(value.toInt());
|
||||
refreshCardFormatLegalities();
|
||||
break;
|
||||
case DeckListModelColumns::CARD_NAME:
|
||||
node->setName(value.toString());
|
||||
|
|
@ -373,6 +373,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
|
|||
return false;
|
||||
}
|
||||
|
||||
refreshCardFormatLegalities();
|
||||
emitRecursiveUpdates(index);
|
||||
deckList->refreshDeckHash();
|
||||
emit deckHashChanged();
|
||||
|
|
@ -415,6 +416,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
|||
emitRecursiveUpdates(parent);
|
||||
}
|
||||
|
||||
refreshCardFormatLegalities();
|
||||
deckList->refreshDeckHash();
|
||||
emit deckHashChanged();
|
||||
|
||||
|
|
@ -818,7 +820,6 @@ void DeckListModel::setActiveFormat(const QString &_format)
|
|||
{
|
||||
deckList->setGameFormat(_format);
|
||||
refreshCardFormatLegalities();
|
||||
emitBackgroundUpdates(QModelIndex()); // start from root
|
||||
}
|
||||
|
||||
void DeckListModel::cleanList()
|
||||
|
|
@ -894,17 +895,17 @@ QStringList DeckListModel::getCustomZoneNames(const QString &boardZoneName) cons
|
|||
return zoneNames;
|
||||
}
|
||||
|
||||
static int maxAllowedForLegality(const FormatRules &format, const QString &legality)
|
||||
static std::optional<int> maxAllowedForLegality(const FormatRules &format, const QString &legality)
|
||||
{
|
||||
for (const AllowedCount &c : format.allowedCounts) {
|
||||
if (c.label == legality) {
|
||||
return c.max;
|
||||
}
|
||||
}
|
||||
return -1; // unknown legality → treat as illegal
|
||||
return std::nullopt; // Unknown legality is distinct from an unlimited count.
|
||||
}
|
||||
|
||||
static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity)
|
||||
static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, qint64 quantity)
|
||||
{
|
||||
if (format.isEmpty()) {
|
||||
return true;
|
||||
|
|
@ -917,9 +918,9 @@ static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &
|
|||
return cardInfo.isLegalInFormat(format);
|
||||
}
|
||||
|
||||
// Exceptions always win
|
||||
if (cardHasAnyException(cardInfo, *formatRules)) {
|
||||
return true;
|
||||
// Explicit format exceptions retain precedence, including finite limits.
|
||||
if (const auto limit = cardCopyLimitException(cardInfo, *formatRules)) {
|
||||
return *limit == -1 || quantity <= *limit;
|
||||
}
|
||||
|
||||
// check legality prop
|
||||
|
|
@ -928,42 +929,37 @@ static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &
|
|||
return false;
|
||||
}
|
||||
|
||||
int maxAllowed = maxAllowedForLegality(*formatRules, legality);
|
||||
|
||||
if (maxAllowed == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxAllowed < 0) { // unlimited
|
||||
return true;
|
||||
}
|
||||
|
||||
return quantity <= maxAllowed;
|
||||
}
|
||||
|
||||
static bool isCardNodeLegalForFormat(const QString &format, const InnerDecklistNode *zone, const DecklistCardNode *card)
|
||||
{
|
||||
// Don't check legality for tokens
|
||||
if (zone->getName() == DECK_ZONE_TOKENS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// unknown cards are not legal
|
||||
ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef());
|
||||
if (!exactCard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// actual check
|
||||
return isCardQuantityLegalForFormat(format, exactCard.getInfo(), card->getNumber());
|
||||
const auto maxAllowed = maxAllowedForLegality(*formatRules, legality);
|
||||
return maxAllowed && (*maxAllowed == -1 || (*maxAllowed >= 0 && quantity <= *maxAllowed));
|
||||
}
|
||||
|
||||
void DeckListModel::refreshCardFormatLegalities()
|
||||
{
|
||||
QString format = deckList->getGameFormat();
|
||||
|
||||
deckList->forEachCard([&format](const InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
bool legal = isCardNodeLegalForFormat(format, zone, card);
|
||||
const QString format = deckList->getGameFormat();
|
||||
QHash<QString, qint64> quantities;
|
||||
QHash<const DecklistCardNode *, CardInfoPtr> cardInfos;
|
||||
deckList->forEachCard([&](const InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
if (zone->getName() == DECK_ZONE_TOKENS) {
|
||||
return;
|
||||
}
|
||||
const ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef());
|
||||
if (exactCard) {
|
||||
const auto info = exactCard.getCardPtr();
|
||||
cardInfos.insert(card, info);
|
||||
// The maybeboard is not part of the submitted deck. Count the other
|
||||
// non-token zones together, including printings and nested custom zones.
|
||||
if (zone->getName() != DECK_ZONE_MAYBEBOARD) {
|
||||
quantities[info->getName()] += std::max(0, card->getNumber());
|
||||
}
|
||||
}
|
||||
});
|
||||
deckList->forEachCard([&](const InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
const auto info = cardInfos.value(card);
|
||||
const qint64 quantity =
|
||||
info && zone->getName() != DECK_ZONE_MAYBEBOARD ? quantities.value(info->getName()) : card->getNumber();
|
||||
const bool legal =
|
||||
zone->getName() == DECK_ZONE_TOKENS || (info && isCardQuantityLegalForFormat(format, *info, quantity));
|
||||
card->setFormatLegality(legal);
|
||||
});
|
||||
emitBackgroundUpdates(QModelIndex());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
qDebug() << "warning: an empty card was added to set" << printingInfo.getSet()->getShortName();
|
||||
}
|
||||
cards.insert(name, newCard);
|
||||
magicFormatsDirty = true;
|
||||
|
||||
return newCard;
|
||||
}
|
||||
|
|
@ -760,8 +761,38 @@ static FormatRulesNameMap buildDefaultMagicFormats()
|
|||
|
||||
const FormatRulesNameMap &OracleImporter::createDefaultMagicFormats()
|
||||
{
|
||||
static const FormatRulesNameMap cached = buildDefaultMagicFormats();
|
||||
return cached;
|
||||
static const FormatRulesNameMap defaults = buildDefaultMagicFormats();
|
||||
if (!magicFormatsDirty) {
|
||||
return magicFormats;
|
||||
}
|
||||
|
||||
// Sort by name so saved rules are independent of QHash iteration order.
|
||||
QMap<QString, int> finiteLimits;
|
||||
for (const auto &card : cards) {
|
||||
if (const auto limit = parseDeckCopyLimit(card->getName(), card->getText())) {
|
||||
finiteLimits.insert(card->getName(), *limit);
|
||||
}
|
||||
}
|
||||
magicFormats = defaults;
|
||||
if (!finiteLimits.isEmpty()) {
|
||||
for (auto it = magicFormats.begin(); it != magicFormats.end(); ++it) {
|
||||
// Copy the value before appending: defaults and other importer
|
||||
// instances retain their existing shared FormatRules objects.
|
||||
FormatRulesPtr format(new FormatRules(*it.value()));
|
||||
for (auto limit = finiteLimits.cbegin(); limit != finiteLimits.cend(); ++limit) {
|
||||
ExceptionRule exception;
|
||||
exception.conditions.append({"name", "equals", limit.key()});
|
||||
// A card's copy allowance does not make it legal in a format
|
||||
// where it is banned, restricted or outside the card pool.
|
||||
exception.conditions.append({"format-" + it.key(), "equals", "legal"});
|
||||
exception.maxCopies = limit.value();
|
||||
format->exceptions.append(exception);
|
||||
}
|
||||
it.value() = format;
|
||||
}
|
||||
}
|
||||
magicFormatsDirty = false;
|
||||
return magicFormats;
|
||||
}
|
||||
|
||||
int OracleImporter::startImport()
|
||||
|
|
@ -772,6 +803,8 @@ int OracleImporter::startImport()
|
|||
importError.clear();
|
||||
sets.clear();
|
||||
cards.clear();
|
||||
magicFormats.clear();
|
||||
magicFormatsDirty = true;
|
||||
localizedEntries.clear();
|
||||
splitLocalizedTexts.clear();
|
||||
if (allSets.isEmpty()) {
|
||||
|
|
@ -859,6 +892,8 @@ int OracleImporter::failImport(const QString &message)
|
|||
importError = message;
|
||||
sets.clear();
|
||||
cards.clear();
|
||||
magicFormats.clear();
|
||||
magicFormatsDirty = true;
|
||||
localizedEntries.clear();
|
||||
splitLocalizedTexts.clear();
|
||||
qWarning() << message;
|
||||
|
|
@ -886,6 +921,8 @@ void OracleImporter::clear()
|
|||
prepareImport();
|
||||
sets.clear();
|
||||
cards.clear();
|
||||
magicFormats.clear();
|
||||
magicFormatsDirty = true;
|
||||
allSets.clear();
|
||||
rawSetsData.clear();
|
||||
localizedEntries.clear();
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ private:
|
|||
*/
|
||||
CardNameMap cards;
|
||||
|
||||
// Shared defaults are immutable; card-specific copy limits belong to this import.
|
||||
FormatRulesNameMap magicFormats;
|
||||
bool magicFormatsDirty = true;
|
||||
|
||||
/**
|
||||
* The sets, indexed by short name.
|
||||
*/
|
||||
|
|
@ -321,7 +325,8 @@ public:
|
|||
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
||||
/**
|
||||
* @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable.
|
||||
* @brief Returns format rules including finite copy allowances from imported cards.
|
||||
* The result is cached until cards are added or cleared and must be treated as immutable.
|
||||
*/
|
||||
const FormatRulesNameMap &createDefaultMagicFormats();
|
||||
const CardNameMap &getCardList() const
|
||||
|
|
|
|||
|
|
@ -71,3 +71,52 @@ bool parseCipt(const QString &name, const QString &text)
|
|||
|
||||
return ciptPattern.match(text).hasMatch();
|
||||
}
|
||||
|
||||
std::optional<int> parseDeckCopyLimit(const QString &name, const QString &text)
|
||||
{
|
||||
// Require a complete, standalone allowance. Other effects that mention a
|
||||
// number of cards or a different card name must not change deck construction.
|
||||
static const QRegularExpression rule(
|
||||
R"(^A deck can have up to ([a-z]+(?:[ -][a-z]+)*|[0-9]+) cards? named ([^\r\n]+)\.\r?$)",
|
||||
QRegularExpression::MultilineOption);
|
||||
static const QStringList smallNumbers = {
|
||||
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
||||
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};
|
||||
static const QStringList tens = {"twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};
|
||||
auto normalizedName = [](QString cardName) {
|
||||
return cardName.replace("Æ", "AE").replace("’", "'").normalized(QString::NormalizationForm_C);
|
||||
};
|
||||
const QString expectedName = normalizedName(name);
|
||||
if (expectedName.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<int> limit;
|
||||
auto matches = rule.globalMatch(text);
|
||||
while (matches.hasNext()) {
|
||||
const auto match = matches.next();
|
||||
if (normalizedName(match.captured(2)) != expectedName) {
|
||||
continue;
|
||||
}
|
||||
const QString count = match.captured(1);
|
||||
bool numeric = false;
|
||||
int value = count.toInt(&numeric);
|
||||
if (!numeric) {
|
||||
// English words through ninety-nine; larger counts remain supported
|
||||
// as decimal numerals. Unknown wording is deliberately not guessed.
|
||||
const QStringList words = QString(count).replace('-', ' ').split(' ');
|
||||
value = words.size() == 1 ? smallNumbers.indexOf(words.first()) : -1;
|
||||
const int ten = tens.indexOf(words.first());
|
||||
if (ten >= 0 && words.size() <= 2) {
|
||||
const int unit = words.size() == 1 ? 0 : smallNumbers.indexOf(words.last());
|
||||
if (unit >= 0 && unit < 10 && (words.size() == 1 || unit > 0)) {
|
||||
value = (ten + 2) * 10 + unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value <= 0 || (limit && *limit != value)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
limit = value;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
#define PARSEHELPERS_H
|
||||
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
/** @brief Read a finite deck-copy allowance naming this card in canonical English rules text. */
|
||||
std::optional<int> parseDeckCopyLimit(const QString &name, const QString &text);
|
||||
|
||||
bool parseCipt(const QString &name, const QString &text);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/format/format_legality_rules.h>
|
||||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
|
@ -289,6 +290,131 @@ TEST(DeckListModelZoneIntegration, AddCardToCustomZoneWithNestedSubZoneAppends)
|
|||
ASSERT_TRUE(added.parent().data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
}
|
||||
|
||||
class DeckListModelCopyLimits : public testing::Test
|
||||
{
|
||||
protected:
|
||||
CardDatabase *database = CardDatabaseManager::getInstance();
|
||||
FormatRulesPtr format;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
database->clear();
|
||||
format.reset(new FormatRules);
|
||||
format->formatName = "copy-limit-test";
|
||||
format->allowedCounts = {{4, "legal"}, {1, "restricted"}, {0, "banned"}};
|
||||
database->addFormat(format);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
database->clear();
|
||||
}
|
||||
|
||||
CardInfoPtr addCard(const QString &name, const QString &legality = "legal")
|
||||
{
|
||||
auto card = CardInfo::newInstance(name);
|
||||
card->setProperty("format-copy-limit-test", legality);
|
||||
database->addCard(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
QSharedPointer<DeckList> newDeck()
|
||||
{
|
||||
auto deck = QSharedPointer<DeckList>::create();
|
||||
deck->setGameFormat(format->formatName);
|
||||
return deck;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, FiniteExceptionHonorsItsLimitAndRetainsExplicitPrecedence)
|
||||
{
|
||||
const QString name = "Seven Dwarves";
|
||||
addCard(name, "banned"); // Explicit format exceptions retain the precedence established in #6535.
|
||||
format->exceptions.append({{{"name", "equals", name}}, 7});
|
||||
auto deck = newDeck();
|
||||
auto *row = deck->getTree()->addCard(name, 7, DECK_ZONE_MAIN, -1);
|
||||
DeckListModel model(nullptr, deck);
|
||||
EXPECT_TRUE(row->getFormatLegality());
|
||||
row->setNumber(8);
|
||||
model.rebuildTree();
|
||||
EXPECT_FALSE(row->getFormatLegality());
|
||||
}
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, CopiesAcrossPrintingsSideboardAndNestedZonesShareOneLimit)
|
||||
{
|
||||
addCard("Same card");
|
||||
auto deck = newDeck();
|
||||
auto *tree = deck->getTree();
|
||||
auto *first = tree->addCard("Same card", 2, DECK_ZONE_MAIN, -1, "ONE", "1", "printing-one");
|
||||
auto *second = tree->addCard("Same card", 2, DECK_ZONE_SIDE, -1, "TWO", "2", "printing-two");
|
||||
auto *custom = tree->addCustomZone(DECK_ZONE_MAIN, "Removal");
|
||||
auto *nested = new InnerDecklistNode("Nested", custom);
|
||||
auto *third = new DecklistCardNode("Same card", 1, nested, -1);
|
||||
auto *token = tree->addCard("Same card", 100, DECK_ZONE_TOKENS, -1);
|
||||
auto *maybe = tree->addCard("Same card", 4, DECK_ZONE_MAYBEBOARD, -1);
|
||||
DeckListModel model(nullptr, deck);
|
||||
EXPECT_FALSE(first->getFormatLegality());
|
||||
EXPECT_FALSE(second->getFormatLegality());
|
||||
EXPECT_FALSE(third->getFormatLegality());
|
||||
EXPECT_TRUE(token->getFormatLegality());
|
||||
EXPECT_TRUE(maybe->getFormatLegality());
|
||||
third->setNumber(0);
|
||||
model.rebuildTree();
|
||||
EXPECT_TRUE(first->getFormatLegality());
|
||||
EXPECT_TRUE(second->getFormatLegality());
|
||||
}
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, EditingOrRemovingOneRowRefreshesTheOtherRows)
|
||||
{
|
||||
addCard("Same card");
|
||||
auto deck = newDeck();
|
||||
auto *first = deck->getTree()->addCard("Same card", 2, DECK_ZONE_MAIN, -1);
|
||||
auto *second = deck->getTree()->addCard("Same card", 2, DECK_ZONE_SIDE, -1);
|
||||
DeckListModel model(nullptr, deck);
|
||||
const auto index = model.findCard("Same card", DECK_ZONE_SIDE);
|
||||
ASSERT_TRUE(index.isValid());
|
||||
ASSERT_TRUE(model.setData(index.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT), 3, Qt::EditRole));
|
||||
EXPECT_FALSE(first->getFormatLegality());
|
||||
EXPECT_FALSE(second->getFormatLegality());
|
||||
ASSERT_TRUE(model.removeRows(index.row(), 1, index.parent()));
|
||||
EXPECT_TRUE(first->getFormatLegality());
|
||||
}
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, NegativeRowsCannotCancelCopiesElsewhere)
|
||||
{
|
||||
addCard("Same card");
|
||||
auto deck = newDeck();
|
||||
auto *first = deck->getTree()->addCard("Same card", 5, DECK_ZONE_MAIN, -1);
|
||||
auto *negative = deck->getTree()->addCard("Same card", -1, DECK_ZONE_SIDE, -1);
|
||||
DeckListModel model(nullptr, deck);
|
||||
EXPECT_FALSE(first->getFormatLegality());
|
||||
EXPECT_FALSE(negative->getFormatLegality());
|
||||
}
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, UnlimitedCountIsDistinctFromUnknownLegality)
|
||||
{
|
||||
addCard("Unlimited");
|
||||
addCard("Unknown", "not-a-status");
|
||||
format->allowedCounts = {{-1, "legal"}};
|
||||
auto deck = newDeck();
|
||||
auto *unlimited = deck->getTree()->addCard("Unlimited", 100, DECK_ZONE_MAIN, -1);
|
||||
auto *unknown = deck->getTree()->addCard("Unknown", 1, DECK_ZONE_MAIN, -1);
|
||||
DeckListModel model(nullptr, deck);
|
||||
EXPECT_TRUE(unlimited->getFormatLegality());
|
||||
EXPECT_FALSE(unknown->getFormatLegality());
|
||||
}
|
||||
|
||||
TEST_F(DeckListModelCopyLimits, MostPermissiveExceptionWinsIncludingUnlimited)
|
||||
{
|
||||
const auto card = addCard("Exception");
|
||||
const QList<CardCondition> conditions{{"name", "equals", "Exception"}};
|
||||
format->exceptions = {{conditions, 7}, {conditions, 9}};
|
||||
EXPECT_EQ(cardCopyLimitException(*card, *format), 9);
|
||||
format->exceptions.append({conditions, -1});
|
||||
EXPECT_EQ(cardCopyLimitException(*card, *format), -1);
|
||||
EXPECT_FALSE(cardCopyLimitException(*addCard("No exception"), *format));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "../../oracle/src/database_writer.h"
|
||||
#include "../../oracle/src/oracleimporter.h"
|
||||
#include "../../oracle/src/parsehelpers.h"
|
||||
#include "../../oracle/src/raw_json_scanner.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include <QChar>
|
||||
#include <QDate>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -27,6 +29,8 @@
|
|||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/card/database/card_database_data.h>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/card/format/format_legality_rules.h>
|
||||
#include <libcockatrice/card/set/card_set.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
|
@ -327,6 +331,162 @@ TEST_F(OracleImporterTest, CreateDefaultMagicFormatsCaching)
|
|||
ASSERT_EQ(first.value("standard").data(), second.value("standard").data());
|
||||
}
|
||||
|
||||
TEST(DeckCopyLimitParser, ReadsNamedAllowancesWithoutACardNameList)
|
||||
{
|
||||
EXPECT_EQ(parseDeckCopyLimit("Seven Dwarves", "A deck can have up to seven cards named Seven Dwarves."), 7);
|
||||
EXPECT_EQ(parseDeckCopyLimit("Nazgûl",
|
||||
"Deathtouch\nA deck can have up to nine cards named Nazgûl.\nThe Ring tempts you."),
|
||||
9);
|
||||
EXPECT_EQ(parseDeckCopyLimit("Future Card", "A deck can have up to 17 cards named Future Card."), 17);
|
||||
EXPECT_EQ(parseDeckCopyLimit("Future Card", "A deck can have up to twenty-three cards named Future Card."), 23);
|
||||
EXPECT_EQ(parseDeckCopyLimit("Future Card", "A deck can have up to ninety nine cards named Future Card."), 99);
|
||||
EXPECT_EQ(parseDeckCopyLimit("AEther's Ally", "A deck can have up to six cards named Æther’s Ally."), 6);
|
||||
}
|
||||
|
||||
TEST(DeckCopyLimitParser, RejectsUnknownMalformedAndOtherCardRules)
|
||||
{
|
||||
const QString name = "Example";
|
||||
for (const QString &text :
|
||||
{QString(), QString("A deck can have any number of cards named Example."),
|
||||
QString("A deck can have up to seven cards named Other Card."),
|
||||
QString("A deck can have up to seven cards named Example"),
|
||||
QString("A deck can have up to many cards named Example."),
|
||||
QString("A deck can have up to zero cards named Example."),
|
||||
QString("A deck can have up to 0 cards named Example."),
|
||||
QString("A deck can have up to -7 cards named Example."),
|
||||
QString("A deck can have up to 99999999999999999999 cards named Example."),
|
||||
QString("A deck can have up to twenty zero cards named Example."),
|
||||
QString("If you control a Dragon, A deck can have up to seven cards named Example."),
|
||||
QString(
|
||||
"A deck can have up to seven cards named Example.\nA deck can have up to nine cards named Example.")}) {
|
||||
EXPECT_FALSE(parseDeckCopyLimit(name, text).has_value()) << text.toStdString();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CreateDefaultMagicFormatsDerivesFiniteLimitsFromCanonicalText)
|
||||
{
|
||||
QVariantMap legalities;
|
||||
const auto defaults = importer->createDefaultMagicFormats();
|
||||
for (auto it = defaults.cbegin(); it != defaults.cend(); ++it) {
|
||||
legalities.insert(it.key(), "Legal");
|
||||
}
|
||||
auto dwarves = makeCard("Seven Dwarves", "", "", legalities);
|
||||
dwarves["text"] = "Seven Dwarves gets +1/+1 for each other creature named Seven Dwarves you control.\n"
|
||||
"A deck can have up to seven cards named Seven Dwarves.";
|
||||
auto nazgul = makeCard("Nazgûl", "", "", legalities);
|
||||
nazgul["text"] = "Deathtouch\nA deck can have up to nine cards named Nazgûl.";
|
||||
auto unknown = makeCard("Unknown Card");
|
||||
unknown["text"] = "A deck can have up to many cards named Unknown Card.";
|
||||
auto otherCard = makeCard("Other Card");
|
||||
otherCard["text"] = "A deck can have up to seven cards named Seven Dwarves.";
|
||||
importer->importCardsFromSet(set, QJsonArray{dwarves, nazgul, unknown, otherCard, makeCard("Ordinary Card")});
|
||||
|
||||
const auto &formats = importer->createDefaultMagicFormats();
|
||||
for (const auto &format : formats) {
|
||||
ASSERT_EQ(format->exceptions.size(), 4);
|
||||
EXPECT_EQ(format->exceptions[0].maxCopies, -1);
|
||||
EXPECT_EQ(format->exceptions[1].maxCopies, -1);
|
||||
for (const auto &expected : {qMakePair(QString("Seven Dwarves"), 7), qMakePair(QString("Nazgûl"), 9)}) {
|
||||
bool found = false;
|
||||
for (const auto &rule : format->exceptions) {
|
||||
if (rule.maxCopies == expected.second) {
|
||||
ASSERT_EQ(rule.conditions.size(), 2);
|
||||
EXPECT_EQ(rule.conditions.last().field, "format-" + format->formatName.toLower());
|
||||
EXPECT_EQ(rule.conditions.last().matchType, "equals");
|
||||
EXPECT_EQ(rule.conditions.last().value, "legal");
|
||||
EXPECT_EQ(rule.conditions.first().field, "name");
|
||||
EXPECT_EQ(rule.conditions.first().matchType, "equals");
|
||||
EXPECT_EQ(rule.conditions.first().value, expected.first);
|
||||
EXPECT_TRUE(exceptionAppliesToCard(*importer->getCardList().value(expected.first), rule));
|
||||
EXPECT_FALSE(exceptionAppliesToCard(*importer->getCardList().value("Other Card"), rule));
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, FiniteCopyAllowancesDoNotOverrideFormatEligibility)
|
||||
{
|
||||
const QVariantMap legalities{
|
||||
{"standard", "Legal"}, {"modern", "Banned"}, {"vintage", "Restricted"}, {"commander", "Not_Legal"}};
|
||||
auto dwarves = makeCard("Seven Dwarves", "", "", legalities);
|
||||
dwarves["text"] = "A deck can have up to seven cards named Seven Dwarves.";
|
||||
importer->importCardsFromSet(set, QJsonArray{dwarves});
|
||||
const auto &formats = importer->createDefaultMagicFormats();
|
||||
const auto card = importer->getCardList().value("Seven Dwarves");
|
||||
ASSERT_TRUE(card);
|
||||
EXPECT_EQ(cardCopyLimitException(*card, *formats.value("standard")), 7);
|
||||
for (const auto &format : {"modern", "vintage", "commander", "legacy"}) {
|
||||
EXPECT_FALSE(cardCopyLimitException(*card, *formats.value(format)).has_value()) << format;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, DerivedFormatCacheRefreshesWithoutChangingSharedDefaults)
|
||||
{
|
||||
OracleImporter otherImporter;
|
||||
const auto defaults = otherImporter.createDefaultMagicFormats();
|
||||
EXPECT_EQ(importer->createDefaultMagicFormats().value("standard"), defaults.value("standard"));
|
||||
|
||||
auto dwarves = makeCard("Seven Dwarves");
|
||||
dwarves["text"] = "A deck can have up to seven cards named Seven Dwarves.";
|
||||
importer->importCardsFromSet(set, QJsonArray{dwarves});
|
||||
const auto first = importer->createDefaultMagicFormats();
|
||||
EXPECT_NE(first.value("standard"), defaults.value("standard"));
|
||||
EXPECT_EQ(first.value("standard"), importer->createDefaultMagicFormats().value("standard"));
|
||||
ASSERT_EQ(first.value("standard")->exceptions.size(), 3);
|
||||
|
||||
auto nazgul = makeCard("Nazgûl");
|
||||
nazgul["text"] = "A deck can have up to nine cards named Nazgûl.";
|
||||
importer->importCardsFromSet(set, QJsonArray{nazgul});
|
||||
const auto second = importer->createDefaultMagicFormats();
|
||||
EXPECT_EQ(second.value("standard")->exceptions.size(), 4);
|
||||
EXPECT_EQ(first.value("standard")->exceptions.size(), 3);
|
||||
EXPECT_EQ(defaults.value("standard")->exceptions.size(), 2);
|
||||
EXPECT_EQ(otherImporter.createDefaultMagicFormats().value("standard"), defaults.value("standard"));
|
||||
|
||||
importer->clear();
|
||||
EXPECT_EQ(importer->createDefaultMagicFormats().value("standard"), defaults.value("standard"));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, FiniteCopyRulesSurviveImportSaveAndXmlReload)
|
||||
{
|
||||
const QVariantMap legalities{{"commander", "Legal"}, {"modern", "Not_Legal"}};
|
||||
auto dwarves = makeCard("Seven Dwarves", "", "", legalities);
|
||||
dwarves["text"] = "A deck can have up to seven cards named Seven Dwarves.";
|
||||
auto nazgul = makeCard("Nazgûl", "", "", legalities);
|
||||
nazgul["text"] = "A deck can have up to nine cards named Nazgûl.";
|
||||
const QJsonObject setData{{"code", "TST"},
|
||||
{"name", "Test Set"},
|
||||
{"type", "expansion"},
|
||||
{"releaseDate", "2024-01-01"},
|
||||
{"cards", QJsonArray{dwarves, nazgul}}};
|
||||
const QJsonObject source{{"data", QJsonObject{{"TST", setData}}}};
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(QJsonDocument(source).toJson()));
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
|
||||
QTemporaryDir directory;
|
||||
ASSERT_TRUE(directory.isValid());
|
||||
const QString filename = directory.filePath("cards.xml");
|
||||
ASSERT_TRUE(importer->saveToFile(filename, "fixture", "1"));
|
||||
QFile file(filename);
|
||||
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
CardDatabaseData restored;
|
||||
CockatriceXml4Parser parser(controller);
|
||||
parser.parseFileInto(file, restored);
|
||||
const auto commander = restored.formats.value("commander");
|
||||
const auto modern = restored.formats.value("modern");
|
||||
ASSERT_TRUE(commander);
|
||||
ASSERT_TRUE(modern);
|
||||
for (const auto &expected : {qMakePair(QString("Seven Dwarves"), 7), qMakePair(QString("Nazgûl"), 9)}) {
|
||||
const auto card = restored.cards.value(expected.first);
|
||||
ASSERT_TRUE(card);
|
||||
EXPECT_EQ(cardCopyLimitException(*card, *commander), expected.second);
|
||||
EXPECT_FALSE(cardCopyLimitException(*card, *modern).has_value());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// readSetsFromByteArray tests
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue