[Client] Added an exclude and color identity filter for the visual database display (#7587)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* [Client] Add exclude and color identity filters to the visual database display

* [Filters] Extract the color filter mode derivation into libcockatrice_filters

The mode table and the 'which mode produced this filter set' derivation are
filter semantics, not widget code, so they belong next to CardFilter rather
than in an anonymous namespace in the client. Moving them also makes them
testable: filter_mode_test pins every mode, the Custom fallback and the
'keep the live mode when it still fits' rule.

* [Tests] Cover the negated color identity query

ci! read the camelCase colorIdentity property instead of the lowercase
coloridentity key, so it matched the empty string and rejected every card.
The new case uses a card whose colors and color identity differ, so it fails
on the old lookup and passes on the fixed one.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
This commit is contained in:
Jurriaan 2026-11-05 12:40:32 +01:00 • committed by GitHub
parent 64c7809332
commit 74686d1736
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 703 additions and 141 deletions

View file

@ -15,9 +15,23 @@
#include <QtGlobal> #include <QtGlobal>
#include <libcockatrice/filters/filter_tree.h> #include <libcockatrice/filters/filter_tree.h>
namespace
{
/** @brief Remove the filters that apply to the given color and attribute from the given model. */
static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr)
{
for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) {
if (filter->term() == color) {
filterModel->removeFilter(filter);
}
}
}
} // namespace
/** /**
* This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter * This widget provides a graphical control element for the CardFilter::Attr::AttrColor and
* model. * CardFilter::Attr::AttrColorIdentity filters applied to the filter model.
* @param parent The Qt Widget that this widget will parent to * @param parent The Qt Widget that this widget will parent to
* @param _filterModel The filter model that this widget will manipulate * @param _filterModel The filter model that this widget will manipulate
*/ */
@ -59,11 +73,25 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi()
modeComboBox->blockSignals(true); modeComboBox->blockSignals(true);
modeComboBox->clear(); modeComboBox->clear();
modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch));
modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes));
modeComboBox->addItem(tr("Excludes"), QVariant::fromValue(FilterMode::Excludes));
modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude));
modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch));
modeComboBox->addItem(tr("Color identity"), QVariant::fromValue(FilterMode::ColorIdentity));
modeComboBox->setItemData(0, tr("Cards containing any of the selected colors"), Qt::ToolTipRole);
modeComboBox->setItemData(1, tr("Cards containing none of the selected colors"), Qt::ToolTipRole);
modeComboBox->setItemData(2, tr("Cards containing any of the selected colors and none of the unselected colors"),
Qt::ToolTipRole);
modeComboBox->setItemData(3, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole);
modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole);
if (currentMode == FilterMode::Custom) {
addCustomFilterModeItem();
}
modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter"));
modeComboBox->setAccessibleName(tr("Color filter mode"));
// Restore current mode // Restore current mode
const int index = modeComboBox->findData(QVariant::fromValue(currentMode)); const int index = modeComboBox->findData(QVariant::fromValue(currentMode));
@ -74,6 +102,28 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi()
modeComboBox->blockSignals(false); modeComboBox->blockSignals(false);
} }
void VisualDatabaseDisplayColorFilterWidget::setFilterMode(FilterMode filterMode)
{
int index = modeComboBox->findData(QVariant::fromValue(filterMode));
if (index < 0 && filterMode == FilterMode::Custom) {
index = modeComboBox->count();
addCustomFilterModeItem();
}
if (index >= 0) {
modeComboBox->setCurrentIndex(index);
}
}
void VisualDatabaseDisplayColorFilterWidget::addCustomFilterModeItem()
{
int customIndex = modeComboBox->count();
modeComboBox->addItem(tr("Custom"), QVariant::fromValue(FilterMode::Custom));
modeComboBox->setItemData(customIndex, tr("Cards matching an unknown combination of color filters"),
Qt::ToolTipRole);
}
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active)
{ {
if (active) { if (active) {
@ -85,74 +135,24 @@ void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, boo
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color)
{ {
QString colorString = color; FilterModeInfo info = getFilterModeInfo(currentMode);
QString typeStr;
// Remove previous filters removeFilters(filterModel, color, info.attr);
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); if (info.includeType != CardFilter::Type::TypeEnd) {
QList<const CardFilter *> matchingFilters; filterModel->addFilter(new CardFilter(color, info.includeType, info.attr));
for (const CardFilter *filter : allColorFilters) {
if (filter->term() == color) {
matchingFilters.append(filter);
}
}
for (const CardFilter *filter : matchingFilters) {
filterModel->removeFilter(filter);
}
// Add actual filter
switch (currentMode) {
case FilterMode::ExactMatch:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor));
break;
case FilterMode::Includes:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
break;
} }
} }
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color)
{ {
QString colorString = color; FilterModeInfo info = getFilterModeInfo(currentMode);
// Remove inclusion filters removeFilters(filterModel, color, info.attr);
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
QList<const CardFilter *> matchingFilters;
for (const CardFilter *filter : allColorFilters) { // Add exclusion filters if the mode demands it.
if (filter->term() == color) { if (info.excludeType != CardFilter::Type::TypeEnd) {
matchingFilters.append(filter); filterModel->addFilter(new CardFilter(color, info.excludeType, info.attr));
}
}
for (const CardFilter *filter : matchingFilters) {
filterModel->removeFilter(filter);
}
// Add exclusion filters if the mode demands it
switch (currentMode) {
case FilterMode::ExactMatch:
filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude:
filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break;
case FilterMode::Includes:
// No exclusion in Includes mode
break;
} }
} }
@ -169,6 +169,7 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
filterModel->filterTree()->blockSignals(true); filterModel->filterTree()->blockSignals(true);
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor); filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColorIdentity);
const QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>(); const QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
@ -211,52 +212,53 @@ QList<QChar> VisualDatabaseDisplayColorFilterWidget::getActiveColors()
void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel()
{ {
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); QList<const CardFilter *> filters;
filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColor));
filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColorIdentity));
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>(); // Re-derive the mode from the filters in the model, so that a loaded filter set selects the mode that
// produced it (or Custom when none could have).
currentMode = deriveFilterMode(filters, currentMode);
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { // Update the combobox. Add a Custom item only if we have a custom filter mode.
int customIndex = modeComboBox->findData(QVariant::fromValue(FilterMode::Custom));
modeComboBox->blockSignals(true);
if (currentMode == FilterMode::Custom) {
if (customIndex < 0) {
customIndex = modeComboBox->count();
addCustomFilterModeItem();
}
modeComboBox->setCurrentIndex(customIndex);
} else {
if (customIndex >= 0) {
modeComboBox->removeItem(customIndex);
}
int index = modeComboBox->findData(QVariant::fromValue(currentMode));
if (index >= 0) {
modeComboBox->setCurrentIndex(index);
}
}
modeComboBox->blockSignals(false);
// Update the mana symbol widgets by looking for the current mode's include or exclude filters.
const FilterModeInfo modeInfo = getFilterModeInfo(currentMode);
const bool useExclude =
modeInfo.includeType == CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd;
const CardFilter::Type filterType = useExclude ? modeInfo.excludeType : modeInfo.includeType;
filters = filterModel->getFiltersOfType(modeInfo.attr);
for (ManaSymbolWidget *manaSymbolWidget : findChildren<ManaSymbolWidget *>()) {
bool found = false; bool found = false;
for (const CardFilter *filter : allColorFilters) { for (const CardFilter *filter : filters) {
if (manaSymbolWidget->getSymbolChar() == filter->term()) { if (filter->type() == filterType && manaSymbolWidget->getSymbolChar() == filter->term()) {
switch (currentMode) {
case FilterMode::ExactMatch:
switch (filter->type()) {
case CardFilter::Type::TypeAnd:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
case FilterMode::Includes:
switch (filter->type()) {
case CardFilter::Type::TypeOr:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
case FilterMode::IncludeExclude:
switch (filter->type()) {
case CardFilter::Type::TypeOr:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
}
found = true; found = true;
break;
} }
} }
if (!found) { manaSymbolWidget->setColorActive(useExclude ? !found : found);
setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false); manaSymbolWidget->setDisabled(currentMode == FilterMode::Custom);
}
} }
} }

View file

@ -10,8 +10,8 @@
#include <QByteArray> #include <QByteArray>
#include <QChar> #include <QChar>
#include <QList> #include <QList>
#include <QMetaType>
#include <QWidget> #include <QWidget>
#include <libcockatrice/filters/filter_mode.h>
#include <libcockatrice/utility/debouncer.h> #include <libcockatrice/utility/debouncer.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
@ -38,15 +38,6 @@ private:
int circleDiameter; int circleDiameter;
}; };
enum class FilterMode
{
ExactMatch, // Only selected colors are included, all others are excluded.
Includes, // Include selected colors (OR condition).
IncludeExclude // Include selected colors (OR) and exclude unselected colors (AND NOT).
};
Q_DECLARE_METATYPE(FilterMode)
class VisualDatabaseDisplayColorFilterWidget : public QWidget class VisualDatabaseDisplayColorFilterWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -54,11 +45,13 @@ class VisualDatabaseDisplayColorFilterWidget : public QWidget
public: public:
explicit VisualDatabaseDisplayColorFilterWidget(QWidget *parent, FilterTreeModel *filterModel); explicit VisualDatabaseDisplayColorFilterWidget(QWidget *parent, FilterTreeModel *filterModel);
void retranslateUi(); void retranslateUi();
void setFilterMode(FilterMode filterMode);
signals: signals:
void filterModeChanged(FilterMode filterMode); void filterModeChanged(FilterMode filterMode);
private slots: private slots:
void addCustomFilterModeItem();
void handleColorToggled(QChar color, bool active); void handleColorToggled(QChar color, bool active);
void addFilter(QChar color); void addFilter(QChar color);
void removeFilter(QChar color); void removeFilter(QChar color);

View file

@ -160,6 +160,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
filterModel->blockSignals(true); filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true); filterModel->filterTree()->blockSignals(true);
colorFilterWidget->setFilterMode(FilterMode::Includes);
filterModel->clear(); filterModel->clear();
filterModel->blockSignals(false); filterModel->blockSignals(false);
filterModel->filterTree()->blockSignals(false); filterModel->filterTree()->blockSignals(false);

View file

@ -316,6 +316,10 @@ void CardInfo::setColors(const QString &value)
{ {
setProperty(Mtg::Colors, value); setProperty(Mtg::Colors, value);
} }
QString CardInfo::getColorIdentity() const
{
return getProperty(Mtg::ColorIdentity);
}
QString CardInfo::getLoyalty() const QString CardInfo::getLoyalty() const
{ {
return getProperty(Mtg::Loyalty); return getProperty(Mtg::Loyalty);

View file

@ -469,6 +469,7 @@ public:
[[nodiscard]] QString getCmc() const; [[nodiscard]] QString getCmc() const;
[[nodiscard]] QString getColors() const; [[nodiscard]] QString getColors() const;
void setColors(const QString &value); void setColors(const QString &value);
[[nodiscard]] QString getColorIdentity() const;
[[nodiscard]] QString getLoyalty() const; [[nodiscard]] QString getLoyalty() const;
[[nodiscard]] QString getMainCardType() const; [[nodiscard]] QString getMainCardType() const;
[[nodiscard]] QString getManaCost() const; [[nodiscard]] QString getManaCost() const;

View file

@ -1,10 +1,10 @@
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_mode.h
libcockatrice/filters/filter_tree.h libcockatrice/filters/filter_string.h libcockatrice/filters/filter_tree.h
) )
add_library( add_library(
libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_string.cpp libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_mode.cpp
libcockatrice/filters/filter_tree.cpp libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp
) )
add_dependencies(libcockatrice_filters libcockatrice_card) add_dependencies(libcockatrice_filters libcockatrice_card)

View file

@ -69,6 +69,8 @@ const QString CardFilter::attrName(Attr a)
return tr("Type"); return tr("Type");
case AttrColor: case AttrColor:
return tr("Color"); return tr("Color");
case AttrColorIdentity:
return tr("Color Identity");
case AttrText: case AttrText:
return tr("Text"); return tr("Text");
case AttrSet: case AttrSet:

View file

@ -32,6 +32,7 @@ public:
{ {
AttrCmc = 0, AttrCmc = 0,
AttrColor, AttrColor,
AttrColorIdentity,
AttrLoyalty, AttrLoyalty,
AttrManaCost, AttrManaCost,
AttrName, AttrName,

View file

@ -0,0 +1,77 @@
/**
* @file filter_mode.cpp
* @ingroup Filters
*/
#include "libcockatrice/filters/filter_mode.h"
#include <QString>
FilterModeInfo getFilterModeInfo(FilterMode filterMode)
{
switch (filterMode) {
case FilterMode::Includes:
return {CardFilter::Type::TypeOr, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor};
case FilterMode::Excludes:
return {CardFilter::Type::TypeAndNot, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor};
case FilterMode::IncludeExclude:
return {CardFilter::Type::TypeOr, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor};
case FilterMode::ExactMatch:
return {CardFilter::Type::TypeAnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor};
case FilterMode::ColorIdentity:
return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity};
case FilterMode::Custom:
case FilterMode::End:
break;
}
return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrEnd};
}
FilterMode deriveFilterMode(const QList<const CardFilter *> &filters, FilterMode currentMode)
{
constexpr int modeCount = static_cast<int>(FilterMode::End);
bool modeImpossible[modeCount] = {};
const QString colors = "WUBRG";
for (int i = 0; i < modeCount; ++i) {
const FilterModeInfo modeInfo = getFilterModeInfo(static_cast<FilterMode>(i));
// Remove the modes that cannot create one of the filters.
for (const CardFilter *filter : filters) {
if (filter->attr() != modeInfo.attr ||
(filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) {
modeImpossible[i] = true;
break;
}
}
// Modes that have both an include and an exclude filter should always have a filter for each color.
if (modeInfo.includeType != CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd &&
!modeImpossible[i]) {
for (const QChar &color : colors) {
int count = 0;
for (const CardFilter *filter : filters) {
count += filter->term() == color;
}
if (count != 1) {
modeImpossible[i] = true;
break;
}
}
}
}
// Keep the current mode if it can produce the filters, so that a live mode is never swapped for another.
// Switching to and from the custom mode is always possible.
if (!modeImpossible[static_cast<int>(currentMode)] && currentMode != FilterMode::Custom) {
return currentMode;
}
for (int i = 0; i < modeCount; ++i) {
if (!modeImpossible[i] || i == static_cast<int>(FilterMode::Custom)) {
return static_cast<FilterMode>(i);
}
}
return FilterMode::Custom;
}

View file

@ -0,0 +1,62 @@
/**
* @file filter_mode.h
* @ingroup Filters
*/
#ifndef LIBCOCKATRICE_FILTERS_FILTER_MODE_H
#define LIBCOCKATRICE_FILTERS_FILTER_MODE_H
#include "libcockatrice/filters/filter_card.h"
#include <QChar>
#include <QList>
#include <QMetaType>
/**
* @brief The way selected and unselected colors are combined into card filters.
*/
enum class FilterMode
{
Includes = 0, // Include selected colors (OR condition).
Excludes, // Exclude selected colors (AND NOT).
IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT).
ExactMatch, // Only selected colors are included, all others are excluded.
ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as AND NOT for each
// unselected color).
Custom, // The color filters do not match any of the above modes. Not included in the combo box.
End
};
Q_DECLARE_METATYPE(FilterMode)
/**
* @brief The filters a single color contributes in a given mode.
*/
struct FilterModeInfo
{
// When a color symbol is enabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd.
CardFilter::Type includeType;
// When a color symbol is disabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd.
CardFilter::Type excludeType;
// The attribute to filter for when adding the filters.
CardFilter::Attr attr;
};
/** @brief Return the filters the given mode contributes for one color. */
FilterModeInfo getFilterModeInfo(FilterMode filterMode);
/**
* @brief Determine the mode that produced the given color filters.
*
* Returns @p currentMode unchanged when that mode can still produce @p filters, so that a live mode is never
* replaced by an equally valid one. Otherwise returns the first mode that can produce them, falling back to
* FilterMode::Custom when none can.
*
* @param filters The filters on CardFilter::Attr::AttrColor and CardFilter::Attr::AttrColorIdentity.
* @param currentMode The mode currently selected in the UI.
*/
FilterMode deriveFilterMode(const QList<const CardFilter *> &filters, FilterMode currentMode);
#endif // LIBCOCKATRICE_FILTERS_FILTER_MODE_H

View file

@ -409,7 +409,7 @@ static void setupParserRules()
const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i'; const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i';
if (sv.tokens[1][0] == ':') { if (sv.tokens[1][0] == ':') {
return [=](const CardData &x) { return [=](const CardData &x) {
const QString match = identity ? x->getColors() : x->getProperty("coloridentity"); const QString match = identity ? x->getColors() : x->getColorIdentity();
if (parts.contains("m") && match.length() < 2) { if (parts.contains("m") && match.length() < 2) {
return false; return false;
} }
@ -427,7 +427,7 @@ static void setupParserRules()
} }
return [=](const CardData &x) { return [=](const CardData &x) {
const QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); const QString match = identity ? x->getColors() : x->getColorIdentity();
if (parts.contains("m") && match.length() < 2) { if (parts.contains("m") && match.length() < 2) {
return false; return false;
} }

View file

@ -176,23 +176,24 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st
lowercaseTerm = term.toLower(); lowercaseTerm = term.toLower();
switch (attr()) { switch (attr()) {
case CardFilter::AttrColor: { case CardFilter::AttrColor:
QString converted = term.trimmed(); case CardFilter::AttrColorIdentity: {
QString converted = term.trimmed().toLower();
converted.replace("green", "g", Qt::CaseInsensitive); converted.replace("green", "g");
converted.replace("grn", "g", Qt::CaseInsensitive); converted.replace("grn", "g");
converted.replace("blue", "u", Qt::CaseInsensitive); converted.replace("blue", "u");
converted.replace("blu", "u", Qt::CaseInsensitive); converted.replace("blu", "u");
converted.replace("black", "b", Qt::CaseInsensitive); converted.replace("black", "b");
converted.replace("blk", "b", Qt::CaseInsensitive); converted.replace("blk", "b");
converted.replace("red", "r", Qt::CaseInsensitive); converted.replace("red", "r");
converted.replace("white", "w", Qt::CaseInsensitive); converted.replace("white", "w");
converted.replace("wht", "w", Qt::CaseInsensitive); converted.replace("wht", "w");
converted.replace("colorless", "c", Qt::CaseInsensitive); converted.replace("colorless", "c");
converted.replace("colourless", "c", Qt::CaseInsensitive); converted.replace("colourless", "c");
converted.replace("none", "c", Qt::CaseInsensitive); converted.replace("none", "c");
converted.replace(QString(" "), QString(""), Qt::CaseInsensitive); converted.replace(QString(" "), QString(""));
convertedColorTerm = converted; convertedColorTerm = converted;
break; break;
@ -358,12 +359,17 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const
return subType.contains(term, Qt::CaseInsensitive); return subType.contains(term, Qt::CaseInsensitive);
} }
bool FilterItem::acceptColor(const CardInfoPtr &info) const /**
* @brief Return true when every letter of the given term appears in the given colors string.
* If the term is equal to "c", then only return true if the color string is empty.
*
* @param term The letters which should be included, or "c"
* @param colors The string to check for letters in
*/
static bool matchesColor(const QString &term, const QString &colors)
{ {
const QString colors = info->getColors();
// Colorless card filter // Colorless card filter
if (convertedColorTerm.toLower() == "c" && colors.length() < 1) { if (term == "c" && colors.length() < 1) {
return true; return true;
} }
@ -372,13 +378,23 @@ bool FilterItem::acceptColor(const CardInfoPtr &info) const
* then we should match all of them to the card's colors * then we should match all of them to the card's colors
*/ */
int match_count = 0; int match_count = 0;
for (const QChar &color : convertedColorTerm) { for (const QChar &color : term) {
if (colors.contains(color, Qt::CaseInsensitive)) { if (colors.contains(color, Qt::CaseInsensitive)) {
match_count++; match_count++;
} }
} }
return match_count == convertedColorTerm.length(); return match_count == term.length();
}
bool FilterItem::acceptColor(const CardInfoPtr &info) const
{
return matchesColor(convertedColorTerm, info->getColors());
}
bool FilterItem::acceptColorIdentity(const CardInfoPtr &info) const
{
return matchesColor(convertedColorTerm, info->getColorIdentity());
} }
bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const
@ -540,6 +556,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr &info,
return acceptType(info); return acceptType(info);
case CardFilter::AttrColor: case CardFilter::AttrColor:
return acceptColor(info); return acceptColor(info);
case CardFilter::AttrColorIdentity:
return acceptColorIdentity(info);
case CardFilter::AttrText: case CardFilter::AttrText:
return acceptText(info, searchLanguage); return acceptText(info, searchLanguage);
case CardFilter::AttrSet: case CardFilter::AttrSet:

View file

@ -244,6 +244,7 @@ public:
[[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptColor(const CardInfoPtr &info) const; [[nodiscard]] bool acceptColor(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptColorIdentity(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptSet(const CardInfoPtr &info) const; [[nodiscard]] bool acceptSet(const CardInfoPtr &info) const;
[[nodiscard]] bool acceptManaCost(const CardInfoPtr &info) const; [[nodiscard]] bool acceptManaCost(const CardInfoPtr &info) const;

View file

@ -21,10 +21,12 @@ cockatrice_add_test(
) )
# ------------------------ # ------------------------
# Filter String Test # Guard to match condition for libcockatrice_filters in the root CMakeLists.txt
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
# ------------------------ # ------------------------
if(WITH_ORACLE OR WITH_CLIENT) if(WITH_ORACLE OR WITH_CLIENT)
# ------------------------
# Filter String Test
# ------------------------
cockatrice_add_test( cockatrice_add_test(
NAME NAME
filter_string_test filter_string_test
@ -36,6 +38,34 @@ if(WITH_ORACLE OR WITH_CLIENT)
libcockatrice_filters libcockatrice_filters
) )
# ------------------------
# Filter Tree Test
# ------------------------
cockatrice_add_test(
NAME
filter_tree_test
SOURCES
${VERSION_STRING_CPP}
filter_tree_test.cpp
mocks.cpp
LIBS
libcockatrice_filters
)
# ------------------------
# Filter Mode Test
# ------------------------
cockatrice_add_test(
NAME
filter_mode_test
SOURCES
${VERSION_STRING_CPP}
filter_mode_test.cpp
mocks.cpp
LIBS
libcockatrice_filters
)
# ------------------------ # ------------------------
# Visual Display Model Test # Visual Display Model Test
# ------------------------ # ------------------------

View file

@ -0,0 +1,147 @@
#include "libcockatrice/filters/filter_mode.h"
#include <gtest/gtest.h>
class FilterModeTest : public ::testing::Test
{
protected:
~FilterModeTest() override
{
for (CardFilter *filter : owned) {
delete filter;
}
}
/** @brief Create a filter owned by the fixture and append it to the set under test. */
const CardFilter *
add(const QString &term, CardFilter::Type type, CardFilter::Attr attr = CardFilter::Attr::AttrColor)
{
owned.append(new CardFilter(term, type, attr));
filters.append(owned.last());
return owned.last();
}
QList<CardFilter *> owned;
QList<const CardFilter *> filters;
};
TEST(FilterModeInfoTest, EachModeDescribesItsOwnFilters)
{
EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).includeType, CardFilter::Type::TypeOr);
EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).excludeType, CardFilter::Type::TypeEnd);
EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).attr, CardFilter::Attr::AttrColor);
EXPECT_EQ(getFilterModeInfo(FilterMode::Excludes).includeType, CardFilter::Type::TypeAndNot);
EXPECT_EQ(getFilterModeInfo(FilterMode::Excludes).excludeType, CardFilter::Type::TypeEnd);
EXPECT_EQ(getFilterModeInfo(FilterMode::IncludeExclude).includeType, CardFilter::Type::TypeOr);
EXPECT_EQ(getFilterModeInfo(FilterMode::IncludeExclude).excludeType, CardFilter::Type::TypeAndNot);
EXPECT_EQ(getFilterModeInfo(FilterMode::ExactMatch).includeType, CardFilter::Type::TypeAnd);
EXPECT_EQ(getFilterModeInfo(FilterMode::ExactMatch).excludeType, CardFilter::Type::TypeAndNot);
// Color identity only excludes: the selected colors must be the identity, so nothing is included positively.
EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).includeType, CardFilter::Type::TypeEnd);
EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).excludeType, CardFilter::Type::TypeAndNot);
EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).attr, CardFilter::Attr::AttrColorIdentity);
EXPECT_EQ(getFilterModeInfo(FilterMode::Custom).attr, CardFilter::Attr::AttrEnd);
}
TEST_F(FilterModeTest, EmptyFilterSetKeepsACompatibleMode)
{
// Includes and Color identity both allow an empty set, so the live mode is not swapped for another.
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Includes);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::ColorIdentity), FilterMode::ColorIdentity);
}
// Include / Exclude and Exact match always hold one filter per color, so an empty set cannot have come from
// them and falls back to the first mode that can describe it.
TEST_F(FilterModeTest, EmptyFilterSetFallsBackWhenTheModeNeedsOneFilterPerColor)
{
EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::Includes);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::Includes);
}
TEST_F(FilterModeTest, IncludesFromOrFilters)
{
add("W", CardFilter::Type::TypeOr);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Includes);
// A single OR filter cannot come from Excludes, so the first mode that can produce it is selected.
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Excludes), FilterMode::Includes);
}
TEST_F(FilterModeTest, ExcludesFromAndNotFilters)
{
add("W", CardFilter::Type::TypeAndNot);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Excludes), FilterMode::Excludes);
// Includes needs an OR or an end filter, so an AND NOT filter selects Excludes.
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Excludes);
}
TEST_F(FilterModeTest, IncludeExcludeNeedsOneFilterPerColor)
{
add("W", CardFilter::Type::TypeOr);
for (const QChar color : QString("UBRG")) {
add(QString(color), CardFilter::Type::TypeAndNot);
}
EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::IncludeExclude);
}
TEST_F(FilterModeTest, IncludeExcludeRejectsAnIncompleteColorSet)
{
add("W", CardFilter::Type::TypeOr);
add("U", CardFilter::Type::TypeAndNot);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::Custom);
}
TEST_F(FilterModeTest, ExactMatchFromAndAndAndNotFilters)
{
add("W", CardFilter::Type::TypeAnd);
for (const QChar color : QString("UBRG")) {
add(QString(color), CardFilter::Type::TypeAndNot);
}
EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::ExactMatch);
}
TEST_F(FilterModeTest, ColorIdentityFromIdentityAndNotFilters)
{
for (const QChar color : QString("BRG")) {
add(QString(color), CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity);
}
EXPECT_EQ(deriveFilterMode(filters, FilterMode::ColorIdentity), FilterMode::ColorIdentity);
}
TEST_F(FilterModeTest, MixedAttributesAreCustom)
{
add("W", CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor);
add("U", CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Custom);
}
TEST_F(FilterModeTest, CustomLeavesAsSoonAsAModeCanExplainTheFilters)
{
add("W", CardFilter::Type::TypeOr);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Custom), FilterMode::Includes);
}
// Exact match with no color selected stores one AND NOT filter per color. That is a legitimate Exact match
// state, so the live mode is kept; a mode that cannot produce the set re-derives to Excludes, which produces
// the identical filters.
TEST_F(FilterModeTest, FullyExcludedColorSetIsExactMatchOrExcludes)
{
for (const QChar color : QString("WUBRG")) {
add(QString(color), CardFilter::Type::TypeAndNot);
}
EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::ExactMatch);
EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Excludes);
}

View file

@ -123,6 +123,25 @@ CardInfoPtr greenIdentityCard()
return CardInfo::newInstance("Identity Cat", "", false, {{"colors", "G"}, {"coloridentity", "G"}}, {}, {}, {}, {}); return CardInfo::newInstance("Identity Cat", "", false, {{"colors", "G"}, {"coloridentity", "G"}}, {}, {}, {}, {});
} }
// A card whose color identity differs from its colors, so that a query reading the wrong one is visible.
CardInfoPtr splitIdentityCard()
{
return CardInfo::newInstance("Split Identity", "", false, {{"colors", "G"}, {"coloridentity", "W"}}, {}, {}, {},
{});
}
// "ci!" (exactly these color identities) used to read the camelCase "colorIdentity" property instead of the
// lowercase "coloridentity" key, so it matched the empty string and rejected every card.
TEST_F(CardQuery, ColorIdentityExactReadsTheColorIdentityProperty)
{
const CardData card = splitIdentityCard();
ASSERT_TRUE(FilterString("ci!w").check(card));
ASSERT_FALSE(FilterString("ci!g").check(card));
// "c!" still reads colors, not identity.
ASSERT_TRUE(FilterString("c!g").check(card));
ASSERT_FALSE(FilterString("c!w").check(card));
}
// Whitespace belongs around the separator, never inside an operator token: "c i: g" must not // Whitespace belongs around the separator, never inside an operator token: "c i: g" must not
// be read as the color-identity operator "ci" (it is a generic "c" AND a field query on "i"). // be read as the color-identity operator "ci" (it is a generic "c" AND a field query on "i").
TEST_F(CardQuery, ColorIdentityOperatorIsNotSplitByWhitespace) TEST_F(CardQuery, ColorIdentityOperatorIsNotSplitByWhitespace)

View file

@ -0,0 +1,204 @@
#include "libcockatrice/filters/filter_tree.h"
#include <gtest/gtest.h>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/filters/filter_string.h>
namespace
{
class FilterTreeTest : public ::testing::Test
{
protected:
void SetUp() override
{
language = CardSearchLanguage{};
island = CardInfo::newInstance("Island", "", false, {{"colors", ""}, {"coloridentity", "U"}}, {}, {}, {}, {});
artifact =
CardInfo::newInstance("Artifact", "", false, {{"colors", ""}, {"coloridentity", ""}}, {}, {}, {}, {});
truth = CardInfo::newInstance("Truth", "", false, {{"colors", "U"}, {"coloridentity", "U"}}, {}, {}, {}, {});
notDead =
CardInfo::newInstance("Not Dead", "", false, {{"colors", "B"}, {"coloridentity", "B"}}, {}, {}, {}, {});
wallOfDenial = CardInfo::newInstance("Wall of Denial", "", false, {{"colors", "WU"}, {"coloridentity", "WU"}},
{}, {}, {}, {});
maelstromAngel = CardInfo::newInstance("Maelstrom Archangel", "", false,
{{"colors", "WUBRG"}, {"coloridentity", "WUBRG"}}, {}, {}, {}, {});
}
CardSearchLanguage language;
CardData island; // c/u
CardData artifact; // c
CardData truth; // u
CardData notDead; // b
CardData wallOfDenial; // wu
CardData maelstromAngel; // wubrg
};
} // namespace
// =====================================================================================================================
// acceptColor
// =====================================================================================================================
TEST_F(FilterTreeTest, Color)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wu");
ASSERT_FALSE(filterTree.acceptsCard(island, language));
ASSERT_FALSE(filterTree.acceptsCard(artifact, language));
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorColorless)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "c");
ASSERT_TRUE(filterTree.acceptsCard(island, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorNot)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAndNot, "b");
ASSERT_TRUE(filterTree.acceptsCard(island, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
ASSERT_TRUE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorAliases)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "green");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "grn");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blue");
ASSERT_TRUE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blu");
ASSERT_TRUE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "black");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(notDead, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blk");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(notDead, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "red");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "white");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wht");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colorless");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colourless");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
filterTree.clear();
filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "none");
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
filterTree.clear();
}
// =====================================================================================================================
// acceptColorIdentity
// =====================================================================================================================
TEST_F(FilterTreeTest, ColorIdentity)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "wu");
ASSERT_FALSE(filterTree.acceptsCard(island, language));
ASSERT_FALSE(filterTree.acceptsCard(artifact, language));
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorIdentityColorless)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "c");
ASSERT_FALSE(filterTree.acceptsCard(island, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
ASSERT_FALSE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorIdentityNot)
{
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b");
ASSERT_TRUE(filterTree.acceptsCard(island, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
ASSERT_TRUE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language));
}
TEST_F(FilterTreeTest, ColorIdentityMultiple)
{
// Simulate a WU commander.
FilterTree filterTree;
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b");
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "r");
filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "g");
ASSERT_TRUE(filterTree.acceptsCard(island, language));
ASSERT_TRUE(filterTree.acceptsCard(artifact, language));
ASSERT_TRUE(filterTree.acceptsCard(truth, language));
ASSERT_FALSE(filterTree.acceptsCard(notDead, language));
ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language));
ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}