mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Client] DeckSyncProfile + sidecar + DeckDiff with tests
This commit is contained in:
parent
11fdfb4610
commit
be072ffd68
12 changed files with 742 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ set(HEADERS
|
|||
libcockatrice/deck_list/tree/abstract_deck_list_node.h
|
||||
libcockatrice/deck_list/tree/deck_list_card_node.h
|
||||
libcockatrice/deck_list/tree/inner_deck_list_node.h
|
||||
libcockatrice/deck_list/deck_diff.h
|
||||
libcockatrice/deck_list/deck_file_format.h
|
||||
libcockatrice/deck_list/deck_list.h
|
||||
libcockatrice/deck_list/deck_list_history_manager.h
|
||||
|
|
@ -25,6 +26,7 @@ add_library(
|
|||
libcockatrice/deck_list/tree/abstract_deck_list_node.cpp
|
||||
libcockatrice/deck_list/tree/deck_list_card_node.cpp
|
||||
libcockatrice/deck_list/tree/inner_deck_list_node.cpp
|
||||
libcockatrice/deck_list/deck_diff.cpp
|
||||
libcockatrice/deck_list/deck_file_format.cpp
|
||||
libcockatrice/deck_list/deck_list.cpp
|
||||
libcockatrice/deck_list/deck_list_history_manager.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
#include "deck_diff.h"
|
||||
|
||||
#include "deck_list.h"
|
||||
#include "tree/deck_list_card_node.h"
|
||||
#include "tree/inner_deck_list_node.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
using ZoneCardKey = QPair<QString, QString>;
|
||||
|
||||
QMap<ZoneCardKey, int> zoneCardCounts(const DeckList &deck)
|
||||
{
|
||||
QMap<ZoneCardKey, int> counts;
|
||||
deck.forEachCard([&counts](InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
counts[{zone->getName(), card->getName()}] += card->getNumber();
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
bool changeLessThan(const DeckCardChange &a, const DeckCardChange &b)
|
||||
{
|
||||
if (a.zone != b.zone) {
|
||||
return a.zone < b.zone;
|
||||
}
|
||||
return a.cardName < b.cardName;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DeckDiffResult DeckDiff::diff(const DeckList &oldDeck, const DeckList &newDeck)
|
||||
{
|
||||
const QMap<ZoneCardKey, int> oldCounts = zoneCardCounts(oldDeck);
|
||||
const QMap<ZoneCardKey, int> newCounts = zoneCardCounts(newDeck);
|
||||
|
||||
QSet<ZoneCardKey> keys(oldCounts.keyBegin(), oldCounts.keyEnd());
|
||||
keys.unite(QSet<ZoneCardKey>(newCounts.keyBegin(), newCounts.keyEnd()));
|
||||
|
||||
DeckDiffResult result;
|
||||
for (const ZoneCardKey &key : keys) {
|
||||
const int oldCount = oldCounts.value(key, 0);
|
||||
const int newCount = newCounts.value(key, 0);
|
||||
if (oldCount != newCount) {
|
||||
result.changes.append({key.first, key.second, oldCount, newCount});
|
||||
}
|
||||
}
|
||||
std::sort(result.changes.begin(), result.changes.end(), changeLessThan);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckDiff::canAutoMerge(const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges)
|
||||
{
|
||||
QSet<ZoneCardKey> localKeys;
|
||||
for (const DeckCardChange &change : localChanges.changes) {
|
||||
localKeys.insert({change.zone, change.cardName});
|
||||
}
|
||||
for (const DeckCardChange &change : remoteChanges.changes) {
|
||||
if (localKeys.contains({change.zone, change.cardName})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DeckList
|
||||
DeckDiff::merge(const DeckList &baseDeck, const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges)
|
||||
{
|
||||
QMap<ZoneCardKey, int> counts = zoneCardCounts(baseDeck);
|
||||
for (const DeckCardChange &change : localChanges.changes) {
|
||||
counts[{change.zone, change.cardName}] = change.newCount;
|
||||
}
|
||||
for (const DeckCardChange &change : remoteChanges.changes) {
|
||||
counts[{change.zone, change.cardName}] = change.newCount;
|
||||
}
|
||||
|
||||
DeckList merged(baseDeck.getMetadata(), DecklistNodeTree(), baseDeck.getSideboardPlans());
|
||||
QList<ZoneCardKey> keys = counts.keys();
|
||||
std::sort(keys.begin(), keys.end());
|
||||
for (const ZoneCardKey &key : keys) {
|
||||
const int count = qMax(0, counts.value(key));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
merged.addCard(key.second, key.first);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
71
libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h
Normal file
71
libcockatrice_deck_list/libcockatrice/deck_list/deck_diff.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @file deck_diff.h
|
||||
* @brief Card-level diff and three-way merge for deck collection sync.
|
||||
*/
|
||||
|
||||
#ifndef DECK_DIFF_H
|
||||
#define DECK_DIFF_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
class DeckList;
|
||||
|
||||
/** @brief A single card-count change within one zone. */
|
||||
struct DeckCardChange
|
||||
{
|
||||
QString zone; ///< Zone name, e.g. "main".
|
||||
QString cardName; ///< Card name.
|
||||
int oldCount = 0; ///< Copies in the old deck.
|
||||
int newCount = 0; ///< Copies in the new deck.
|
||||
/** @brief Copies added (positive) or removed (negative). */
|
||||
[[nodiscard]] int delta() const
|
||||
{
|
||||
return newCount - oldCount;
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Card-level difference between two decks, sorted by zone then card name. */
|
||||
struct DeckDiffResult
|
||||
{
|
||||
QList<DeckCardChange> changes;
|
||||
/** @brief True when both decks hold the same cards in the same zones. */
|
||||
[[nodiscard]] bool isEmpty() const
|
||||
{
|
||||
return changes.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class DeckDiff
|
||||
* @brief Pure card-count diff and three-way merge for deck sync.
|
||||
*
|
||||
* Compares card names and copy counts per zone only; printing information
|
||||
* (set, collector number, provider) is not tracked, so a merged deck keeps
|
||||
* base printings with merged counts.
|
||||
*/
|
||||
class DeckDiff
|
||||
{
|
||||
public:
|
||||
/** @brief Diffs oldDeck against newDeck. */
|
||||
static DeckDiffResult diff(const DeckList &oldDeck, const DeckList &newDeck);
|
||||
/**
|
||||
* @brief True when both change sets touch disjoint (zone, card) keys and a merge needs no arbitration.
|
||||
*/
|
||||
static bool canAutoMerge(const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges);
|
||||
/**
|
||||
* @brief Applies both change sets onto a copy of baseDeck.
|
||||
*
|
||||
* @precondition localChanges and remoteChanges were both computed with
|
||||
* diff() against baseDeck. Stale or foreign change sets silently
|
||||
* resurrect or delete cards.
|
||||
*
|
||||
* Counts are clamped at zero. On overlapping keys the remote change wins;
|
||||
* callers offer this result as "Use Merge" only when canAutoMerge is true.
|
||||
* Metadata and sideboard plans come from baseDeck.
|
||||
*/
|
||||
static DeckList
|
||||
merge(const DeckList &baseDeck, const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges);
|
||||
};
|
||||
|
||||
#endif // DECK_DIFF_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue