mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 17:43:23 -08:00
Honor finite deck copy limits across printings and deck zones [skip ci]
This commit is contained in:
parent
e08402ac94
commit
142341bc3c
9 changed files with 438 additions and 48 deletions
|
|
@ -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,4 +1,5 @@
|
|||
#include "../../oracle/src/oracleimporter.h"
|
||||
#include "../../oracle/src/parsehelpers.h"
|
||||
#include "../../oracle/src/raw_json_scanner.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
|
||||
|
|
@ -6,6 +7,8 @@
|
|||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QDate>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -20,10 +23,13 @@
|
|||
#include <QRegularExpressionMatch>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
#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>
|
||||
|
|
@ -318,6 +324,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