[DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander'

Took 2 hours 45 minutes

Took 12 seconds
This commit is contained in:
Lukas Brübach 2025-12-14 09:09:26 +01:00 committed by Brübach, Lukas
parent c02cf5e89e
commit 9759cdd07f
18 changed files with 892 additions and 6 deletions

View file

@ -275,6 +275,13 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp

View file

@ -1,6 +1,8 @@
#include "deck_editor_deck_dock_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_api_accessor.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include "deck_list_style_proxy.h"
#include "deck_state_manager.h"
@ -131,6 +133,24 @@ void DeckEditorDeckDockWidget::createDeckDock()
formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded
// --- Commander bracket row (hidden, unless format is 'commander') ---
bracketLabel = new QLabel(tr("Bracket:"), this);
bracketValueLabel = new QLabel(this);
bracketValueLabel->setText("-");
bracketValueLabel->setObjectName("bracketValueLabel");
bracketInfoButton = new QToolButton(this);
bracketInfoButton->setText("?");
bracketInfoButton->setAutoRaise(true);
bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this);
bracketRefreshButton->setIcon(QPixmap("theme:icons/reload"));
bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &DeckEditorDeckDockWidget::requestBracketEstimate);
commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit;
@ -216,13 +236,23 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(formatLabel, 2, 0);
upperLayout->addWidget(formatComboBox, 2, 1);
upperLayout->addWidget(bannerCardLabel, 3, 0);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(bracketLabel, 3, 0);
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
auto *bracketRow = new QHBoxLayout;
bracketRow->addWidget(bracketValueLabel);
bracketRow->addWidget(bracketInfoButton);
bracketRow->addWidget(bracketRefreshButton);
bracketRow->addStretch();
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
upperLayout->addLayout(bracketRow, 3, 1);
upperLayout->addWidget(bannerCardLabel, 4, 0);
upperLayout->addWidget(bannerCardComboBox, 4, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1");
@ -280,6 +310,47 @@ void DeckEditorDeckDockWidget::createDeckDock()
}
}
void DeckEditorDeckDockWidget::requestBracketEstimate()
{
bracketRefreshButton->setEnabled(false);
bracketInfoButton->setEnabled(false);
bracketValueLabel->setText(tr("Calculating…"));
requestId = CommanderSpellbookApiAccessor::instance().estimateBracket(*deckModel->getDeckList(), this);
connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this,
&DeckEditorDeckDockWidget::onEstimateBracketFinished);
}
void DeckEditorDeckDockWidget::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id,
QObject *requester,
const EstimateBracketResult &result)
{
if (requester != this || static_cast<int>(id) != requestId) {
return;
}
BracketExplainer explainer;
lastBracketExplanation = explainer.explain(result);
// Display bracket
bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToOfficialString(result.bracketTag));
bracketRefreshButton->setEnabled(true);
// Build tooltip
QString tooltip;
for (const auto &section : lastBracketExplanation.sections) {
tooltip += "<b>" + section.title + "</b><br>";
for (const auto &line : section.bulletPoints) {
tooltip += "" + line + "<br>";
}
tooltip += "<br>";
}
bracketInfoButton->setToolTip(tooltip);
bracketInfoButton->setEnabled(!tooltip.isEmpty());
}
void DeckEditorDeckDockWidget::initializeFormats()
{
QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys();
@ -300,15 +371,55 @@ void DeckEditorDeckDockWidget::initializeFormats()
// Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1);
}
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
QString formatKey;
if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString();
deckStateManager->setFormat(formatKey);
} else {
deckStateManager->setFormat(""); // clear format if deselected
}
emit deckModified();
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
bracketLabel->setVisible(isCommander);
bracketValueLabel->setVisible(isCommander);
bracketInfoButton->setVisible(isCommander);
bracketRefreshButton->setVisible(isCommander);
if (!isCommander) {
bracketValueLabel->setText("-");
bracketInfoButton->setToolTip({});
bracketInfoButton->setEnabled(false);
bracketRefreshButton->setEnabled(false);
} else {
bracketRefreshButton->setEnabled(true);
maybeAutoEstimateBracket();
}
});
maybeAutoEstimateBracket();
}
void DeckEditorDeckDockWidget::maybeAutoEstimateBracket()
{
const QString formatKey = deckModel->getDeckList()->getGameFormat();
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
if (!isCommander) {
return;
}
// Avoid firing if we already have a result or a request in flight
if (!bracketRefreshButton->isEnabled()) {
return;
}
// Defer to avoid races during init / model rebuild
QTimer::singleShot(0, this, &DeckEditorDeckDockWidget::requestBracketEstimate);
}
ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@ -740,6 +851,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:"));
bracketInfoButton->setToolTip(tr("Why this bracket?"));
bracketRefreshButton->setToolTip(tr("Recalculate bracket"));
hashLabel1->setText(tr("Hash:"));

View file

@ -10,6 +10,7 @@
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../../key_signals.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include "../utility/custom_line_edit.h"
#include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include "deck_list_history_manager_widget.h"
@ -21,6 +22,7 @@
#include <QTreeView>
#include <libcockatrice/card/card_info.h>
class EstimateBracketResult;
class DeckListModel;
class AbstractTabDeckEditor;
class DeckEditorDeckDockWidget : public QDockWidget
@ -33,6 +35,7 @@ public:
QTreeView *deckView;
QComboBox *bannerCardComboBox;
void createDeckDock();
void requestBracketEstimate();
ExactCard getCurrentCard();
void retranslateUi();
@ -59,6 +62,8 @@ public slots:
void actSwapSelection();
void actRemoveCard();
void initializeFormats();
void maybeAutoEstimateBracket();
void onEstimateBracketFinished(quint64 id, QObject *requester, const EstimateBracketResult &result);
signals:
void selectedCardChanged(const ExactCard &card);
@ -89,6 +94,15 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
QLabel *bracketLabel;
QLabel *bracketValueLabel;
QToolButton *bracketInfoButton;
QToolButton *bracketRefreshButton;
BracketExplanation lastBracketExplanation;
int requestId;
DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);

View file

@ -0,0 +1,21 @@
#include "card_in_deck_request.h"
void CardInDeckRequest::fromJson(const QJsonObject &json)
{
card = json.value("card").toString();
quantity = json.value("quantity").toInt();
}
QJsonObject CardInDeckRequest::toJson() const
{
QJsonObject json;
json.insert("card", card);
json.insert("quantity", quantity);
return json;
}
void CardInDeckRequest::debugPrint() const
{
qDebug() << "Card:" << card;
qDebug() << "Quantity:" << quantity;
}

View file

@ -0,0 +1,23 @@
#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H
#define COCKATRICE_CARD_IN_DECK_REQUEST_H
#include <QJsonObject>
class CardInDeckRequest
{
public:
// Constructor
CardInDeckRequest() = default;
// Parse deck-related data from JSON
void fromJson(const QJsonObject &json);
QJsonObject toJson() const;
// Debug method for logging
void debugPrint() const;
private:
QString card;
int quantity;
};
#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H

View file

@ -0,0 +1,79 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#include <QString>
namespace CommanderSpellbookBracketTag
{
enum class BracketTag
{
Ruthless,
Spicy,
Powerful,
Oddball,
PreconAppropriate,
Casual,
Unknown
};
inline static BracketTag bracketTagFromString(const QString &s)
{
if (s == "R")
return BracketTag::Ruthless;
if (s == "S")
return BracketTag::Spicy;
if (s == "P")
return BracketTag::Powerful;
if (s == "O")
return BracketTag::Oddball;
if (s == "PA")
return BracketTag::PreconAppropriate;
if (s == "C")
return BracketTag::Casual;
return BracketTag::Unknown;
}
inline static QString bracketTagToString(BracketTag tag)
{
switch (tag) {
case BracketTag::Ruthless:
return "Ruthless";
case BracketTag::Spicy:
return "Spicy";
case BracketTag::Powerful:
return "Powerful";
case BracketTag::Oddball:
return "Oddball";
case BracketTag::PreconAppropriate:
return "Precon Appropriate";
case BracketTag::Casual:
return "Casual";
case BracketTag::Unknown:
return "Unknown";
}
return {};
}
inline static QString bracketTagToOfficialString(BracketTag tag)
{
switch (tag) {
case BracketTag::Ruthless:
return "[5] cEDH";
case BracketTag::Spicy:
return "[4] Optimized";
case BracketTag::Powerful:
return "[3] Upgraded";
case BracketTag::Oddball:
return "[2] Core";
case BracketTag::PreconAppropriate:
return "[1] Exhibition";
case BracketTag::Casual:
return "[1] Casual";
case BracketTag::Unknown:
return "Unknown";
}
return {};
}
} // namespace CommanderSpellbookBracketTag
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H

View file

@ -0,0 +1,22 @@
#include "commander_spellbook_card_result.h"
void CommanderSpellbookCardResult::fromJson(const QJsonObject &json)
{
id = json.value("id").toString();
name = json.value("name").toString();
oracleId = json.value("oracleId").toString();
spoiler = json.value("spoiler").toBool();
typeLine = json.value("typeLine").toString();
imageUriFrontPng = json.value("imageUriFrontPng").toString();
imageUriFrontLarge = json.value("imageUriFrontLarge").toString();
imageUriFrontNormal = json.value("imageUriFrontNormal").toString();
imageUriFrontSmall = json.value("imageUriFrontSmall").toString();
imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString();
imageUriBackPng = json.value("imageUriBackPng").toString();
imageUriBackLarge = json.value("imageUriBackLarge").toString();
imageUriBackNormal = json.value("imageUriBackNormal").toString();
imageUriBackSmall = json.value("imageUriBackSmall").toString();
imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString();
}

View file

@ -0,0 +1,30 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#include <QJsonObject>
#include <QString>
class CommanderSpellbookCardResult
{
public:
void fromJson(const QJsonObject &json);
QString id;
QString name;
QString oracleId;
bool spoiler = false;
QString typeLine;
QString imageUriFrontPng;
QString imageUriFrontLarge;
QString imageUriFrontNormal;
QString imageUriFrontSmall;
QString imageUriFrontArtCrop;
QString imageUriBackPng;
QString imageUriBackLarge;
QString imageUriBackNormal;
QString imageUriBackSmall;
QString imageUriBackArtCrop;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H

View file

@ -0,0 +1,118 @@
#include "commander_spellbook_deck_request.h"
#include <QDebug>
#include <QJsonArray>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
void CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json)
{
mainDeck.clear();
commanderDeck.clear();
// Main deck
const QJsonArray mainArray = json.value("main").toArray();
for (const QJsonValue &value : mainArray) {
if (!value.isObject()) {
continue;
}
CardInDeckRequest card;
card.fromJson(value.toObject());
mainDeck.append(card);
// Max size allowed by commanderspellbook
if (mainDeck.size() >= 600) {
break;
}
}
// Commanders
const QJsonArray commanderArray = json.value("commanders").toArray();
for (const QJsonValue &value : commanderArray) {
if (!value.isObject()) {
continue;
}
CardInDeckRequest card;
card.fromJson(value.toObject());
commanderDeck.append(card);
// Max size allowed by commanderspellbook
if (commanderDeck.size() >= 12) {
break;
}
}
}
QJsonObject CommanderSpellbookDeckRequest::toJson() const
{
QJsonObject json;
QJsonArray mainArray;
for (const CardInDeckRequest &card : mainDeck) {
mainArray.append(card.toJson());
}
QJsonArray commanderArray;
for (const CardInDeckRequest &card : commanderDeck) {
commanderArray.append(card.toJson());
}
json.insert("main", mainArray);
json.insert("commanders", commanderArray);
return json;
}
void CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck)
{
mainDeck.clear();
commanderDeck.clear();
// --- Mainboard ---
const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN});
for (const DecklistCardNode *node : mainCards) {
if (!node) {
continue;
}
CardInDeckRequest req;
QJsonObject json;
json.insert("card", node->getName());
json.insert("quantity", node->getNumber());
req.fromJson(json);
mainDeck.append(req);
// Max size allowed by commanderspellbook
if (mainDeck.size() >= 600) {
break;
}
}
// --- Commander (bannerCard) ---
const auto &metadata = deck.getMetadata();
if (!metadata.bannerCard.name.isEmpty()) {
CardInDeckRequest commander;
QJsonObject json;
json.insert("card", metadata.bannerCard.name);
json.insert("quantity", 1);
commander.fromJson(json);
commanderDeck.append(commander);
}
}
void CommanderSpellbookDeckRequest::debugPrint() const
{
qDebug() << "Main deck:";
for (const CardInDeckRequest &card : mainDeck) {
card.debugPrint();
}
qDebug() << "Commanders:";
for (const CardInDeckRequest &card : commanderDeck) {
card.debugPrint();
}
}

View file

@ -0,0 +1,34 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H
#define COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H
#include "card_in_deck_request.h"
#include "libcockatrice/deck_list/deck_list.h"
#include <QJsonObject>
#include <QVector>
class CommanderSpellbookDeckRequest
{
public:
CommanderSpellbookDeckRequest() = default;
void fromJson(const QJsonObject &json);
QJsonObject toJson() const;
void fromDeckList(const DeckList &deck);
void debugPrint() const;
const QVector<CardInDeckRequest> &main() const
{
return mainDeck;
}
const QVector<CardInDeckRequest> &commanders() const
{
return commanderDeck;
}
private:
QVector<CardInDeckRequest> mainDeck; // maxItems: 600
QVector<CardInDeckRequest> commanderDeck; // maxItems: 12
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H

View file

@ -0,0 +1,47 @@
#include "commander_spellbook_estimate_bracket_result.h"
static void parseCards(const QJsonObject &json, const QString &key, QVector<CommanderSpellbookCardResult> &out)
{
out.clear();
for (const auto &v : json.value(key).toArray()) {
if (!v.isObject())
continue;
CommanderSpellbookCardResult c;
c.fromJson(v.toObject());
out.append(c);
}
}
static void parseVariants(const QJsonObject &json, const QString &key, QVector<CommanderSpellbookVariantResult> &out)
{
out.clear();
for (const auto &v : json.value(key).toArray()) {
if (!v.isObject())
continue;
CommanderSpellbookVariantResult vr;
vr.fromJson(v.toObject());
out.append(vr);
}
}
void EstimateBracketResult::fromJson(const QJsonObject &json)
{
bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString());
parseCards(json, "gameChangerCards", gameChangerCards);
parseCards(json, "massLandDenialCards", massLandDenialCards);
parseCards(json, "extraTurnCards", extraTurnCards);
parseCards(json, "tutorCards", tutorCards);
parseVariants(json, "massLandDenialTemplates", massLandDenialTemplates);
parseVariants(json, "massLandDenialCombos", massLandDenialCombos);
parseVariants(json, "extraTurnTemplates", extraTurnTemplates);
parseVariants(json, "extraTurnsCombos", extraTurnsCombos);
parseVariants(json, "tutorTemplates", tutorTemplates);
parseVariants(json, "lockCombos", lockCombos);
parseVariants(json, "skipTurnsCombos", skipTurnsCombos);
parseVariants(json, "definitelyEarlyGameTwoCardCombos", definitelyEarlyGameTwoCardCombos);
parseVariants(json, "arguablyEarlyGameTwoCardCombos", arguablyEarlyGameTwoCardCombos);
parseVariants(json, "definitelyLateGameTwoCardCombos", definitelyLateGameTwoCardCombos);
parseVariants(json, "borderlineLateGameTwoCardCombos", borderlineLateGameTwoCardCombos);
}

View file

@ -0,0 +1,33 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H
#include "commander_spellbook_card_result.h"
#include "commander_spellbook_variant_result.h"
#include <QVector>
class EstimateBracketResult
{
public:
void fromJson(const QJsonObject &json);
CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown;
QVector<CommanderSpellbookCardResult> gameChangerCards;
QVector<CommanderSpellbookCardResult> massLandDenialCards;
QVector<CommanderSpellbookCardResult> extraTurnCards;
QVector<CommanderSpellbookCardResult> tutorCards;
QVector<CommanderSpellbookVariantResult> massLandDenialTemplates;
QVector<CommanderSpellbookVariantResult> massLandDenialCombos;
QVector<CommanderSpellbookVariantResult> extraTurnTemplates;
QVector<CommanderSpellbookVariantResult> extraTurnsCombos;
QVector<CommanderSpellbookVariantResult> tutorTemplates;
QVector<CommanderSpellbookVariantResult> lockCombos;
QVector<CommanderSpellbookVariantResult> skipTurnsCombos;
QVector<CommanderSpellbookVariantResult> definitelyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> arguablyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> definitelyLateGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> borderlineLateGameTwoCardCombos;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H

View file

@ -0,0 +1,31 @@
#include "commander_spellbook_variant_result.h"
void CommanderSpellbookVariantResult::fromJson(const QJsonObject &json)
{
id = json.value("id").toString();
status = json.value("status").toString();
uses = json.value("uses").toArray();
cardRequires = json.value("requires").toArray();
produces = json.value("produces").toArray();
of = json.value("of").toArray();
includes = json.value("includes").toArray();
manaNeeded = json.value("manaNeeded").toArray();
manaValueNeeded = json.value("manaValueNeeded").toArray();
easyPrerequisites = json.value("easyPrerequisites").toArray();
notablePrerequisites = json.value("notablePrerequisites").toArray();
description = json.value("description").toString();
notes = json.value("notes").toString();
popularity = json.value("popularity").toDouble();
spoiler = json.value("spoiler").toBool();
bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString());
legalities = json.value("legalities").toObject();
prices = json.value("prices").toObject();
variantCount = json.value("variantCount").toInt();
}

View file

@ -0,0 +1,41 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#include "commander_spellbook_bracket_tag.h"
#include <QJsonArray>
#include <QJsonObject>
class CommanderSpellbookVariantResult
{
public:
void fromJson(const QJsonObject &json);
QString id;
QString status;
QJsonArray uses;
QJsonArray cardRequires;
QJsonArray produces;
QJsonArray of;
QJsonArray includes;
QJsonArray manaNeeded;
QJsonArray manaValueNeeded;
QJsonArray easyPrerequisites;
QJsonArray notablePrerequisites;
QString description;
QString notes;
double popularity = 0.0;
bool spoiler = false;
CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown;
QJsonObject legalities;
QJsonObject prices;
int variantCount = 0;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H

View file

@ -0,0 +1,77 @@
#include "commander_spellbook_api_accessor.h"
#include "api_response/commander_spellbook_deck_request.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>
#include <version_string.h>
static const QUrl ESTIMATE_BRACKET_URL(QStringLiteral("https://backend.commanderspellbook.com/estimate-bracket"));
CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance()
{
static CommanderSpellbookApiAccessor instance;
return instance;
}
CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent)
{
}
CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck,
QObject *requester)
{
CommanderSpellbookDeckRequest deckRequest;
deckRequest.fromDeckList(deck);
QJsonDocument doc(deckRequest.toJson());
QByteArray body = doc.toJson(QJsonDocument::Compact);
QNetworkRequest req(ESTIMATE_BRACKET_URL);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = network.post(req, body);
const RequestId id = nextRequestId++;
reply->setProperty("requestId", QVariant::fromValue(id));
reply->setProperty("requester", QVariant::fromValue(requester));
connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); });
return id;
}
void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply)
{
reply->deleteLater();
const RequestId id = reply->property("requestId").toULongLong();
QObject *requester = reply->property("requester").value<QObject *>();
if (!requester) {
// Requester died — silently drop
return;
}
if (reply->error() != QNetworkReply::NoError) {
emit estimateBracketError(id, requester, reply->errorString());
return;
}
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
emit estimateBracketError(id, requester, QStringLiteral("Invalid JSON response"));
return;
}
EstimateBracketResult result;
result.fromJson(doc.object());
emit estimateBracketFinished(id, requester, result);
}

View file

@ -0,0 +1,37 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H
#define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H
#include "api_response/commander_spellbook_estimate_bracket_result.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <libcockatrice/deck_list/deck_list.h>
class CommanderSpellbookApiAccessor final : public QObject
{
Q_OBJECT
public:
static CommanderSpellbookApiAccessor &instance();
using RequestId = quint64;
RequestId estimateBracket(const DeckList &deck, QObject *requester);
signals:
void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result);
void estimateBracketError(RequestId id, QObject *requester, const QString &errorMessage);
private:
explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr);
Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor)
void onEstimateReplyFinished(QNetworkReply *reply);
QNetworkAccessManager network;
RequestId nextRequestId = 1;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H

View file

@ -0,0 +1,131 @@
#include "commander_spellbook_bracket_explainer.h"
static QString cardList(const QVector<CommanderSpellbookCardResult> &cards, int max = 5)
{
QStringList names;
for (int i = 0; i < cards.size() && i < max; ++i) {
names << cards[i].name;
}
if (cards.size() > max) {
names << QString("and %1 more").arg(cards.size() - max);
}
return names.join(", ");
}
static QString comboCount(const QVector<CommanderSpellbookVariantResult> &variants)
{
return QString::number(variants.size());
}
BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r)
{
BracketExplanation out;
out.bracket = r.bracketTag;
// --- Game changers ---
if (!r.gameChangerCards.isEmpty()) {
BracketExplanationSection s;
s.title = "Game-changing cards";
s.bulletPoints << QString("Your deck contains %1 game-changing cards, such as %2.")
.arg(r.gameChangerCards.size())
.arg(cardList(r.gameChangerCards));
out.sections << s;
}
// --- Tutors ---
if (!r.tutorCards.isEmpty()) {
BracketExplanationSection s;
s.title = "Tutors";
s.bulletPoints << QString("The deck runs %1 tutor cards, including %2.")
.arg(r.tutorCards.size())
.arg(cardList(r.tutorCards));
out.sections << s;
}
// --- Extra turns ---
if (!r.extraTurnCards.isEmpty()) {
BracketExplanationSection s;
s.title = "Extra turn effects";
s.bulletPoints << QString("Extra turn spells were detected (%1), such as %2.")
.arg(r.extraTurnCards.size())
.arg(cardList(r.extraTurnCards));
out.sections << s;
}
// --- Mass land denial ---
if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Mass land denial";
if (!r.massLandDenialCards.isEmpty()) {
s.bulletPoints << QString("The deck includes %1 mass land denial cards (%2).")
.arg(r.massLandDenialCards.size())
.arg(cardList(r.massLandDenialCards));
}
if (!r.massLandDenialCombos.isEmpty()) {
s.bulletPoints << QString("%1 mass land denial combo variants were identified.")
.arg(comboCount(r.massLandDenialCombos));
}
out.sections << s;
}
// --- Lock / skip turns ---
if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Lock or skip-turn combos";
if (!r.lockCombos.isEmpty()) {
s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos));
}
if (!r.skipTurnsCombos.isEmpty()) {
s.bulletPoints << QString("%1 skip-turn combo variants were detected.").arg(comboCount(r.skipTurnsCombos));
}
out.sections << s;
}
// --- Early-game combos ---
if (!r.definitelyEarlyGameTwoCardCombos.isEmpty() || !r.arguablyEarlyGameTwoCardCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Early-game two-card combos";
if (!r.definitelyEarlyGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 definitely early-game two-card combos were found.")
.arg(comboCount(r.definitelyEarlyGameTwoCardCombos));
}
if (!r.arguablyEarlyGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 arguably early-game two-card combos were found.")
.arg(comboCount(r.arguablyEarlyGameTwoCardCombos));
}
out.sections << s;
}
// --- Late-game combos ---
if (!r.definitelyLateGameTwoCardCombos.isEmpty() || !r.borderlineLateGameTwoCardCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Late-game two-card combos";
if (!r.definitelyLateGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 definitely late-game two-card combos were found.")
.arg(comboCount(r.definitelyLateGameTwoCardCombos));
}
if (!r.borderlineLateGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 borderline late-game two-card combos were found.")
.arg(comboCount(r.borderlineLateGameTwoCardCombos));
}
out.sections << s;
}
return out;
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H
#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H
#include "api_response/commander_spellbook_estimate_bracket_result.h"
struct BracketExplanationSection
{
QString title;
QStringList bulletPoints;
};
struct BracketExplanation
{
CommanderSpellbookBracketTag::BracketTag bracket;
QList<BracketExplanationSection> sections;
bool isEmpty() const
{
return sections.isEmpty();
}
};
class BracketExplainer
{
public:
static BracketExplanation explain(const EstimateBracketResult &result);
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H