servatrice/common/decklist.cpp
2015-07-06 00:30:17 -04:00

760 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <QFile>
#include <QTextStream>
#include <QVariant>
#include <QCryptographicHash>
#include <QDebug>
#include "decklist.h"
SideboardPlan::SideboardPlan(const QString &_name, const QList<MoveCard_ToZone> &_moveList)
: name(_name), moveList(_moveList)
{
}
void SideboardPlan::setMoveList(const QList<MoveCard_ToZone> &_moveList)
{
moveList = _moveList;
}
bool SideboardPlan::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "name")
name = xml->readElementText();
else if (childName == "move_card_to_zone") {
MoveCard_ToZone m;
while (!xml->atEnd()) {
xml->readNext();
const QString childName2 = xml->name().toString();
if (xml->isStartElement()) {
if (childName2 == "card_name")
m.set_card_name(xml->readElementText().toStdString());
else if (childName2 == "start_zone")
m.set_start_zone(xml->readElementText().toStdString());
else if (childName2 == "target_zone")
m.set_target_zone(xml->readElementText().toStdString());
} else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) {
moveList.append(m);
break;
}
}
}
} else if (xml->isEndElement() && (childName == "sideboard_plan"))
return true;
}
return false;
}
void SideboardPlan::write(QXmlStreamWriter *xml)
{
xml->writeStartElement("sideboard_plan");
xml->writeTextElement("name", name);
for (int i = 0; i < moveList.size(); ++i) {
xml->writeStartElement("move_card_to_zone");
xml->writeTextElement("card_name", QString::fromStdString(moveList[i].card_name()));
xml->writeTextElement("start_zone", QString::fromStdString(moveList[i].start_zone()));
xml->writeTextElement("target_zone", QString::fromStdString(moveList[i].target_zone()));
xml->writeEndElement();
}
xml->writeEndElement();
}
AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent)
: parent(_parent)
{
if (parent)
parent->append(this);
}
int AbstractDecklistNode::depth() const
{
if (parent)
return parent->depth() + 1;
else
return 0;
}
InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent)
: AbstractDecklistNode(_parent), name(other->getName())
{
for (int i = 0; i < other->size(); ++i) {
InnerDecklistNode *inner = dynamic_cast<InnerDecklistNode *>(other->at(i));
if (inner)
new InnerDecklistNode(inner, this);
else
new DecklistCardNode(dynamic_cast<DecklistCardNode *>(other->at(i)), this);
}
}
InnerDecklistNode::~InnerDecklistNode()
{
clearTree();
}
QString InnerDecklistNode::visibleNameFromName(const QString &_name)
{
if (_name == "main")
return QObject::tr("Maindeck");
else if (_name == "side")
return QObject::tr("Sideboard");
else if (_name == "tokens")
return QObject::tr("Tokens");
else
return _name;
}
void InnerDecklistNode::setSortMethod(DeckSortMethod method)
{
sortMethod = method;
for (int i = 0; i < size(); i++)
at(i)->setSortMethod(method);
}
QString InnerDecklistNode::getVisibleName() const
{
return visibleNameFromName(name);
}
void InnerDecklistNode::clearTree()
{
for (int i = 0; i < size(); i++)
delete at(i);
clear();
}
DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent)
: AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber()), price(other->getPrice())
{
}
AbstractDecklistNode *InnerDecklistNode::findChild(const QString &name)
{
for (int i = 0; i < size(); i++)
if (at(i)->getName() == name)
return at(i);
return 0;
}
int InnerDecklistNode::height() const
{
return at(0)->height() + 1;
}
int InnerDecklistNode::recursiveCount(bool countTotalCards) const
{
int result = 0;
for (int i = 0; i < size(); i++) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(at(i));
if (node)
result += node->recursiveCount(countTotalCards);
else if (countTotalCards)
result += dynamic_cast<AbstractDecklistCardNode *>(at(i))->getNumber();
else
result += 1;
}
return result;
}
float InnerDecklistNode::recursivePrice(bool countTotalCards) const
{
float result = 0;
for (int i = 0; i < size(); i++) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(at(i));
if (node)
result += node->recursivePrice(countTotalCards);
else if (countTotalCards)
result += dynamic_cast<AbstractDecklistCardNode *>(at(i))->getTotalPrice();
}
return result;
}
bool InnerDecklistNode::compare(AbstractDecklistNode *other) const
{
switch (sortMethod) {
case 0:
return compareNumber(other);
case 1:
return compareName(other);
case 2:
return comparePrice(other);
}
return 0;
}
bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const
{
InnerDecklistNode *other2 = dynamic_cast<InnerDecklistNode *>(other);
if (other2) {
int n1 = recursiveCount(true);
int n2 = other2->recursiveCount(true);
return (n1 != n2) ? (n1 > n2) : compareName(other);
} else {
return false;
}
}
bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const
{
InnerDecklistNode *other2 = dynamic_cast<InnerDecklistNode *>(other);
if (other2) {
return (getName() > other2->getName());
} else {
return false;
}
}
bool InnerDecklistNode::comparePrice(AbstractDecklistNode *other) const
{
InnerDecklistNode *other2 = dynamic_cast<InnerDecklistNode *>(other);
if (other2) {
int p1 = 100*recursivePrice(true);
int p2 = 100*other2->recursivePrice(true);
return (p1 != p2) ? (p1 > p2) : compareName(other);
} else {
return false;
}
}
bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const
{
switch (sortMethod) {
case ByNumber:
return compareNumber(other);
case ByName:
return compareName(other);
case ByPrice:
return compareTotalPrice(other);
}
return 0;
}
bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const
{
AbstractDecklistCardNode *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
if (other2) {
int n1 = getNumber();
int n2 = other2->getNumber();
return (n1 != n2) ? (n1 > n2) : compareName(other);
} else {
return true;
}
}
bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const
{
AbstractDecklistCardNode *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
if (other2) {
return (getName() > other2->getName());
} else {
return true;
}
}
bool AbstractDecklistCardNode::compareTotalPrice(AbstractDecklistNode *other) const
{
AbstractDecklistCardNode *other2 = dynamic_cast<AbstractDecklistCardNode *>(other);
if (other2) {
int p1 = 100*getTotalPrice();
int p2 = 100*other2->getTotalPrice();
return (p1 != p2) ? (p1 > p2) : compareName(other);
} else {
return true;
}
}
class InnerDecklistNode::compareFunctor {
private:
Qt::SortOrder order;
public:
compareFunctor(Qt::SortOrder _order) : order(_order) { }
inline bool operator()(QPair<int, AbstractDecklistNode *> a, QPair<int, AbstractDecklistNode *> b) const
{
return (order == Qt::AscendingOrder) ^ (a.second->compare(b.second));
}
};
bool InnerDecklistNode::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "zone") {
InnerDecklistNode *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
newZone->readElement(xml);
} else if (childName == "card") {
float price = (xml->attributes().value("price") != NULL) ? xml->attributes().value("price").toString().toFloat() : 0;
DecklistCardNode *newCard = new DecklistCardNode(xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(), price, this);
newCard->readElement(xml);
}
} else if (xml->isEndElement() && (childName == "zone"))
return false;
}
return true;
}
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
{
xml->writeStartElement("zone");
xml->writeAttribute("name", name);
for (int i = 0; i < size(); i++)
at(i)->writeElement(xml);
xml->writeEndElement(); // zone
}
bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
if (xml->isEndElement() && xml->name() == "card")
return false;
}
return true;
}
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
{
xml->writeEmptyElement("card");
xml->writeAttribute("number", QString::number(getNumber()));
xml->writeAttribute("price", QString::number(getPrice()));
xml->writeAttribute("name", getName());
}
QVector<QPair<int, int> > InnerDecklistNode::sort(Qt::SortOrder order)
{
QVector<QPair<int, int> > result(size());
// Initialize temporary list with contents of current list
QVector<QPair<int, AbstractDecklistNode *> > tempList(size());
for (int i = size() - 1; i >= 0; --i) {
tempList[i].first = i;
tempList[i].second = at(i);
}
// Sort temporary list
compareFunctor cmp(order);
qSort(tempList.begin(), tempList.end(), cmp);
// Map old indexes to new indexes and
// copy temporary list to the current one
for (int i = size() - 1; i >= 0; --i) {
result[i].first = tempList[i].first;
result[i].second = i;
replace(i, tempList[i].second);
}
return result;
}
DeckList::DeckList()
{
root = new InnerDecklistNode;
}
// TODO: http://qt-project.org/doc/qt-4.8/qobject.html#no-copy-constructor-or-assignment-operator
DeckList::DeckList(const DeckList &other)
: QObject(),
name(other.name),
comments(other.comments),
deckHash(other.deckHash)
{
root = new InnerDecklistNode(other.getRoot());
QMapIterator<QString, SideboardPlan *> spIterator(other.getSideboardPlans());
while (spIterator.hasNext()) {
spIterator.next();
sideboardPlans.insert(spIterator.key(), new SideboardPlan(spIterator.key(), spIterator.value()->getMoveList()));
}
updateDeckHash();
}
DeckList::DeckList(const QString &nativeString)
{
root = new InnerDecklistNode;
loadFromString_Native(nativeString);
}
DeckList::~DeckList()
{
delete root;
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
while (i.hasNext())
delete i.next().value();
}
QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan()
{
SideboardPlan *current = sideboardPlans.value(QString(), 0);
if (!current)
return QList<MoveCard_ToZone>();
else
return current->getMoveList();
}
void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan)
{
SideboardPlan *current = sideboardPlans.value(QString(), 0);
if (!current) {
current = new SideboardPlan;
sideboardPlans.insert(QString(), current);
}
current->setMoveList(plan);
}
bool DeckList::readElement(QXmlStreamReader *xml)
{
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "deckname")
name = xml->readElementText();
else if (childName == "comments")
comments = xml->readElementText();
else if (childName == "zone") {
InnerDecklistNode *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), root);
newZone->readElement(xml);
} else if (childName == "sideboard_plan") {
SideboardPlan *newSideboardPlan = new SideboardPlan;
if (newSideboardPlan->readElement(xml))
sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan);
else
delete newSideboardPlan;
}
} else if (xml->isEndElement() && (childName == "cockatrice_deck"))
return false;
return true;
}
void DeckList::write(QXmlStreamWriter *xml)
{
xml->writeStartElement("cockatrice_deck");
xml->writeAttribute("version", "1");
xml->writeTextElement("deckname", name);
xml->writeTextElement("comments", comments);
for (int i = 0; i < root->size(); i++)
root->at(i)->writeElement(xml);
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
while (i.hasNext())
i.next().value()->write(xml);
xml->writeEndElement();
}
bool DeckList::loadFromXml(QXmlStreamReader *xml)
{
if (xml->error()) {
qDebug() << "Error loading deck from xml: " << xml->errorString();
return false;
}
cleanList();
while (!xml->atEnd()) {
xml->readNext();
if (xml->isStartElement()) {
if (xml->name() != "cockatrice_deck")
return false;
while (!xml->atEnd()) {
xml->readNext();
if (!readElement(xml))
break;
}
}
}
updateDeckHash();
if (xml->error()) {
qDebug() << "Error loading deck from xml: " << xml->errorString();
return false;
}
return true;
}
bool DeckList::loadFromString_Native(const QString &nativeString)
{
QXmlStreamReader xml(nativeString);
return loadFromXml(&xml);
}
QString DeckList::writeToString_Native()
{
QString result;
QXmlStreamWriter xml(&result);
xml.writeStartDocument();
write(&xml);
xml.writeEndDocument();
return result;
}
bool DeckList::loadFromFile_Native(QIODevice *device)
{
QXmlStreamReader xml(device);
return loadFromXml(&xml);
}
bool DeckList::saveToFile_Native(QIODevice *device)
{
QXmlStreamWriter xml(device);
xml.setAutoFormatting(true);
xml.writeStartDocument();
write(&xml);
xml.writeEndDocument();
return true;
}
bool DeckList::loadFromStream_Plain(QTextStream &in)
{
cleanList();
InnerDecklistNode *main = 0, *side = 0;
bool inSideboard = false;
int okRows = 0;
while (!in.atEnd()) {
QString line = in.readLine().simplified();
if (line.startsWith("//"))
continue;
InnerDecklistNode *zone;
if (line.startsWith("Sideboard", Qt::CaseInsensitive)) {
inSideboard = true;
continue;
} else if (line.startsWith("SB:", Qt::CaseInsensitive)) {
line = line.mid(3).trimmed();
if (!side)
side = new InnerDecklistNode("side", root);
zone = side;
} else if (inSideboard) {
if (!side)
side = new InnerDecklistNode("side", root);
zone = side;
} else {
if (!main)
main = new InnerDecklistNode("main", root);
zone = main;
}
// Filter out MWS edition symbols and basic land extras
QRegExp rx("\\[.*\\]");
line.remove(rx);
rx.setPattern("\\(.*\\)");
line.remove(rx);
//Filter out post card name editions
rx.setPattern("\\|.*$");
line.remove(rx);
line = line.simplified();
int i = line.indexOf(' ');
int cardNameStart = i + 1;
// If the count ends with an 'x', ignore it. For example,
// "4x Storm Crow" will count 4 correctly.
if (i > 0 && line[i - 1] == 'x') {
i--;
}
bool ok;
int number = line.left(i).toInt(&ok);
if (!ok)
continue;
QString cardName = line.mid(cardNameStart);
// Common differences between cockatrice's card names
// and what's commonly used in decklists
rx.setPattern("");
cardName.replace(rx, "'");
rx.setPattern("Æ");
cardName.replace(rx, "AE");
rx.setPattern("^Aether");
cardName.replace(rx, "AEther");
rx.setPattern("\\s*[|/]{1,2}\\s*");
cardName.replace(rx, " // ");
// Replace only if the ampersand is preceded by a non-capital letter,
// as would happen with acronyms. So 'Fire & Ice' is replaced but not
// 'R&D' or 'R & D'.
//
// Qt regexes don't support lookbehind so we capture and replace
// instead.
rx.setPattern("([^A-Z])\\s*&\\s*");
if (rx.indexIn(cardName) != -1) {
cardName.replace(rx, QString("%1 // ").arg(rx.cap(1)));
}
++okRows;
new DecklistCardNode(cardName, number, 0, zone);
}
updateDeckHash();
return (okRows > 0);
}
bool DeckList::loadFromFile_Plain(QIODevice *device)
{
QTextStream in(device);
return loadFromStream_Plain(in);
}
struct WriteToStream {
QTextStream &stream;
WriteToStream(QTextStream &_stream) : stream(_stream) {}
void operator()(
const InnerDecklistNode *node,
const DecklistCardNode *card
) {
if (node->getName() == "side") {
stream << "SB: ";
}
stream << QString("%1 %2\n").arg(
card->getNumber()
).arg(
card->getName()
);
}
};
bool DeckList::saveToStream_Plain(QTextStream &out)
{
WriteToStream writeToStream(out);
forEachCard(writeToStream);
return true;
}
bool DeckList::saveToFile_Plain(QIODevice *device)
{
QTextStream out(device);
return saveToStream_Plain(out);
}
QString DeckList::writeToString_Plain()
{
QString result;
QTextStream out(&result);
saveToStream_Plain(out);
return result;
}
void DeckList::cleanList()
{
root->clearTree();
setName();
setComments();
deckHash = QString();
emit deckHashChanged();
}
void DeckList::getCardListHelper(InnerDecklistNode *item, QSet<QString> &result) const
{
for (int i = 0; i < item->size(); ++i) {
DecklistCardNode *node = dynamic_cast<DecklistCardNode *>(item->at(i));
if (node)
result.insert(node->getName());
else
getCardListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
}
}
QStringList DeckList::getCardList() const
{
QSet<QString> result;
getCardListHelper(root, result);
return result.toList();
}
int DeckList::getSideboardSize() const
{
int size = 0;
for (int i = 0; i < root->size(); ++i) {
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
if (node->getName() != "side")
continue;
for (int j = 0; j < node->size(); j++) {
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
size += card->getNumber();
}
}
return size;
}
DecklistCardNode *DeckList::addCard(const QString &cardName, const QString &zoneName)
{
InnerDecklistNode *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
if (!zoneNode)
zoneNode = new InnerDecklistNode(zoneName, root);
DecklistCardNode *node = new DecklistCardNode(cardName, 1, 0, zoneNode);
updateDeckHash();
return node;
}
bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
{
if (node == root)
return true;
bool updateHash = false;
if (!rootNode) {
rootNode = root;
updateHash = true;
}
int index = rootNode->indexOf(node);
if (index != -1) {
delete rootNode->takeAt(index);
if (!rootNode->size())
deleteNode(rootNode, rootNode->getParent());
if (updateHash)
updateDeckHash();
return true;
}
for (int i = 0; i < rootNode->size(); i++) {
InnerDecklistNode *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
if (inner)
if (deleteNode(node, inner)) {
if (updateHash)
updateDeckHash();
return true;
}
}
return false;
}
void DeckList::updateDeckHash()
{
QStringList cardList;
bool isValidDeckList = true;
for (int i = 0; i < root->size(); i++)
{
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
for (int j = 0; j < node->size(); j++)
{
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
for (int k = 0; k < card->getNumber(); ++k)
{
if (node->getName() == "main" || node->getName() == "side") // Mainboard or Sideboard
{
cardList.append((node->getName() == "side" ? "SB:" : "") + card->getName().toLower());
}
else if (node->getName() != "tokens") // Neither Mainboard, Sideboard, or Tokens... cheater?
{
isValidDeckList = false;
break; break; break; // Deck is invalid, end the entire check
}
}
}
}
cardList.sort();
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
quint64 number = (((quint64) (unsigned char) deckHashArray[0]) << 32)
+ (((quint64) (unsigned char) deckHashArray[1]) << 24)
+ (((quint64) (unsigned char) deckHashArray[2] << 16))
+ (((quint64) (unsigned char) deckHashArray[3]) << 8)
+ (quint64) (unsigned char) deckHashArray[4];
deckHash = (isValidDeckList) ? QString::number(number, 32).rightJustified(8, '0') : "INVALID";
emit deckHashChanged();
}