mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
[DeckList] Add custom deck zones to the deck tree (#7176)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [DeckList] Add custom deck zones to the deck tree Introduce user-definable zones nested under a board zone (main, side or maybeboard) so players can organize cards inside a board without changing board semantics. - addCustomZone, renameCustomZone, moveCustomZone and removeCustomZone manage zones. Names are unique across the whole deck and the standard zone names (main/side/maybeboard/tokens) stay reserved. - Board zones are created lazily on first use. - getZoneObjFromName resolves custom names to their nested node so addCard and XML loading route cards into them. Unknown names keep creating legacy top-level zones. - deleteNode keeps empty custom zones alive and only prunes empty board zones. - New deck_list_zones test suite locks hash parity with flat decks, sideboard size accounting, maybeboard exclusion from plain export and native-format round-trips. Took 17 minutes Took 11 minutes * Extract to function Took 4 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
e12293bb28
commit
0a09884c78
6 changed files with 559 additions and 2 deletions
|
|
@ -145,7 +145,8 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode
|
|||
if (index != -1) {
|
||||
delete rootNode->takeAt(index);
|
||||
|
||||
if (rootNode->empty()) {
|
||||
// Empty custom zones are kept while empty board zones get pruned.
|
||||
if (rootNode->empty() && rootNode->getParent() == root) {
|
||||
deleteNode(rootNode, rootNode->getParent());
|
||||
}
|
||||
|
||||
|
|
@ -188,15 +189,157 @@ void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *,
|
|||
|
||||
/**
|
||||
* Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist.
|
||||
*
|
||||
* Top-level zones take precedence, then deck-unique custom zones nested under boards
|
||||
* are resolved. Unknown names create a new top-level zone (legacy behavior).
|
||||
*/
|
||||
InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const
|
||||
{
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
if (node->getName() == zoneName) {
|
||||
if (node && node->getName() == zoneName) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto *customZone = findCustomZoneByName(zoneName)) {
|
||||
return customZone;
|
||||
}
|
||||
|
||||
return new InnerDecklistNode(zoneName, root);
|
||||
}
|
||||
|
||||
InnerDecklistNode *DecklistNodeTree::findBoardZone(const QString &boardZoneName) const
|
||||
{
|
||||
return dynamic_cast<InnerDecklistNode *>(root->findChild(boardZoneName));
|
||||
}
|
||||
|
||||
InnerDecklistNode *DecklistNodeTree::findOrCreateBoardZone(const QString &boardZoneName)
|
||||
{
|
||||
auto *boardZone = findBoardZone(boardZoneName);
|
||||
if (!boardZone &&
|
||||
(boardZoneName == DECK_ZONE_MAYBEBOARD || boardZoneName == DECK_ZONE_MAIN || boardZoneName == DECK_ZONE_SIDE)) {
|
||||
// The boards are lazy zones: they only exist once cards or custom zones need them.
|
||||
boardZone = new InnerDecklistNode(boardZoneName, root);
|
||||
}
|
||||
return boardZone;
|
||||
}
|
||||
|
||||
InnerDecklistNode *DecklistNodeTree::addCustomZone(const QString &boardZoneName, const QString &zoneName)
|
||||
{
|
||||
if (hasZoneName(zoneName)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *boardZone = findOrCreateBoardZone(boardZoneName);
|
||||
|
||||
if (!boardZone) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new InnerDecklistNode(zoneName, boardZone);
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::renameCustomZone(const QString &oldZoneName, const QString &newZoneName)
|
||||
{
|
||||
if (hasZoneName(newZoneName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *zone = findCustomZoneByName(oldZoneName);
|
||||
if (!zone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
zone->setName(newZoneName);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName)
|
||||
{
|
||||
auto *zone = findCustomZoneByName(zoneName);
|
||||
if (!zone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *currentBoardZone = zone->getParent();
|
||||
if (currentBoardZone && currentBoardZone->getName() == newBoardZoneName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *newBoardZone = findOrCreateBoardZone(newBoardZoneName);
|
||||
if (!newBoardZone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentBoardZone->removeOne(zone);
|
||||
newBoardZone->append(zone);
|
||||
zone->setParent(newBoardZone);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::removeCustomZone(const QString &zoneName)
|
||||
{
|
||||
auto *zone = findCustomZoneByName(zoneName);
|
||||
if (!zone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detach and delete without pruning the board zone.
|
||||
auto *boardZone = zone->getParent();
|
||||
boardZone->removeOne(zone);
|
||||
delete zone;
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<const InnerDecklistNode *> DecklistNodeTree::getCustomZones(const QString &boardZoneName) const
|
||||
{
|
||||
QList<const InnerDecklistNode *> result;
|
||||
|
||||
auto *boardZone = findBoardZone(boardZoneName);
|
||||
if (!boardZone) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < boardZone->size(); i++) {
|
||||
if (auto *customZone = dynamic_cast<InnerDecklistNode *>(boardZone->at(i))) {
|
||||
result.append(customZone);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
InnerDecklistNode *DecklistNodeTree::findCustomZoneByName(const QString &zoneName) const
|
||||
{
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
auto *boardZone = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
if (!boardZone) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < boardZone->size(); j++) {
|
||||
auto *customZone = dynamic_cast<InnerDecklistNode *>(boardZone->at(j));
|
||||
if (customZone && customZone->getName() == zoneName) {
|
||||
return customZone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::hasZoneName(const QString &zoneName) const
|
||||
{
|
||||
// The standard zones are reserved names even before they are created lazily.
|
||||
if (zoneName == DECK_ZONE_MAIN || zoneName == DECK_ZONE_SIDE || zoneName == DECK_ZONE_MAYBEBOARD ||
|
||||
zoneName == DECK_ZONE_TOKENS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (root->findChild(zoneName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return findCustomZoneByName(zoneName) != nullptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,43 @@ public:
|
|||
const bool formatLegal = true);
|
||||
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Creates a new custom zone nested under a board zone.
|
||||
*
|
||||
* Custom zone names must be unique across the whole deck so that cards can be
|
||||
* added to a custom zone without specifying its board zone.
|
||||
*
|
||||
* @param boardZoneName Name of the board zone (e.g. DECK_ZONE_MAIN).
|
||||
* @param zoneName Name of the custom zone.
|
||||
* @return The created zone node, or nullptr if the name is already in use.
|
||||
*/
|
||||
InnerDecklistNode *addCustomZone(const QString &boardZoneName, const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Renames a custom zone.
|
||||
* @return true on success, false if the zone was not found or the new name is taken.
|
||||
*/
|
||||
bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName);
|
||||
|
||||
/**
|
||||
* @brief Moves a custom zone (and all its cards) to another board zone.
|
||||
* @return true on success, false if the zone or the new board zone was not found.
|
||||
*/
|
||||
bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName);
|
||||
|
||||
/**
|
||||
* @brief Removes a custom zone and all its cards.
|
||||
* @return true if the zone was found and removed.
|
||||
*/
|
||||
bool removeCustomZone(const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Gets all custom zones nested under a board zone.
|
||||
* @param boardZoneName Name of the board zone.
|
||||
* @return The custom zones, in insertion order.
|
||||
*/
|
||||
QList<const InnerDecklistNode *> getCustomZones(const QString &boardZoneName) const;
|
||||
|
||||
/**
|
||||
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
||||
*
|
||||
|
|
@ -88,6 +125,10 @@ public:
|
|||
private:
|
||||
// Helpers for traversing the tree
|
||||
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
|
||||
InnerDecklistNode *findBoardZone(const QString &boardZoneName) const;
|
||||
InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName);
|
||||
InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const;
|
||||
bool hasZoneName(const QString &zoneName) const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
|
||||
|
|
|
|||
|
|
@ -142,6 +142,12 @@ public:
|
|||
return parent;
|
||||
}
|
||||
|
||||
/** @param newParent Reparent this node. The new parent takes ownership. */
|
||||
void setParent(InnerDecklistNode *newParent)
|
||||
{
|
||||
parent = newParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compute the depth of this node in the tree.
|
||||
* @return Distance from the root (root = 0, children = 1, etc.).
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ target_link_libraries(
|
|||
|
||||
add_subdirectory(card_zone_algorithms)
|
||||
add_subdirectory(carddatabase)
|
||||
add_subdirectory(deck_list_zones)
|
||||
add_subdirectory(loading_from_clipboard)
|
||||
add_subdirectory(movecard_tests)
|
||||
add_subdirectory(oracle)
|
||||
|
|
|
|||
10
tests/deck_list_zones/CMakeLists.txt
Normal file
10
tests/deck_list_zones/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
add_executable(deck_list_zones_test deck_list_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_zones_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
deck_list_zones_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_zones_test COMMAND deck_list_zones_test)
|
||||
356
tests/deck_list_zones/deck_list_zones_test.cpp
Normal file
356
tests/deck_list_zones/deck_list_zones_test.cpp
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
/**
|
||||
* @file deck_list_zones_test.cpp
|
||||
* @brief Tests for custom deck zones (deck-unique zones nested under a board zone).
|
||||
*
|
||||
* Custom zones allow players to organize cards within a board (e.g. "Removal" under
|
||||
* the mainboard) without changing the board semantics: cards in a custom zone under
|
||||
* "main" are still mainboard cards for hashing, sideboard size, legality and export.
|
||||
*/
|
||||
|
||||
#include <QStringList>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Collects (board zone name, card node) pairs via forEachCard.
|
||||
*/
|
||||
struct BoardCardPair
|
||||
{
|
||||
QString boardZone;
|
||||
QString cardName;
|
||||
int amount;
|
||||
};
|
||||
|
||||
QList<BoardCardPair> collectBoardCardPairs(const DeckList &deck)
|
||||
{
|
||||
QList<BoardCardPair> result;
|
||||
deck.forEachCard([&result](InnerDecklistNode *boardZone, DecklistCardNode *card) {
|
||||
result.append({boardZone->getName(), card->getName(), card->getNumber()});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPair(const QList<BoardCardPair> &pairs, const QString &boardZone, const QString &cardName)
|
||||
{
|
||||
for (const auto &pair : pairs) {
|
||||
if (pair.boardZone == boardZone && pair.cardName == cardName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalCards(const QList<BoardCardPair> &pairs)
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &pair : pairs) {
|
||||
total += pair.amount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================================================
|
||||
// Zone creation
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, AddCustomZoneNestsUnderBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
auto *zone = tree->addCustomZone(DECK_ZONE_MAIN, "Removal");
|
||||
ASSERT_NE(zone, nullptr);
|
||||
EXPECT_EQ(zone->getName(), QString("Removal"));
|
||||
ASSERT_NE(zone->getParent(), nullptr);
|
||||
EXPECT_EQ(zone->getParent()->getName(), QString(DECK_ZONE_MAIN));
|
||||
|
||||
// The custom zone is nested, not a new top-level zone.
|
||||
auto topLevelZones = tree->getZoneNodes();
|
||||
QStringList topLevelNames;
|
||||
for (auto *node : topLevelZones) {
|
||||
topLevelNames.append(node->getName());
|
||||
}
|
||||
EXPECT_FALSE(topLevelNames.contains("Removal"));
|
||||
|
||||
// It is discoverable through the board zone.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
EXPECT_EQ(customZones.first()->getName(), QString("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, CustomZoneNamesAreDeckUnique)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
// Same name on a different board is rejected.
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_SIDE, "Removal"), nullptr);
|
||||
// A name that collides with a built-in board zone is rejected.
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAIN), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_SIDE), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAYBEBOARD), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_TOKENS), nullptr);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, AddCustomZoneUnknownBoardFails)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
EXPECT_EQ(tree->addCustomZone("not_a_board", "Removal"), nullptr);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MaybeboardIsLazilyCreated)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
|
||||
// The maybeboard board zone now exists, with the custom zone nested inside.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAYBEBOARD);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
EXPECT_EQ(customZones.first()->getName(), QString("Candidates"));
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Card placement
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, AddCardToCustomZoneKeepsBoardSemantics)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 4, "Removal", -1);
|
||||
|
||||
// The card is reported as a mainboard card.
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
|
||||
// It is physically nested inside the custom zone.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
ASSERT_EQ(customZones.first()->size(), 1);
|
||||
auto *card = dynamic_cast<const DecklistCardNode *>(customZones.first()->at(0));
|
||||
ASSERT_NE(card, nullptr);
|
||||
EXPECT_EQ(card->getName(), QString("Lightning Bolt"));
|
||||
EXPECT_EQ(card->getNumber(), 4);
|
||||
|
||||
// Zone-scoped queries include it.
|
||||
EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).contains("Lightning Bolt"));
|
||||
EXPECT_FALSE(deck.getCardList({DECK_ZONE_SIDE}).contains("Lightning Bolt"));
|
||||
EXPECT_EQ(deck.getCardNodes({DECK_ZONE_MAIN}).size(), 1);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, LegacyTopLevelZoneStillWorks)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
// Unknown zone names create a legacy top-level zone (backwards compatibility).
|
||||
tree->addCard("Legacy Card", 2, "custom_legacy_zone", -1);
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, "custom_legacy_zone", "Legacy Card"));
|
||||
EXPECT_EQ(deck.getCardList({}).count("Legacy Card"), 1);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Zone management
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, RenameCustomZone)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->renameCustomZone("Removal", "Bolt Zone"));
|
||||
EXPECT_TRUE(hasPair(collectBoardCardPairs(deck), DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Bolt Zone"));
|
||||
|
||||
// Renaming to a taken name fails.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Other"), nullptr);
|
||||
EXPECT_FALSE(tree->renameCustomZone("Bolt Zone", "Other"));
|
||||
// Renaming a nonexistent zone fails.
|
||||
EXPECT_FALSE(tree->renameCustomZone("Ghost Zone", "Whatever"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MoveCustomZoneMovesCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE));
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
|
||||
// The custom zone is now nested under side.
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0);
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_SIDE).size(), 1);
|
||||
|
||||
// Moving to an unknown board fails.
|
||||
EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, RemoveCustomZoneRemovesCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->removeCustomZone("Removal"));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0);
|
||||
EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).isEmpty());
|
||||
EXPECT_FALSE(tree->removeCustomZone("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, EmptyCustomZoneIsKeptOnCardDeletion)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
auto *card = tree->addCard("Lightning Bolt", 1, "Removal", -1);
|
||||
|
||||
// Deleting the last card must not delete the empty custom zone.
|
||||
EXPECT_TRUE(tree->deleteNode(card));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Deck-wide behavior
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, HashCountsCustomZoneCardsByBoard)
|
||||
{
|
||||
// Deck A: cards directly in main and side.
|
||||
DeckList direct;
|
||||
direct.addCard("Mountain", DECK_ZONE_MAIN);
|
||||
direct.addCard("Lightning Bolt", DECK_ZONE_MAIN);
|
||||
direct.addCard("Island", DECK_ZONE_SIDE);
|
||||
|
||||
// Deck B: identical, but organized in custom zones.
|
||||
DeckList organized;
|
||||
auto *tree = organized.getTree();
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Lands"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Mountain", 1, "Lands", -1);
|
||||
tree->addCard("Lightning Bolt", 1, "Removal", -1);
|
||||
tree->addCard("Island", 1, "Side Tech", -1);
|
||||
|
||||
EXPECT_EQ(direct.getDeckHash(), organized.getDeckHash());
|
||||
}
|
||||
|
||||
TEST(DeckListZones, SideboardSizeCountsCustomZoneCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Island", 3, "Side Tech", -1);
|
||||
tree->addCard("Forest", 2, DECK_ZONE_SIDE, -1);
|
||||
|
||||
EXPECT_EQ(deck.getSideboardSize(), 5);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, PlainExportIncludesMainAndSideCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
tree->addCard("Island", 1, "Side Tech", -1);
|
||||
|
||||
const QString plain = deck.writeToString_Plain(false, false);
|
||||
EXPECT_TRUE(plain.contains("2 Lightning Bolt"));
|
||||
EXPECT_TRUE(plain.contains("1 Island"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, PlainExportSkipsMaybeboardCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
tree->addCard("Wish Card", 4, "Candidates", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
const QString plain = deck.writeToString_Plain(false, false);
|
||||
EXPECT_FALSE(plain.contains("Wish Card"));
|
||||
EXPECT_TRUE(plain.contains("1 Mountain"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, NativeRoundTripPreservesCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
tree->addCard("Island", 3, "Side Tech", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
// Round-trip through the native format.
|
||||
DeckList restored(deck.writeToString_Native());
|
||||
auto *restoredTree = restored.getTree();
|
||||
|
||||
EXPECT_EQ(restored.getDeckHash(), deck.getDeckHash());
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Removal"));
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_SIDE).size(), 1);
|
||||
|
||||
auto pairs = collectBoardCardPairs(restored);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Island"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Mountain"));
|
||||
EXPECT_EQ(totalCards(pairs), 6);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MaybeboardCustomZoneCardsAreExcludedFromHash)
|
||||
{
|
||||
// Maybeboard cards are editor-only and must never affect the deck hash.
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
tree->addCard("Wish Card", 4, "Candidates", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
DeckList expected;
|
||||
expected.addCard("Mountain", DECK_ZONE_MAIN);
|
||||
|
||||
EXPECT_EQ(deck.getDeckHash(), expected.getDeckHash());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue