Merge pull request #8 from Continuum-Universes-Studio/feature/continuumatrice-product-migration

Feature/continuumatrice product migration
This commit is contained in:
Christian Daniel Jensen 2026-11-03 18:45:57 -07:00 • committed by GitHub
commit 533d2f04c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 5268 additions and 2563 deletions

View file

@ -172,6 +172,13 @@ endfunction()
# --- Unit tests ------------------------------------------------------------------
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility)
# XML v3/v4 exporters in libcockatrice_card reference VERSION_STRING.
# Link the same generated version implementation used by the client/server.
cockatrice_add_test(
NAME essence_types_test
SOURCES ${VERSION_STRING_CPP} essence_types_test.cpp
LIBS libcockatrice_card
)
cockatrice_add_test(NAME clamped_arithmetic_test SOURCES clamped_arithmetic_test.cpp LIBS libcockatrice_utility)
cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIBS libcockatrice_utility)
cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)

View file

@ -26,6 +26,7 @@
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_data.h>
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
#include <libcockatrice/card/game_specific_terms.h>
#include <libcockatrice/card/lazy_properties_hash.h>
#include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/card/relation/card_relation.h>
@ -35,6 +36,7 @@
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/utility/card_ref.h>
#include <string>
#include <utility>
namespace
{
@ -65,6 +67,65 @@ TEST(CardDatabaseTest, LoadXml)
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
}
TEST(CardDatabaseTest, EmptyLoadedSnapshotIsReadyNotLoading)
{
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider pathProvider;
NoopCardSetPriorityController controller;
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
EXPECT_EQ(NotLoaded, db.getLoadStatus());
// A newly bootstrapped Continuumatrice profile contains a valid, but empty,
// XML v4 catalogue. Successful swap must not trap the visual browser on
// "Loading database..." when there are zero cards.
CardDatabaseData emptySnapshot;
db.swapInDatabaseData(std::move(emptySnapshot));
EXPECT_TRUE(db.getCardList().isEmpty());
EXPECT_EQ(Ok, db.getLoadStatus());
db.clear();
EXPECT_EQ(NotLoaded, db.getLoadStatus());
}
TEST(CardInfoTest, ContinuumIdentityAndStatsHaveNativeAndLegacyPropertyAccess)
{
auto native = CardInfo::newInstance("Native Continuum Card");
native->setProperty("identity", "The Shadow Queen");
native->setProperty("attack", "10");
native->setProperty("armour", "3");
native->setProperty("health", "12");
EXPECT_EQ(native->getIdentity(), "The Shadow Queen");
EXPECT_EQ(native->getAttack(), "10");
EXPECT_EQ(native->getArmour(), "3");
EXPECT_EQ(native->getHealth(), "12");
EXPECT_EQ(native->getAtkHp(), "10/12");
EXPECT_EQ(native->getPowTough(), "10/12"); // Legacy accessor remains useful.
auto legacyMse = CardInfo::newInstance("Legacy Continuum Card");
legacyMse->setProperty("alias", "The Guardian");
legacyMse->setProperty("strength", "9");
legacyMse->setProperty("armor", "4");
legacyMse->setProperty("health", "8");
EXPECT_EQ(legacyMse->getIdentity(), "The Guardian");
EXPECT_EQ(legacyMse->getAttack(), "9");
EXPECT_EQ(legacyMse->getArmour(), "4");
EXPECT_EQ(legacyMse->getHealth(), "8");
EXPECT_EQ(legacyMse->getAtkHp(), "9/8");
auto oldPt = CardInfo::newInstance("Old Imported Card");
oldPt->setProperty(Mtg::PowTough, "2/7");
EXPECT_EQ(oldPt->getAttack(), "2");
EXPECT_EQ(oldPt->getHealth(), "7");
EXPECT_EQ(oldPt->getAtkHp(), "2/7");
// Missing values should not be represented as false zeroes.
auto blank = CardInfo::newInstance("No Stats");
EXPECT_TRUE(blank->getAtkHp().isEmpty());
EXPECT_TRUE(blank->getArmour().isEmpty());
}
TEST(CardDatabaseTest, LazyPropertiesHashStreamsBlobAccessors)
{
QHash<QString, QString> props;

View file

@ -0,0 +1,83 @@
#include <gtest/gtest.h>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/essence_types.h>
#include <libcockatrice/utility/essence_counter.h>
#include <QHash>
#include <QString>
TEST(EssenceTypesTest, HasExactlySixteenCanonicalSymbols)
{
EXPECT_EQ(Essence::symbols(), QStringLiteral("MSOGEAQCJBRLVTPN"));
EXPECT_EQ(Essence::symbols().size(), 16);
EXPECT_TRUE(Essence::isSymbol(QChar('N'))); // N is colorless.
EXPECT_TRUE(Essence::isSymbol(QChar('J'))); // J is Jasper, between C and B.
EXPECT_FALSE(Essence::isSymbol(QChar('W'))); // MTG white is not Essence.
EXPECT_FALSE(Essence::isSymbol(QChar('U'))); // MTG blue is not Essence.
}
TEST(EssenceTypesTest, NetworkCounterNamesHaveStableRoundTrip)
{
EXPECT_EQ(Essence::symbols(), EssenceCounter::symbols());
int position = 1; // Counter id 0 is the existing life counter.
for (const QChar code : EssenceCounter::symbols()) {
const QString networkName = EssenceCounter::name(code);
EXPECT_TRUE(networkName.startsWith(QStringLiteral("essence:")));
EXPECT_EQ(EssenceCounter::codeFromName(networkName), code);
EXPECT_TRUE(EssenceCounter::isEssenceCounter(networkName));
++position;
}
EXPECT_EQ(position, 17);
EXPECT_TRUE(EssenceCounter::name(QChar('W')).isEmpty());
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("essence:W")));
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("essence:MM")));
EXPECT_FALSE(EssenceCounter::isEssenceCounter(QStringLiteral("life")));
EXPECT_EQ(EssenceCounter::clampBalance(-1), 0);
EXPECT_EQ(EssenceCounter::clampBalance(-900), 0);
EXPECT_EQ(EssenceCounter::clampBalance(0), 0);
EXPECT_EQ(EssenceCounter::clampBalance(19), 19);
}
TEST(EssenceTypesTest, ParsesAllBracedAndUnbracedTypes)
{
const QString raw = QStringLiteral("{M}{S}{O}{G}{E}{A}{Q}{C}{J}{B}{R}{L}{V}{T}{P}{N}");
EXPECT_EQ(Essence::parseCost(raw).join(QString()), Essence::symbols());
EXPECT_EQ(Essence::parseCost(QStringLiteral("MSN")).join(QString()), QStringLiteral("MSN"));
EXPECT_EQ(Essence::parseCost(QStringLiteral("{C}{J}{B}{N}")).join(QString()), QStringLiteral("CJBN"));
EXPECT_EQ(Essence::parseCost(QStringLiteral("{2}{N}{M}")).join(QStringLiteral("|")),
QStringLiteral("2|N|M"));
}
TEST(EssenceTypesTest, RejectsUnknownLegacyAndHybridCodes)
{
EXPECT_TRUE(Essence::parseCost(QStringLiteral("{W}{U}{Z}{M/N}")).isEmpty());
EXPECT_EQ(Essence::parseCost(QStringLiteral("{N}")), QStringList{QStringLiteral("N")});
}
TEST(EssenceTypesTest, NormalizesUniqueIdentityInCanonicalOrder)
{
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("NSSMMO")), QStringLiteral("MSON"));
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("N")), QStringLiteral("N"));
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("BJNJC")), QStringLiteral("CJBN"));
EXPECT_EQ(Essence::normalizeIdentity(QStringLiteral("WU")), QString());
}
TEST(EssenceTypesTest, NativePropertiesTakePrecedenceOverLegacyFallbacks)
{
const QHash<QString, QString> props = {
{QStringLiteral("essencecost"), QStringLiteral("{M}{N}")},
{QStringLiteral("essence"), QStringLiteral("NM")},
{QStringLiteral("essencevalue"), QStringLiteral("2")},
{QStringLiteral("manacost"), QStringLiteral("{W}{U}")},
{QStringLiteral("colors"), QStringLiteral("WU")},
{QStringLiteral("cmc"), QStringLiteral("9")}
};
const CardInfoPtr card = CardInfo::newInstance(QStringLiteral("Essence Test"),
QString(), false, props, {}, {}, {}, {});
ASSERT_TRUE(card);
EXPECT_EQ(card->getManaCost(), QStringLiteral("{M}{N}"));
EXPECT_EQ(card->getEssenceTypes(), QStringLiteral("MN"));
EXPECT_EQ(card->getColors(), QStringLiteral("MN"));
EXPECT_EQ(card->getCmc(), QStringLiteral("2"));
}

View file

@ -103,6 +103,54 @@ TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract)
EXPECT_LT(display.rowCount(), source.rowCount());
}
TEST(ModelContract, ContinuumCardStatsAreSortableAndLabeled)
{
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider paths;
NoopCardSetPriorityController setPriority;
CardDatabase database(nullptr, &prefs, &paths, &setPriority);
auto swift = CardInfo::newInstance("Swift");
swift->setProperty("identity", "Astra");
swift->setProperty("attack", "9");
swift->setProperty("armour", "10");
swift->setProperty("health", "2");
auto sturdy = CardInfo::newInstance("Sturdy");
sturdy->setProperty("identity", "Zephyr");
sturdy->setProperty("attack", "10");
sturdy->setProperty("armour", "2");
sturdy->setProperty("health", "12");
database.addCard(swift);
database.addCard(sturdy);
CardDatabaseModel source(&database, false);
CardDatabaseDisplayModel display;
display.setSourceModel(&source);
ASSERT_EQ(source.columnCount(), CardDatabaseModel::ColumnCount);
ASSERT_EQ(display.rowCount(), 2);
EXPECT_EQ(source.headerData(CardDatabaseModel::AtkHpColumn, Qt::Horizontal).toString(), "ATK/HP");
EXPECT_EQ(source.headerData(CardDatabaseModel::IdentityColumn, Qt::Horizontal).toString(), "Identity");
EXPECT_EQ(source.headerData(CardDatabaseModel::AttackColumn, Qt::Horizontal).toString(), "Attack");
EXPECT_EQ(source.headerData(CardDatabaseModel::ArmourColumn, Qt::Horizontal).toString(), "Armour");
EXPECT_EQ(source.headerData(CardDatabaseModel::HealthColumn, Qt::Horizontal).toString(), "Health");
const auto firstName = [&display] { return display.index(0, CardDatabaseModel::NameColumn).data().toString(); };
display.sort(CardDatabaseModel::AttackColumn, Qt::AscendingOrder);
EXPECT_EQ(firstName(), "Swift") << "9 ATK should precede 10 ATK";
display.sort(CardDatabaseModel::ArmourColumn, Qt::AscendingOrder);
EXPECT_EQ(firstName(), "Sturdy") << "2 Armour should precede 10 Armour";
display.sort(CardDatabaseModel::HealthColumn, Qt::AscendingOrder);
EXPECT_EQ(firstName(), "Swift") << "2 HP should precede 12 HP";
display.sort(CardDatabaseModel::AtkHpColumn, Qt::AscendingOrder);
EXPECT_EQ(firstName(), "Swift") << "ATK/HP should sort attack and health numerically";
display.sort(CardDatabaseModel::IdentityColumn, Qt::DescendingOrder);
EXPECT_EQ(firstName(), "Sturdy") << "Identity should sort text from the alias/identity property";
}
TEST(ModelContract, SetsModelKeepsContract)
{
CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(),