File indexing completed on 2024-05-12 08:00:32

0001 /*
0002  * Copyright (C) 1995 Paul Olav Tvete <paul@troll.no>
0003  * Copyright (C) 2000-2009 Stephan Kulow <coolo@kde.org>
0004  * Copyright (C) 2009-2010 Parker Coates <coates@kde.org>
0005  *
0006  * License of original code:
0007  * -------------------------------------------------------------------------
0008  *   Permission to use, copy, modify, and distribute this software and its
0009  *   documentation for any purpose and without fee is hereby granted,
0010  *   provided that the above copyright notice appear in all copies and that
0011  *   both that copyright notice and this permission notice appear in
0012  *   supporting documentation.
0013  *
0014  *   This file is provided AS IS with no warranties of any kind.  The author
0015  *   shall have no liability with respect to the infringement of copyrights,
0016  *   trade secrets or any patents by this file or any part thereof.  In no
0017  *   event will the author be liable for any lost revenue or profits or
0018  *   other special, indirect and consequential damages.
0019  * -------------------------------------------------------------------------
0020  *
0021  * License of modifications/additions made after 2009-01-01:
0022  * -------------------------------------------------------------------------
0023  *   This program is free software; you can redistribute it and/or
0024  *   modify it under the terms of the GNU General Public License as
0025  *   published by the Free Software Foundation; either version 2 of
0026  *   the License, or (at your option) any later version.
0027  *
0028  *   This program is distributed in the hope that it will be useful,
0029  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0030  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0031  *   GNU General Public License for more details.
0032  *
0033  *   You should have received a copy of the GNU General Public License
0034  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
0035  * -------------------------------------------------------------------------
0036  */
0037 
0038 #include "dealer.h"
0039 
0040 // own
0041 #include "dealerinfo.h"
0042 #include "kpat_debug.h"
0043 #include "messagebox.h"
0044 #include "patsolve/solverinterface.h"
0045 #include "renderer.h"
0046 #include "shuffle.h"
0047 // KCardGame
0048 #include <KCardTheme>
0049 // KF
0050 #include <KConfigGroup>
0051 #include <KLocalizedString>
0052 #include <KMessageBox>
0053 #include <KSharedConfig>
0054 // Qt
0055 #include <QApplication>
0056 #include <QGraphicsSceneMouseEvent>
0057 #include <QRandomGenerator>
0058 #include <QThread>
0059 #include <QXmlStreamReader>
0060 #include <QXmlStreamWriter>
0061 // Std
0062 #include <cmath>
0063 
0064 namespace
0065 {
0066 const qreal wonBoxToSceneSizeRatio = 0.7;
0067 
0068 QString solverStatusMessage(int status, bool everWinnable)
0069 {
0070     switch (status) {
0071     case SolverInterface::SolutionExists:
0072         return i18n("Solver: This game is winnable.");
0073     case SolverInterface::NoSolutionExists:
0074         return everWinnable ? i18n("Solver: This game is no longer winnable.") : i18n("Solver: This game cannot be won.");
0075     case SolverInterface::UnableToDetermineSolvability:
0076         return i18n("Solver: Unable to determine if this game is winnable.");
0077     case SolverInterface::SearchAborted:
0078     case SolverInterface::MemoryLimitReached:
0079     default:
0080         return QString();
0081     }
0082 }
0083 
0084 int readIntAttribute(const QXmlStreamReader &xml, const QString &key, bool *ok = nullptr)
0085 {
0086     QStringView value = xml.attributes().value(key);
0087     return QString::fromRawData(value.data(), value.length()).toInt(ok);
0088 }
0089 
0090 QString suitToString(int suit)
0091 {
0092     switch (suit) {
0093     case KCardDeck::Clubs:
0094         return QStringLiteral("clubs");
0095     case KCardDeck::Diamonds:
0096         return QStringLiteral("diamonds");
0097     case KCardDeck::Hearts:
0098         return QStringLiteral("hearts");
0099     case KCardDeck::Spades:
0100         return QStringLiteral("spades");
0101     default:
0102         return QString();
0103     }
0104 }
0105 
0106 QString rankToString(int rank)
0107 {
0108     switch (rank) {
0109     case KCardDeck::Ace:
0110         return QStringLiteral("ace");
0111     case KCardDeck::Two:
0112         return QStringLiteral("two");
0113     case KCardDeck::Three:
0114         return QStringLiteral("three");
0115     case KCardDeck::Four:
0116         return QStringLiteral("four");
0117     case KCardDeck::Five:
0118         return QStringLiteral("five");
0119     case KCardDeck::Six:
0120         return QStringLiteral("six");
0121     case KCardDeck::Seven:
0122         return QStringLiteral("seven");
0123     case KCardDeck::Eight:
0124         return QStringLiteral("eight");
0125     case KCardDeck::Nine:
0126         return QStringLiteral("nine");
0127     case KCardDeck::Ten:
0128         return QStringLiteral("ten");
0129     case KCardDeck::Jack:
0130         return QStringLiteral("jack");
0131     case KCardDeck::Queen:
0132         return QStringLiteral("queen");
0133     case KCardDeck::King:
0134         return QStringLiteral("king");
0135     default:
0136         return QString();
0137     }
0138 }
0139 }
0140 
0141 class SolverThread : public QThread
0142 {
0143     Q_OBJECT
0144 
0145 public:
0146     SolverThread(SolverInterface *solver)
0147         : m_solver(solver)
0148     {
0149     }
0150 
0151     void run() override
0152     {
0153         SolverInterface::ExitStatus result = m_solver->patsolve();
0154         Q_EMIT finished(result);
0155     }
0156 
0157     void abort()
0158     {
0159         m_solver->stopExecution();
0160         wait();
0161     }
0162 
0163 Q_SIGNALS:
0164     void finished(int result);
0165 
0166 private:
0167     SolverInterface *m_solver;
0168 };
0169 
0170 int DealerScene::moveCount() const
0171 {
0172     return m_loadedMoveCount + m_undoStack.size();
0173 }
0174 
0175 void DealerScene::saveLegacyFile(QIODevice *io)
0176 {
0177     QXmlStreamWriter xml(io);
0178     xml.setAutoFormatting(true);
0179     xml.setAutoFormattingIndent(-1);
0180     xml.writeStartDocument();
0181 
0182     xml.writeDTD(QStringLiteral("<!DOCTYPE kpat>"));
0183 
0184     xml.writeStartElement(QStringLiteral("dealer"));
0185     xml.writeAttribute(QStringLiteral("id"), QString::number(gameId()));
0186     xml.writeAttribute(QStringLiteral("options"), getGameOptions());
0187     xml.writeAttribute(QStringLiteral("number"), QString::number(gameNumber()));
0188     xml.writeAttribute(QStringLiteral("moves"), QString::number(moveCount()));
0189     xml.writeAttribute(QStringLiteral("started"), QString::number(m_dealStarted));
0190     xml.writeAttribute(QStringLiteral("data"), getGameState());
0191 
0192     const auto patPiles = this->patPiles();
0193     for (const PatPile *p : patPiles) {
0194         xml.writeStartElement(QStringLiteral("pile"));
0195         xml.writeAttribute(QStringLiteral("index"), QString::number(p->index()));
0196         xml.writeAttribute(QStringLiteral("z"), QString::number(p->zValue()));
0197 
0198         const auto cards = p->cards();
0199         for (const KCard *c : cards) {
0200             xml.writeStartElement(QStringLiteral("card"));
0201             xml.writeAttribute(QStringLiteral("suit"), QString::number(c->suit()));
0202             xml.writeAttribute(QStringLiteral("value"), QString::number(c->rank()));
0203             xml.writeAttribute(QStringLiteral("faceup"), QString::number(c->isFaceUp()));
0204             xml.writeAttribute(QStringLiteral("z"), QString::number(c->zValue()));
0205             xml.writeEndElement();
0206         }
0207         xml.writeEndElement();
0208     }
0209     xml.writeEndElement();
0210     xml.writeEndDocument();
0211 
0212     m_dealWasJustSaved = true;
0213 }
0214 
0215 bool DealerScene::loadLegacyFile(QIODevice *io)
0216 {
0217     resetInternals();
0218 
0219     QXmlStreamReader xml(io);
0220 
0221     xml.readNextStartElement();
0222 
0223     // Before KDE4.3, KPat didn't store game specific options in the save
0224     // file. This could cause crashes when loading a Spider game with a
0225     // different number of suits than the current setting. Similarly, in
0226     // Klondike the number of cards drawn from the deck was forgotten, but
0227     // this never caused crashes. Fortunately, in Spider we can count the
0228     // number of suits ourselves. For Klondike, there is no way to recover
0229     // that information.
0230     QString options = xml.attributes().value(QStringLiteral("options")).toString();
0231     if (gameId() == 17 && options.isEmpty()) {
0232         QSet<int> suits;
0233         while (!xml.atEnd())
0234             if (xml.readNextStartElement() && xml.name() == QLatin1String("card"))
0235                 suits << readIntAttribute(xml, QStringLiteral("suit"));
0236         options = QString::number(suits.size());
0237 
0238         // "Rewind" back to the <dealer> tag. Yes, this is ugly.
0239         xml.clear();
0240         io->reset();
0241         xml.setDevice(io);
0242         xml.readNextStartElement();
0243     }
0244     setGameOptions(options);
0245 
0246     m_dealNumber = readIntAttribute(xml, QStringLiteral("number"));
0247     m_loadedMoveCount = readIntAttribute(xml, QStringLiteral("moves"));
0248     m_dealStarted = readIntAttribute(xml, QStringLiteral("started"));
0249     QString gameStateData = xml.attributes().value(QStringLiteral("data")).toString();
0250 
0251     QMultiHash<quint32, KCard *> cards;
0252     const auto deckCards = deck()->cards();
0253     for (KCard *c : deckCards)
0254         cards.insert((c->id() & 0xffff), c);
0255 
0256     QHash<int, PatPile *> piles;
0257     const auto patPiles = this->patPiles();
0258     for (PatPile *p : patPiles)
0259         piles.insert(p->index(), p);
0260 
0261     // Loop through <pile>s.
0262     while (xml.readNextStartElement()) {
0263         if (xml.name() != QLatin1String("pile")) {
0264             qCWarning(KPAT_LOG) << "Expected a \"pile\" tag. Found a" << xml.name() << "tag.";
0265             return false;
0266         }
0267 
0268         bool ok;
0269         int index = readIntAttribute(xml, QStringLiteral("index"), &ok);
0270         QHash<int, PatPile *>::const_iterator it = piles.constFind(index);
0271         if (!ok || it == piles.constEnd()) {
0272             qCWarning(KPAT_LOG) << "Unrecognized pile index:" << xml.attributes().value(QStringLiteral("index"));
0273             return false;
0274         }
0275 
0276         PatPile *p = it.value();
0277         p->clear();
0278 
0279         // Loop through <card>s.
0280         while (xml.readNextStartElement()) {
0281             if (xml.name() != QLatin1String("card")) {
0282                 qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag.";
0283                 return false;
0284             }
0285 
0286             bool suitOk, rankOk, faceUpOk;
0287             int suit = readIntAttribute(xml, QStringLiteral("suit"), &suitOk);
0288             int rank = readIntAttribute(xml, QStringLiteral("value"), &rankOk);
0289             bool faceUp = readIntAttribute(xml, QStringLiteral("faceup"), &faceUpOk);
0290 
0291             quint32 id = KCardDeck::getId(KCardDeck::Suit(suit), KCardDeck::Rank(rank), 0);
0292             KCard *card = cards.take(id);
0293 
0294             if (!suitOk || !rankOk || !faceUpOk || !card) {
0295                 qCWarning(KPAT_LOG) << "Unrecognized card: suit=" << xml.attributes().value(QStringLiteral("suit"))
0296                                     << " value=" << xml.attributes().value(QStringLiteral("value"))
0297                                     << " faceup=" << xml.attributes().value(QStringLiteral("faceup"));
0298                 return false;
0299             }
0300 
0301             card->setFaceUp(faceUp);
0302             p->add(card);
0303 
0304             xml.skipCurrentElement();
0305         }
0306         updatePileLayout(p, 0);
0307     }
0308 
0309     setGameState(gameStateData);
0310 
0311     Q_EMIT updateMoves(moveCount());
0312     takeState();
0313 
0314     return true;
0315 }
0316 
0317 void DealerScene::saveFile(QIODevice *io)
0318 {
0319     QXmlStreamWriter xml(io);
0320     xml.setAutoFormatting(true);
0321     xml.setAutoFormattingIndent(-1);
0322     xml.writeStartDocument();
0323 
0324     xml.writeStartElement(QStringLiteral("kpat-game"));
0325     xml.writeAttribute(QStringLiteral("game-type"), m_di->baseIdString());
0326     if (!getGameOptions().isEmpty())
0327         xml.writeAttribute(QStringLiteral("game-type-options"), getGameOptions());
0328     xml.writeAttribute(QStringLiteral("deal-number"), QString::number(gameNumber()));
0329 
0330     if (!m_currentState) {
0331         deck()->stopAnimations();
0332         takeState();
0333     }
0334 
0335     QList<GameState *> allStates;
0336     for (int i = 0; i < m_undoStack.size(); ++i)
0337         allStates << m_undoStack.at(i);
0338     allStates << m_currentState;
0339     for (int i = m_redoStack.size() - 1; i >= 0; --i)
0340         allStates << m_redoStack.at(i);
0341 
0342     QString lastGameSpecificState;
0343 
0344     for (int i = 0; i < allStates.size(); ++i) {
0345         const GameState *state = allStates.at(i);
0346         xml.writeStartElement(QStringLiteral("state"));
0347         if (state->stateData != lastGameSpecificState) {
0348             xml.writeAttribute(QStringLiteral("game-specific-state"), state->stateData);
0349             lastGameSpecificState = state->stateData;
0350         }
0351         if (i == m_undoStack.size())
0352             xml.writeAttribute(QStringLiteral("current"), QStringLiteral("true"));
0353 
0354         for (const CardStateChange &change : std::as_const(state->changes)) {
0355             xml.writeStartElement(QStringLiteral("move"));
0356             xml.writeAttribute(QStringLiteral("pile"), change.newState.pile->objectName());
0357             xml.writeAttribute(QStringLiteral("position"), QString::number(change.newState.index));
0358 
0359             bool faceChanged = !change.oldState.pile || change.oldState.faceUp != change.newState.faceUp;
0360 
0361             for (const KCard *card : change.cards) {
0362                 xml.writeStartElement(QStringLiteral("card"));
0363                 xml.writeAttribute(QStringLiteral("id"), QStringLiteral("%1").arg(card->id(), 7, 10, QLatin1Char('0')));
0364                 xml.writeAttribute(QStringLiteral("suit"), suitToString(card->suit()));
0365                 xml.writeAttribute(QStringLiteral("rank"), rankToString(card->rank()));
0366                 if (faceChanged)
0367                     xml.writeAttribute(QStringLiteral("turn"), change.newState.faceUp ? QStringLiteral("face-up") : QStringLiteral("face-down"));
0368                 xml.writeEndElement();
0369             }
0370 
0371             xml.writeEndElement();
0372         }
0373         xml.writeEndElement();
0374     }
0375 
0376     xml.writeEndElement();
0377     xml.writeEndDocument();
0378 
0379     m_dealWasJustSaved = true;
0380 }
0381 
0382 // arg_takeState is default true, but we disable it for command line usage of this function
0383 bool DealerScene::loadFile(QIODevice *io, bool arg_takeState)
0384 {
0385     resetInternals();
0386 
0387     bool reenableAutoDrop = autoDropEnabled();
0388     setAutoDropEnabled(false);
0389 
0390     QXmlStreamReader xml(io);
0391 
0392     xml.readNextStartElement();
0393 
0394     if (xml.name() != QLatin1String("kpat-game")) {
0395         qCWarning(KPAT_LOG) << "First tag is not \"kpat-game\"";
0396         return false;
0397     }
0398 
0399     m_dealNumber = readIntAttribute(xml, QStringLiteral("deal-number"));
0400     setGameOptions(xml.attributes().value(QStringLiteral("game-type-options")).toString());
0401 
0402     QMultiHash<quint32, KCard *> cardHash;
0403     const auto cards = deck()->cards();
0404     for (KCard *c : cards)
0405         cardHash.insert(c->id(), c);
0406 
0407     QHash<QString, KCardPile *> pileHash;
0408     const auto piles = this->piles();
0409     for (KCardPile *p : piles)
0410         pileHash.insert(p->objectName(), p);
0411 
0412     int undosToDo = -1;
0413 
0414     while (xml.readNextStartElement()) {
0415         if (xml.name() != QLatin1String("state")) {
0416             qCWarning(KPAT_LOG) << "Expected a \"state\" tag. Found a" << xml.name() << "tag.";
0417             return false;
0418         }
0419 
0420         if (xml.attributes().hasAttribute(QStringLiteral("game-specific-state")))
0421             setGameState(xml.attributes().value(QStringLiteral("game-specific-state")).toString());
0422 
0423         if (undosToDo > -1)
0424             ++undosToDo;
0425         else if (xml.attributes().value(QStringLiteral("current")) == QLatin1String("true"))
0426             undosToDo = 0;
0427 
0428         while (xml.readNextStartElement()) {
0429             if (xml.name() != QLatin1String("move")) {
0430                 qCWarning(KPAT_LOG) << "Expected a \"move\" tag. Found a" << xml.name() << "tag.";
0431                 return false;
0432             }
0433 
0434             QString pileName = xml.attributes().value(QStringLiteral("pile")).toString();
0435             KCardPile *pile = pileHash.value(pileName);
0436 
0437             bool indexOk;
0438             int index = readIntAttribute(xml, QStringLiteral("position"), &indexOk);
0439 
0440             if (!pile || !indexOk) {
0441                 qCWarning(KPAT_LOG) << "Unrecognized pile or index.";
0442                 return false;
0443             }
0444 
0445             while (xml.readNextStartElement()) {
0446                 if (xml.name() != QLatin1String("card")) {
0447                     qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag.";
0448                     return false;
0449                 }
0450 
0451                 KCard *card = cardHash.value(readIntAttribute(xml, QStringLiteral("id")));
0452                 if (!card) {
0453                     qCWarning(KPAT_LOG) << "Unrecognized card.";
0454                     return false;
0455                 }
0456 
0457                 if (xml.attributes().value(QStringLiteral("turn")) == QLatin1String("face-up"))
0458                     card->setFaceUp(true);
0459                 else if (xml.attributes().value(QStringLiteral("turn")) == QLatin1String("face-down"))
0460                     card->setFaceUp(false);
0461 
0462                 pile->insert(index, card);
0463 
0464                 ++index;
0465                 xml.skipCurrentElement();
0466             }
0467         }
0468         if (arg_takeState) {
0469             takeState();
0470         }
0471     }
0472 
0473     m_loadedMoveCount = 0;
0474     m_dealStarted = moveCount() > 0;
0475     Q_EMIT updateMoves(moveCount());
0476 
0477     while (undosToDo > 0) {
0478         undo();
0479         --undosToDo;
0480     }
0481 
0482     for (KCardPile *p : piles)
0483         updatePileLayout(p, 0);
0484 
0485     setAutoDropEnabled(reenableAutoDrop);
0486 
0487     return true;
0488 }
0489 
0490 DealerScene::DealerScene(const DealerInfo *di)
0491     : m_di(di)
0492     , m_solver(nullptr)
0493     , m_solverThread(nullptr)
0494     , m_peekedCard(nullptr)
0495     , m_dealNumber(0)
0496     , m_loadedMoveCount(0)
0497     , m_neededFutureMoves(1)
0498     , m_supportedActions(0)
0499     , m_autoDropEnabled(false)
0500     , m_solverEnabled(false)
0501     , m_dealStarted(false)
0502     , m_dealWasEverWinnable(false)
0503     , m_dealHasBeenWon(false)
0504     , m_dealWasJustSaved(false)
0505     , m_statisticsRecorded(false)
0506     , m_playerReceivedHelp(false)
0507     , m_toldAboutWonGame(false)
0508     , m_toldAboutLostGame(false)
0509     , m_dropSpeedFactor(1)
0510     , m_interruptAutoDrop(false)
0511     , m_dealInProgress(false)
0512     , m_hintInProgress(false)
0513     , m_demoInProgress(false)
0514     , m_dropInProgress(false)
0515     , m_hintQueued(false)
0516     , m_demoQueued(false)
0517     , m_dropQueued(false)
0518     , m_newCardsQueued(false)
0519     , m_takeStateQueued(false)
0520     , m_currentState(nullptr)
0521 {
0522     setItemIndexMethod(QGraphicsScene::NoIndex);
0523 
0524     m_solverUpdateTimer.setInterval(250);
0525     m_solverUpdateTimer.setSingleShot(true);
0526     connect(&m_solverUpdateTimer, &QTimer::timeout, this, &DealerScene::stopAndRestartSolver);
0527 
0528     m_demoTimer.setSingleShot(true);
0529     connect(&m_demoTimer, &QTimer::timeout, this, &DealerScene::demo);
0530 
0531     m_dropTimer.setSingleShot(true);
0532     connect(&m_dropTimer, &QTimer::timeout, this, &DealerScene::drop);
0533 
0534     m_wonItem = new MessageBox();
0535     m_wonItem->setZValue(2000);
0536     m_wonItem->hide();
0537     addItem(m_wonItem);
0538 
0539     connect(this, &DealerScene::cardAnimationDone, this, &DealerScene::animationDone);
0540 
0541     connect(this, &DealerScene::cardDoubleClicked, this, &DealerScene::tryAutomaticMove);
0542     // Make rightClick == doubleClick. See bug #151921
0543     connect(this, &DealerScene::cardRightClicked, this, &DealerScene::tryAutomaticMove);
0544 }
0545 
0546 DealerScene::~DealerScene()
0547 {
0548     stop();
0549 
0550     disconnect();
0551     if (m_solverThread)
0552         m_solverThread->abort();
0553     delete m_solverThread;
0554     m_solverThread = nullptr;
0555     delete m_solver;
0556     m_solver = nullptr;
0557     qDeleteAll(m_undoStack);
0558     delete m_currentState;
0559     qDeleteAll(m_redoStack);
0560     delete m_wonItem;
0561 }
0562 
0563 void DealerScene::addPatPile(PatPile *pile)
0564 {
0565     if (!m_patPiles.contains(pile))
0566         m_patPiles.append(pile);
0567 }
0568 
0569 void DealerScene::removePatPile(PatPile *pile)
0570 {
0571     m_patPiles.removeAll(pile);
0572 }
0573 
0574 void DealerScene::clearPatPiles()
0575 {
0576     const auto patPiles = this->patPiles();
0577     for (PatPile *p : patPiles) {
0578         removePatPile(p);
0579         removePile(p);
0580     }
0581 }
0582 
0583 QList<PatPile *> DealerScene::patPiles() const
0584 {
0585     return m_patPiles;
0586 }
0587 
0588 void DealerScene::cardsMoved(const QList<KCard *> &cards, KCardPile *oldPile, KCardPile *newPile)
0589 {
0590     PatPile *newPatPile = dynamic_cast<PatPile *>(newPile);
0591     PatPile *oldPatPile = dynamic_cast<PatPile *>(oldPile);
0592 
0593     if (oldPatPile && oldPatPile->isFoundation() && newPatPile && !newPatPile->isFoundation()) {
0594         for (KCard *c : cards)
0595             m_cardsRemovedFromFoundations.insert(c);
0596     } else {
0597         for (KCard *c : cards)
0598             m_cardsRemovedFromFoundations.remove(c);
0599     }
0600 
0601     if (!m_dropInProgress && !m_dealInProgress) {
0602         m_dealStarted = true;
0603         takeState();
0604     }
0605 }
0606 
0607 void DealerScene::startHint()
0608 {
0609     stopDemo();
0610     stopDrop();
0611 
0612     if (isCardAnimationRunning()) {
0613         m_hintQueued = true;
0614         return;
0615     }
0616 
0617     if (isKeyboardModeActive())
0618         setKeyboardModeActive(false);
0619 
0620     QList<QGraphicsItem *> toHighlight;
0621     const auto moveHints = getHints();
0622     for (const MoveHint &h : moveHints)
0623         toHighlight << h.card();
0624 
0625     if (!m_winningMoves.isEmpty()) {
0626         MOVE m = m_winningMoves.first();
0627         MoveHint mh = solver()->translateMove(m);
0628         if (mh.isValid())
0629             toHighlight << mh.card();
0630     }
0631 
0632     m_hintInProgress = !toHighlight.isEmpty();
0633     setHighlightedItems(toHighlight);
0634     Q_EMIT hintActive(m_hintInProgress);
0635 }
0636 
0637 void DealerScene::stopHint()
0638 {
0639     if (m_hintInProgress) {
0640         m_hintInProgress = false;
0641         clearHighlightedItems();
0642         Q_EMIT hintActive(false);
0643     }
0644 }
0645 
0646 bool DealerScene::isHintActive() const
0647 {
0648     return m_hintInProgress;
0649 }
0650 
0651 QList<MoveHint> DealerScene::getSolverHints()
0652 {
0653     QList<MoveHint> hintList;
0654 
0655     if (m_solverThread && m_solverThread->isRunning())
0656         m_solverThread->abort();
0657 
0658     solver()->translate_layout();
0659     solver()->patsolve(1);
0660 
0661     const auto moves = solver()->firstMoves();
0662     for (const MOVE &m : moves) {
0663         MoveHint mh = solver()->translateMove(m);
0664         hintList << mh;
0665     }
0666     return hintList;
0667 }
0668 
0669 QList<MoveHint> DealerScene::getHints()
0670 {
0671     if (solver())
0672         return getSolverHints();
0673 
0674     return bruteForceHints();
0675 }
0676 
0677 QList<MoveHint> DealerScene::bruteForceHints()
0678 {
0679     QList<MoveHint> hintList;
0680     const auto patPiles = this->patPiles();
0681     for (PatPile *store : patPiles) {
0682         if (store->isFoundation() || store->isEmpty())
0683             continue;
0684 
0685         QList<KCard *> cards = store->cards();
0686         while (cards.count() && !cards.first()->isFaceUp())
0687             cards.erase(cards.begin());
0688 
0689         QList<KCard *>::Iterator iti = cards.begin();
0690         while (iti != cards.end()) {
0691             if (allowedToRemove(store, (*iti))) {
0692                 for (PatPile *dest : patPiles) {
0693                     int cardIndex = store->indexOf(*iti);
0694                     if (cardIndex == 0 && dest->isEmpty() && !dest->isFoundation())
0695                         continue;
0696 
0697                     if (!checkAdd(dest, dest->cards(), cards))
0698                         continue;
0699 
0700                     if (dest->isFoundation()) {
0701                         hintList << MoveHint(*iti, dest, 127);
0702                     } else {
0703                         QList<KCard *> cardsBelow = cards.mid(0, cardIndex);
0704 
0705                         // if it could be here as well, then it's no use
0706                         if ((cardsBelow.isEmpty() && !dest->isEmpty()) || !checkAdd(store, cardsBelow, cards)) {
0707                             hintList << MoveHint(*iti, dest, 0);
0708                         } else if (checkPrefering(dest, dest->cards(), cards)
0709                                    && !checkPrefering(store, cardsBelow, cards)) { // if checkPrefers says so, we add it nonetheless
0710                             hintList << MoveHint(*iti, dest, 10);
0711                         }
0712                     }
0713                 }
0714             }
0715             cards.erase(iti);
0716             iti = cards.begin();
0717         }
0718     }
0719     return hintList;
0720 }
0721 
0722 static bool prioSort(const MoveHint &c1, const MoveHint &c2)
0723 {
0724     return c1.priority() < c2.priority();
0725 }
0726 
0727 MoveHint DealerScene::chooseHint()
0728 {
0729     if (!m_winningMoves.isEmpty()) {
0730         MOVE m = m_winningMoves.takeFirst();
0731         MoveHint mh = solver()->translateMove(m);
0732 
0733 #ifdef CHOOSE_HINT_DEBUG
0734         if (m.totype == O_Type)
0735             fprintf(stderr, "move from %d out (at %d) Prio: %d\n", m.from, m.turn_index, m.pri);
0736         else
0737             fprintf(stderr, "move from %d to %d (%d) Prio: %d\n", m.from, m.to, m.turn_index, m.pri);
0738 #endif
0739 
0740         return mh;
0741     }
0742 
0743     QList<MoveHint> hintList = getHints();
0744 
0745     if (hintList.isEmpty()) {
0746         return MoveHint();
0747     } else {
0748         // Generate a random number with an exponentional distribution averaging 1/4.
0749         qreal randomExp = qMin<qreal>(-log(1 - QRandomGenerator::global()->generateDouble()) / 4, 1);
0750         int randomIndex = randomExp * (hintList.size() - 1);
0751 
0752         std::sort(hintList.begin(), hintList.end(), prioSort);
0753         return hintList.at(randomIndex);
0754     }
0755 }
0756 
0757 void DealerScene::startNew(int dealNumber)
0758 {
0759     if (dealNumber != -1)
0760         m_dealNumber = qBound(1, dealNumber, INT_MAX);
0761 
0762     // Only record the statistics and reset gameStarted if  we're starting a
0763     // new game number or we're restarting a game we've already won.
0764     if (dealNumber != -1 || m_dealHasBeenWon) {
0765         recordGameStatistics();
0766         m_statisticsRecorded = false;
0767         m_dealStarted = false;
0768     }
0769 
0770     if (isCardAnimationRunning()) {
0771         QTimer::singleShot(100, this, SLOT(startNew()));
0772         return;
0773     }
0774 
0775     if (m_solverThread && m_solverThread->isRunning())
0776         m_solverThread->abort();
0777 
0778     resetInternals();
0779 
0780     Q_EMIT updateMoves(0);
0781 
0782     const auto piles = this->piles();
0783     for (KCardPile *p : piles)
0784         p->clear();
0785 
0786     m_dealInProgress = true;
0787     restart(KpatShuffle::shuffled<KCard *>(deck()->cards(), m_dealNumber));
0788     m_dealInProgress = false;
0789 
0790     takeState();
0791     update();
0792 }
0793 
0794 void DealerScene::resetInternals()
0795 {
0796     stop();
0797 
0798     setKeyboardModeActive(false);
0799 
0800     m_dealHasBeenWon = false;
0801     m_wonItem->hide();
0802 
0803     qDeleteAll(m_undoStack);
0804     m_undoStack.clear();
0805     delete m_currentState;
0806     m_currentState = nullptr;
0807     qDeleteAll(m_redoStack);
0808     m_redoStack.clear();
0809     m_lastKnownCardStates.clear();
0810 
0811     m_dealWasJustSaved = false;
0812     m_dealWasEverWinnable = false;
0813     m_toldAboutLostGame = false;
0814     m_toldAboutWonGame = false;
0815     m_loadedMoveCount = 0;
0816 
0817     m_playerReceivedHelp = false;
0818 
0819     m_dealInProgress = false;
0820 
0821     m_dropInProgress = false;
0822     m_dropSpeedFactor = 1;
0823     m_cardsRemovedFromFoundations.clear();
0824 
0825     const auto cards = deck()->cards();
0826     for (KCard *c : cards) {
0827         c->disconnect(this);
0828         c->stopAnimation();
0829     }
0830 
0831     Q_EMIT solverStateChanged(QString());
0832     Q_EMIT gameInProgress(true);
0833 }
0834 
0835 QPointF posAlongRect(qreal distOnRect, const QRectF &rect)
0836 {
0837     if (distOnRect < rect.width())
0838         return rect.topLeft() + QPointF(distOnRect, 0);
0839     distOnRect -= rect.width();
0840     if (distOnRect < rect.height())
0841         return rect.topRight() + QPointF(0, distOnRect);
0842     distOnRect -= rect.height();
0843     if (distOnRect < rect.width())
0844         return rect.bottomRight() + QPointF(-distOnRect, 0);
0845     distOnRect -= rect.width();
0846     return rect.bottomLeft() + QPointF(0, -distOnRect);
0847 }
0848 
0849 void DealerScene::won()
0850 {
0851     if (m_dealHasBeenWon)
0852         return;
0853 
0854     m_dealHasBeenWon = true;
0855     m_toldAboutWonGame = true;
0856 
0857     stopDemo();
0858     recordGameStatistics();
0859 
0860     Q_EMIT solverStateChanged(QString());
0861 
0862     Q_EMIT newCardsPossible(false);
0863     Q_EMIT undoPossible(false);
0864     Q_EMIT redoPossible(false);
0865     Q_EMIT gameInProgress(false);
0866 
0867     setKeyboardModeActive(false);
0868 
0869     qreal speed = sqrt(width() * width() + height() * height()) / (DURATION_WON);
0870 
0871     QRectF justOffScreen = sceneRect().adjusted(-deck()->cardWidth(), -deck()->cardHeight(), 0, 0);
0872     qreal spacing = 2 * (justOffScreen.width() + justOffScreen.height()) / deck()->cards().size();
0873     qreal distOnRect = 0;
0874 
0875     const auto cards = deck()->cards();
0876     for (KCard *c : cards) {
0877         distOnRect += spacing;
0878         QPointF pos2 = posAlongRect(distOnRect, justOffScreen);
0879         QPointF delta = c->pos() - pos2;
0880         qreal dist = sqrt(delta.x() * delta.x() + delta.y() * delta.y());
0881 
0882         c->setFaceUp(true);
0883         c->animate(pos2, c->zValue(), 0, true, false, dist / speed);
0884     }
0885 
0886     connect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage);
0887 }
0888 
0889 void DealerScene::showWonMessage()
0890 {
0891     disconnect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage);
0892 
0893     // It shouldn't be necessary to stop the demo yet again here, but we
0894     // get crashes if we don't. Will have to look into this further.
0895     stopDemo();
0896 
0897     // Hide all cards to prevent them from showing up accidentally if the
0898     // window is resized.
0899     const auto cards = deck()->cards();
0900     for (KCard *c : cards)
0901         c->hide();
0902 
0903     updateWonItem();
0904     m_wonItem->show();
0905 }
0906 
0907 void DealerScene::updateWonItem()
0908 {
0909     const qreal aspectRatio = Renderer::self()->aspectRatioOfElement(QStringLiteral("message_frame"));
0910     int boxWidth;
0911     int boxHeight;
0912     if (width() < aspectRatio * height()) {
0913         boxWidth = width() * wonBoxToSceneSizeRatio;
0914         boxHeight = boxWidth / aspectRatio;
0915     } else {
0916         boxHeight = height() * wonBoxToSceneSizeRatio;
0917         boxWidth = boxHeight * aspectRatio;
0918     }
0919     m_wonItem->setSize(QSize(boxWidth, boxHeight));
0920 
0921     if (m_playerReceivedHelp)
0922         m_wonItem->setMessage(i18n("Congratulations! We have won."));
0923     else
0924         m_wonItem->setMessage(i18n("Congratulations! You have won."));
0925 
0926     m_wonItem->setPos(QPointF((width() - boxWidth) / 2, (height() - boxHeight) / 2) + sceneRect().topLeft());
0927 }
0928 
0929 bool DealerScene::allowedToAdd(const KCardPile *pile, const QList<KCard *> &cards) const
0930 {
0931     if (!pile->isEmpty() && !pile->topCard()->isFaceUp())
0932         return false;
0933 
0934     const PatPile *p = dynamic_cast<const PatPile *>(pile);
0935     return p && checkAdd(p, p->cards(), cards);
0936 }
0937 
0938 bool DealerScene::allowedToRemove(const KCardPile *pile, const KCard *card) const
0939 {
0940     const PatPile *p = dynamic_cast<const PatPile *>(pile);
0941     QList<KCard *> cards = pile->topCardsDownTo(card);
0942     return p && card->isFaceUp() && !cards.isEmpty() && checkRemove(p, cards);
0943 }
0944 
0945 bool DealerScene::checkAdd(const PatPile *pile, const QList<KCard *> &oldCards, const QList<KCard *> &newCards) const
0946 {
0947     Q_UNUSED(pile)
0948     Q_UNUSED(oldCards)
0949     Q_UNUSED(newCards)
0950     return false;
0951 }
0952 
0953 bool DealerScene::checkRemove(const PatPile *pile, const QList<KCard *> &cards) const
0954 {
0955     Q_UNUSED(pile)
0956     Q_UNUSED(cards)
0957     return false;
0958 }
0959 
0960 bool DealerScene::checkPrefering(const PatPile *pile, const QList<KCard *> &oldCards, const QList<KCard *> &newCards) const
0961 {
0962     Q_UNUSED(pile)
0963     Q_UNUSED(oldCards)
0964     Q_UNUSED(newCards)
0965     return false;
0966 }
0967 
0968 void DealerScene::mousePressEvent(QGraphicsSceneMouseEvent *e)
0969 {
0970     stop();
0971 
0972     const QList<QGraphicsItem *> itemsAtPoint = items(e->scenePos());
0973     KCard *card = qgraphicsitem_cast<KCard *>(itemsAtPoint.isEmpty() ? nullptr : itemsAtPoint.first());
0974 
0975     if (m_peekedCard) {
0976         e->accept();
0977     } else if (e->button() == Qt::RightButton && card && card->pile() && card != card->pile()->topCard() && cardsBeingDragged().isEmpty()
0978                && !isCardAnimationRunning()) {
0979         e->accept();
0980         m_peekedCard = card;
0981         QPointF pos2(card->x() + deck()->cardWidth() / 3.0, card->y() - deck()->cardHeight() / 3.0);
0982         card->setZValue(card->zValue() + 0.1);
0983         card->animate(pos2, card->zValue(), 20, card->isFaceUp(), false, DURATION_FANCYSHOW);
0984     } else {
0985         KCardScene::mousePressEvent(e);
0986         if (!cardsBeingDragged().isEmpty())
0987             Q_EMIT cardsPickedUp();
0988     }
0989 }
0990 
0991 void DealerScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *e)
0992 {
0993     stop();
0994 
0995     if (e->button() == Qt::RightButton && m_peekedCard && m_peekedCard->pile()) {
0996         e->accept();
0997         updatePileLayout(m_peekedCard->pile(), DURATION_FANCYSHOW);
0998         m_peekedCard = nullptr;
0999     } else {
1000         bool hadCards = !cardsBeingDragged().isEmpty();
1001         KCardScene::mouseReleaseEvent(e);
1002         if (hadCards && cardsBeingDragged().isEmpty())
1003             Q_EMIT cardsPutDown();
1004     }
1005 }
1006 
1007 void DealerScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e)
1008 {
1009     if (!m_dealHasBeenWon) {
1010         stop();
1011         KCardScene::mouseDoubleClickEvent(e);
1012     } else {
1013         Q_EMIT newDeal();
1014     }
1015 }
1016 
1017 bool DealerScene::tryAutomaticMove(KCard *card)
1018 {
1019     if (!isCardAnimationRunning() && card && card->pile() && card == card->pile()->topCard() && card->isFaceUp() && allowedToRemove(card->pile(), card)) {
1020         QList<KCard *> cardList = QList<KCard *>() << card;
1021 
1022         const auto patPiles = this->patPiles();
1023         for (PatPile *p : patPiles) {
1024             if (p->isFoundation() && allowedToAdd(p, cardList)) {
1025                 moveCardToPile(card, p, DURATION_MOVE);
1026                 return true;
1027             }
1028         }
1029     }
1030 
1031     return false;
1032 }
1033 
1034 void DealerScene::undo()
1035 {
1036     undoOrRedo(true);
1037 }
1038 
1039 void DealerScene::redo()
1040 {
1041     undoOrRedo(false);
1042 }
1043 
1044 void DealerScene::undoOrRedo(bool undo)
1045 {
1046     stop();
1047 
1048     if (isCardAnimationRunning())
1049         return;
1050 
1051     // The undo and redo actions are almost identical, except for where states
1052     // are pulled from and pushed to, so to keep things generic, we use
1053     // direction dependent const references throughout this code.
1054     QStack<GameState *> &fromStack = undo ? m_undoStack : m_redoStack;
1055     QStack<GameState *> &toStack = undo ? m_redoStack : m_undoStack;
1056 
1057     if (!fromStack.isEmpty() && m_currentState) {
1058         // If we're undoing, we use the oldStates of the changes of the current
1059         // state. If we're redoing, we use the newStates of the changes of the
1060         // nextState.
1061         const QList<CardStateChange> &changes = undo ? m_currentState->changes : fromStack.top()->changes;
1062 
1063         // Update the currentState pointer and undo/redo stacks.
1064         toStack.push(m_currentState);
1065         m_currentState = fromStack.pop();
1066         setGameState(m_currentState->stateData);
1067 
1068         QSet<KCardPile *> pilesAffected;
1069 
1070         auto processCardStateChange = [&](const CardStateChange &change) {
1071             CardState sourceState = undo ? change.newState : change.oldState;
1072             CardState destState = undo ? change.oldState : change.newState;
1073 
1074             PatPile *sourcePile = dynamic_cast<PatPile *>(sourceState.pile);
1075             PatPile *destPile = dynamic_cast<PatPile *>(destState.pile);
1076             bool notDroppable = destState.takenDown || ((sourcePile && sourcePile->isFoundation()) && !(destPile && destPile->isFoundation()));
1077 
1078             pilesAffected << sourceState.pile << destState.pile;
1079 
1080             for (KCard *c : change.cards) {
1081                 m_lastKnownCardStates.insert(c, destState);
1082 
1083                 c->setFaceUp(destState.faceUp);
1084                 destState.pile->insert(destState.index, c);
1085 
1086                 if (notDroppable)
1087                     m_cardsRemovedFromFoundations.insert(c);
1088                 else
1089                     m_cardsRemovedFromFoundations.remove(c);
1090 
1091                 ++sourceState.index;
1092                 ++destState.index;
1093             }
1094         };
1095 
1096         if (undo) {
1097             for (auto it = changes.rbegin(); it != changes.rend(); ++it) {
1098                 processCardStateChange(*it);
1099             }
1100         } else {
1101             for (const CardStateChange &change : changes) {
1102                 processCardStateChange(change);
1103             }
1104         }
1105 
1106         // At this point all cards should be in the right piles, but not
1107         // necessarily at the right positions within those piles. So we
1108         // run through the piles involved and swap card positions until
1109         // everything is back in its place, then relayout the piles.
1110         for (KCardPile *p : std::as_const(pilesAffected)) {
1111             int i = 0;
1112             while (i < p->count()) {
1113                 int index = m_lastKnownCardStates.value(p->at(i)).index;
1114                 if (i == index)
1115                     ++i;
1116                 else
1117                     p->swapCards(i, index);
1118             }
1119 
1120             updatePileLayout(p, 0);
1121         }
1122 
1123         Q_EMIT updateMoves(moveCount());
1124         Q_EMIT undoPossible(!m_undoStack.isEmpty());
1125         Q_EMIT redoPossible(!m_redoStack.isEmpty());
1126 
1127         if (m_toldAboutLostGame) // everything's possible again
1128         {
1129             gameInProgress(true);
1130             m_toldAboutLostGame = false;
1131             m_toldAboutWonGame = false;
1132         }
1133 
1134         int solvability = m_currentState->solvability;
1135         m_winningMoves = m_currentState->winningMoves;
1136 
1137         Q_EMIT solverStateChanged(solverStatusMessage(solvability, m_dealWasEverWinnable));
1138 
1139         if (m_solver && (solvability == SolverInterface::SearchAborted || solvability == SolverInterface::MemoryLimitReached)) {
1140             startSolver();
1141         }
1142     }
1143 }
1144 
1145 void DealerScene::takeState()
1146 {
1147     if (isCardAnimationRunning()) {
1148         m_takeStateQueued = true;
1149         return;
1150     }
1151 
1152     if (!isDemoActive())
1153         m_winningMoves.clear();
1154 
1155     QList<CardStateChange> changes;
1156 
1157     const auto piles = this->piles();
1158     for (KCardPile *p : piles) {
1159         QList<KCard *> currentRun;
1160         CardState oldRunState;
1161         CardState newRunState;
1162 
1163         for (int i = 0; i < p->count(); ++i) {
1164             KCard *c = p->at(i);
1165 
1166             const CardState &oldState = m_lastKnownCardStates.value(c);
1167             CardState newState(p, i, c->isFaceUp(), m_cardsRemovedFromFoundations.contains(c));
1168 
1169             // The card has changed.
1170             if (newState != oldState) {
1171                 // There's a run in progress, but this card isn't part of it.
1172                 if (!currentRun.isEmpty()
1173                     && (oldState.pile != oldRunState.pile || (oldState.index != -1 && oldState.index != oldRunState.index + currentRun.size())
1174                         || oldState.faceUp != oldRunState.faceUp || newState.faceUp != newRunState.faceUp || oldState.takenDown != oldRunState.takenDown
1175                         || newState.takenDown != newRunState.takenDown)) {
1176                     changes << CardStateChange(oldRunState, newRunState, currentRun);
1177                     currentRun.clear();
1178                 }
1179 
1180                 // This card is the start of a new run.
1181                 if (currentRun.isEmpty()) {
1182                     oldRunState = oldState;
1183                     newRunState = newState;
1184                 }
1185 
1186                 currentRun << c;
1187 
1188                 m_lastKnownCardStates.insert(c, newState);
1189             }
1190         }
1191         // Add the last run, if any.
1192         if (!currentRun.isEmpty()) {
1193             changes << CardStateChange(oldRunState, newRunState, currentRun);
1194         }
1195     }
1196 
1197     // If nothing has changed, we're done.
1198     if (changes.isEmpty() && m_currentState && m_currentState->stateData == getGameState()) {
1199         return;
1200     }
1201 
1202     if (m_currentState) {
1203         m_undoStack.push(m_currentState);
1204         qDeleteAll(m_redoStack);
1205         m_redoStack.clear();
1206     }
1207     m_currentState = new GameState(changes, getGameState());
1208 
1209     Q_EMIT redoPossible(false);
1210     Q_EMIT undoPossible(!m_undoStack.isEmpty());
1211     Q_EMIT updateMoves(moveCount());
1212 
1213     m_dealWasJustSaved = false;
1214     if (isGameWon()) {
1215         won();
1216         return;
1217     }
1218 
1219     if (!m_toldAboutWonGame && !m_toldAboutLostGame && isGameLost()) {
1220         Q_EMIT gameInProgress(false);
1221         Q_EMIT solverStateChanged(i18n("Solver: This game is lost."));
1222         m_toldAboutLostGame = true;
1223         stopDemo();
1224         return;
1225     }
1226 
1227     if (!isDemoActive() && !isCardAnimationRunning() && m_solver)
1228         startSolver();
1229 
1230     if (autoDropEnabled() && !isDropActive() && !isDemoActive() && m_redoStack.isEmpty()) {
1231         if (m_interruptAutoDrop)
1232             m_interruptAutoDrop = false;
1233         else
1234             m_dropQueued = true;
1235     }
1236 }
1237 
1238 void DealerScene::setSolverEnabled(bool a)
1239 {
1240     m_solverEnabled = a;
1241 }
1242 
1243 void DealerScene::setAutoDropEnabled(bool enabled)
1244 {
1245     m_autoDropEnabled = enabled;
1246 }
1247 
1248 bool DealerScene::autoDropEnabled() const
1249 {
1250     return m_autoDropEnabled;
1251 }
1252 
1253 void DealerScene::startDrop()
1254 {
1255     stopHint();
1256     stopDemo();
1257 
1258     if (isCardAnimationRunning()) {
1259         m_dropQueued = true;
1260         return;
1261     }
1262 
1263     m_dropInProgress = true;
1264     m_interruptAutoDrop = false;
1265     m_dropSpeedFactor = 1;
1266     Q_EMIT dropActive(true);
1267 
1268     drop();
1269 }
1270 
1271 void DealerScene::stopDrop()
1272 {
1273     if (m_dropInProgress) {
1274         m_dropTimer.stop();
1275         m_dropInProgress = false;
1276         Q_EMIT dropActive(false);
1277 
1278         if (autoDropEnabled() && m_takeStateQueued)
1279             m_interruptAutoDrop = true;
1280     }
1281 }
1282 
1283 bool DealerScene::isDropActive() const
1284 {
1285     return m_dropInProgress;
1286 }
1287 
1288 bool DealerScene::drop()
1289 {
1290     const auto moveHints = getHints();
1291     for (const MoveHint &mh : moveHints) {
1292         if (mh.pile() && mh.pile()->isFoundation() && mh.priority() > 120 && !m_cardsRemovedFromFoundations.contains(mh.card())) {
1293             QList<KCard *> cards = mh.card()->pile()->topCardsDownTo(mh.card());
1294 
1295             QMap<KCard *, QPointF> oldPositions;
1296             for (KCard *c : std::as_const(cards))
1297                 oldPositions.insert(c, c->pos());
1298 
1299             moveCardsToPile(cards, mh.pile(), DURATION_MOVE);
1300 
1301             int count = 0;
1302             for (KCard *c : std::as_const(cards)) {
1303                 c->completeAnimation();
1304                 QPointF destPos = c->pos();
1305                 c->setPos(oldPositions.value(c));
1306 
1307                 int duration = speedUpTime(DURATION_AUTODROP + count * DURATION_AUTODROP / 10);
1308                 c->animate(destPos, c->zValue(), 0, c->isFaceUp(), true, duration);
1309 
1310                 ++count;
1311             }
1312 
1313             m_dropSpeedFactor *= AUTODROP_SPEEDUP_FACTOR;
1314 
1315             takeState();
1316 
1317             return true;
1318         }
1319     }
1320 
1321     m_dropInProgress = false;
1322     Q_EMIT dropActive(false);
1323 
1324     return false;
1325 }
1326 
1327 int DealerScene::speedUpTime(int delay) const
1328 {
1329     if (delay < DURATION_AUTODROP_MINIMUM)
1330         return delay;
1331     else
1332         return qMax<int>(delay * m_dropSpeedFactor, DURATION_AUTODROP_MINIMUM);
1333 }
1334 
1335 void DealerScene::stopAndRestartSolver()
1336 {
1337     if (m_toldAboutLostGame || m_toldAboutWonGame) // who cares?
1338         return;
1339 
1340     if (m_solverThread && m_solverThread->isRunning()) {
1341         m_solverThread->abort();
1342     }
1343 
1344     if (isCardAnimationRunning()) {
1345         startSolver();
1346         return;
1347     }
1348 
1349     slotSolverEnded();
1350 }
1351 
1352 void DealerScene::slotSolverEnded()
1353 {
1354     if (m_solverThread && m_solverThread->isRunning())
1355         return;
1356 
1357     m_solver->translate_layout();
1358     m_winningMoves.clear();
1359     Q_EMIT solverStateChanged(i18n("Solver: Calculating..."));
1360     if (!m_solverThread) {
1361         m_solverThread = new SolverThread(m_solver);
1362         connect(m_solverThread, &SolverThread::finished, this, &DealerScene::slotSolverFinished);
1363     }
1364     m_solverThread->start(m_solverEnabled ? QThread::IdlePriority : QThread::NormalPriority);
1365 }
1366 
1367 void DealerScene::slotSolverFinished(int result)
1368 {
1369     if (result == SolverInterface::SolutionExists) {
1370         m_winningMoves = m_solver->winMoves();
1371         m_dealWasEverWinnable = true;
1372     }
1373 
1374     Q_EMIT solverStateChanged(solverStatusMessage(result, m_dealWasEverWinnable));
1375 
1376     if (m_currentState) {
1377         m_currentState->solvability = static_cast<SolverInterface::ExitStatus>(result);
1378         m_currentState->winningMoves = m_winningMoves;
1379     }
1380 
1381     if (result == SolverInterface::SearchAborted)
1382         startSolver();
1383 }
1384 
1385 int DealerScene::gameNumber() const
1386 {
1387     return m_dealNumber;
1388 }
1389 
1390 void DealerScene::stop()
1391 {
1392     stopHint();
1393     stopDemo();
1394     stopDrop();
1395 }
1396 
1397 void DealerScene::animationDone()
1398 {
1399     Q_ASSERT(!isCardAnimationRunning());
1400 
1401     if (!m_multiStepMoves.isEmpty()) {
1402         continueMultiStepMove();
1403         return;
1404     }
1405 
1406     if (m_takeStateQueued) {
1407         m_takeStateQueued = false;
1408         takeState();
1409     }
1410 
1411     if (m_demoInProgress) {
1412         m_demoTimer.start(TIME_BETWEEN_MOVES);
1413     } else if (m_dropInProgress) {
1414         m_dropTimer.start(speedUpTime(TIME_BETWEEN_MOVES));
1415     } else if (m_newCardsQueued) {
1416         m_newCardsQueued = false;
1417         newCards();
1418     } else if (m_hintQueued) {
1419         m_hintQueued = false;
1420         startHint();
1421     } else if (m_demoQueued) {
1422         m_demoQueued = false;
1423         startDemo();
1424     } else if (m_dropQueued) {
1425         m_dropQueued = false;
1426         startDrop();
1427     }
1428 }
1429 
1430 void DealerScene::startDemo()
1431 {
1432     stopHint();
1433     stopDrop();
1434 
1435     if (isCardAnimationRunning()) {
1436         m_demoQueued = true;
1437         return;
1438     }
1439 
1440     m_demoInProgress = true;
1441     m_playerReceivedHelp = true;
1442     m_dealStarted = true;
1443 
1444     demo();
1445 }
1446 
1447 void DealerScene::stopDemo()
1448 {
1449     if (m_demoInProgress) {
1450         m_demoTimer.stop();
1451         m_demoInProgress = false;
1452         Q_EMIT demoActive(false);
1453     }
1454 }
1455 
1456 bool DealerScene::isDemoActive() const
1457 {
1458     return m_demoInProgress;
1459 }
1460 
1461 void DealerScene::demo()
1462 {
1463     if (isCardAnimationRunning()) {
1464         m_demoQueued = true;
1465         return;
1466     }
1467 
1468     m_demoInProgress = true;
1469     m_playerReceivedHelp = true;
1470     m_dealStarted = true;
1471     clearHighlightedItems();
1472 
1473     m_demoTimer.stop();
1474 
1475     MoveHint mh = chooseHint();
1476     if (mh.isValid()) {
1477         KCard *card = mh.card();
1478         Q_ASSERT(card);
1479         KCardPile *sourcePile = mh.card()->pile();
1480         Q_ASSERT(sourcePile);
1481         Q_ASSERT(allowedToRemove(sourcePile, card));
1482         PatPile *destPile = mh.pile();
1483         Q_ASSERT(destPile);
1484         Q_ASSERT(sourcePile != destPile);
1485         QList<KCard *> cards = sourcePile->topCardsDownTo(card);
1486         Q_ASSERT(allowedToAdd(destPile, cards));
1487 
1488         if (destPile->isEmpty()) {
1489             qCDebug(KPAT_LOG) << "Moving" << card->objectName() << "from the" << sourcePile->objectName() << "pile to the" << destPile->objectName()
1490                               << "pile, which is empty";
1491         } else {
1492             qCDebug(KPAT_LOG) << "Moving" << card->objectName() << "from the" << sourcePile->objectName() << "pile to the" << destPile->objectName()
1493                               << "pile, putting it on top of" << destPile->topCard()->objectName();
1494         }
1495 
1496         moveCardsToPile(cards, destPile, DURATION_DEMO);
1497     } else if (!newCards()) {
1498         if (isGameWon()) {
1499             won();
1500         } else {
1501             stopDemo();
1502             slotSolverEnded();
1503         }
1504         return;
1505     }
1506 
1507     Q_EMIT demoActive(true);
1508     takeState();
1509 }
1510 
1511 void DealerScene::drawDealRowOrRedeal()
1512 {
1513     stop();
1514 
1515     if (isCardAnimationRunning()) {
1516         m_newCardsQueued = true;
1517         return;
1518     }
1519 
1520     m_newCardsQueued = false;
1521     newCards();
1522 }
1523 
1524 bool DealerScene::newCards()
1525 {
1526     return false;
1527 }
1528 
1529 void DealerScene::setSolver(SolverInterface *s)
1530 {
1531     delete m_solver;
1532     delete m_solverThread;
1533     m_solver = s;
1534     m_solverThread = nullptr;
1535 }
1536 
1537 bool DealerScene::isGameWon() const
1538 {
1539     const auto patPiles = this->patPiles();
1540     for (PatPile *p : patPiles) {
1541         if (!p->isFoundation() && !p->isEmpty())
1542             return false;
1543     }
1544     return true;
1545 }
1546 
1547 void DealerScene::startSolver()
1548 {
1549     if (m_solverEnabled)
1550         m_solverUpdateTimer.start();
1551 }
1552 
1553 bool DealerScene::isGameLost() const
1554 {
1555     if (!m_winningMoves.isEmpty()) {
1556         return false;
1557     }
1558     if (solver()) {
1559         if (m_solverThread && m_solverThread->isRunning())
1560             m_solverThread->abort();
1561 
1562         solver()->translate_layout();
1563         return solver()->patsolve(neededFutureMoves()) == SolverInterface::NoSolutionExists;
1564     }
1565     return false;
1566 }
1567 
1568 void DealerScene::recordGameStatistics()
1569 {
1570     // Don't record the game if it was never started, if it is unchanged since
1571     // it was last saved (allowing the user to close KPat after saving without
1572     // it recording a loss) or if it has already been recorded.//         takeState(); // copying it again
1573     if (m_dealStarted && !m_dealWasJustSaved && !m_statisticsRecorded) {
1574         int id = oldId();
1575 
1576         QString totalPlayedKey = QStringLiteral("total%1").arg(id);
1577         QString wonKey = QStringLiteral("won%1").arg(id);
1578         QString winStreakKey = QStringLiteral("winstreak%1").arg(id);
1579         QString maxWinStreakKey = QStringLiteral("maxwinstreak%1").arg(id);
1580         QString loseStreakKey = QStringLiteral("loosestreak%1").arg(id);
1581         QString maxLoseStreakKey = QStringLiteral("maxloosestreak%1").arg(id);
1582         QString minMovesKey = QStringLiteral("minmoves%1").arg(id);
1583 
1584         KConfigGroup config(KSharedConfig::openConfig(), scores_group);
1585 
1586         int totalPlayed = config.readEntry(totalPlayedKey, 0);
1587         int won = config.readEntry(wonKey, 0);
1588         int winStreak = config.readEntry(winStreakKey, 0);
1589         int maxWinStreak = config.readEntry(maxWinStreakKey, 0);
1590         int loseStreak = config.readEntry(loseStreakKey, 0);
1591         int maxLoseStreak = config.readEntry(maxLoseStreakKey, 0);
1592         int minMoves = config.readEntry(minMovesKey, -1);
1593 
1594         ++totalPlayed;
1595 
1596         if (m_dealHasBeenWon) {
1597             ++won;
1598             ++winStreak;
1599             maxWinStreak = qMax(winStreak, maxWinStreak);
1600             loseStreak = 0;
1601             if (minMoves < 0)
1602                 minMoves = moveCount();
1603             else
1604                 minMoves = qMin(minMoves, moveCount());
1605         } else {
1606             ++loseStreak;
1607             maxLoseStreak = qMax(loseStreak, maxLoseStreak);
1608             winStreak = 0;
1609         }
1610 
1611         config.writeEntry(totalPlayedKey, totalPlayed);
1612         config.writeEntry(wonKey, won);
1613         config.writeEntry(winStreakKey, winStreak);
1614         config.writeEntry(maxWinStreakKey, maxWinStreak);
1615         config.writeEntry(loseStreakKey, loseStreak);
1616         config.writeEntry(maxLoseStreakKey, maxLoseStreak);
1617         config.writeEntry(minMovesKey, minMoves);
1618 
1619         m_statisticsRecorded = true;
1620     }
1621 }
1622 
1623 void DealerScene::relayoutScene()
1624 {
1625     KCardScene::relayoutScene();
1626 
1627     if (m_wonItem->isVisible())
1628         updateWonItem();
1629 }
1630 
1631 int DealerScene::gameId() const
1632 {
1633     return m_di->baseId();
1634 }
1635 
1636 void DealerScene::setActions(int actions)
1637 {
1638     m_supportedActions = actions;
1639 }
1640 
1641 int DealerScene::actions() const
1642 {
1643     return m_supportedActions;
1644 }
1645 
1646 QList<QAction *> DealerScene::configActions() const
1647 {
1648     return QList<QAction *>();
1649 }
1650 
1651 SolverInterface *DealerScene::solver() const
1652 {
1653     return m_solver;
1654 }
1655 
1656 int DealerScene::neededFutureMoves() const
1657 {
1658     return m_neededFutureMoves;
1659 }
1660 
1661 void DealerScene::setNeededFutureMoves(int i)
1662 {
1663     m_neededFutureMoves = i;
1664 }
1665 
1666 void DealerScene::setDeckContents(int copies, const QList<KCardDeck::Suit> &suits)
1667 {
1668     Q_ASSERT(copies >= 1);
1669     Q_ASSERT(!suits.isEmpty());
1670 
1671     // Note that the order in which the cards are created can not be changed
1672     // without breaking the game numbering. For historical reasons, KPat
1673     // generates card by rank and then by suit, rather than the more common
1674     // suit then rank ordering.
1675     QList<quint32> ids;
1676     unsigned int number = 0;
1677     for (int i = 0; i < copies; ++i) {
1678         const auto ranks = KCardDeck::standardRanks();
1679         for (const KCardDeck::Rank &r : ranks)
1680             for (const KCardDeck::Suit &s : suits)
1681                 ids << KCardDeck::getId(s, r, number++);
1682     }
1683     deck()->setDeckContents(ids);
1684 }
1685 
1686 QImage DealerScene::createDump() const
1687 {
1688     const QSize previewSize(480, 320);
1689 
1690     const auto cards = deck()->cards();
1691     for (KCard *c : cards)
1692         c->completeAnimation();
1693 
1694     QMultiMap<qreal, QGraphicsItem *> itemsByZ;
1695     const auto items = this->items();
1696     for (QGraphicsItem *item : items) {
1697         Q_ASSERT(item->zValue() >= 0);
1698         itemsByZ.insert(item->zValue(), item);
1699     }
1700 
1701     QImage img(contentArea().size().toSize(), QImage::Format_ARGB32);
1702     img.fill(Qt::transparent);
1703     QPainter p(&img);
1704 
1705     for (QGraphicsItem *item : std::as_const(itemsByZ)) {
1706         if (item->isVisible()) {
1707             p.save();
1708             p.setTransform(item->deviceTransform(p.worldTransform()), false);
1709             item->paint(&p, nullptr);
1710             p.restore();
1711         }
1712     }
1713 
1714     p.end();
1715 
1716     img = img.scaled(previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
1717 
1718     QImage img2(previewSize, QImage::Format_ARGB32);
1719     img2.fill(Qt::transparent);
1720     QPainter p2(&img2);
1721     p2.drawImage((img2.width() - img.width()) / 2, (img2.height() - img.height()) / 2, img);
1722     p2.end();
1723 
1724     return img2;
1725 }
1726 
1727 void DealerScene::mapOldId(int id)
1728 {
1729     Q_UNUSED(id);
1730 }
1731 
1732 int DealerScene::oldId() const
1733 {
1734     return gameId();
1735 }
1736 
1737 QString DealerScene::getGameState() const
1738 {
1739     return QString();
1740 }
1741 
1742 void DealerScene::setGameState(const QString &state)
1743 {
1744     Q_UNUSED(state);
1745 }
1746 
1747 QString DealerScene::getGameOptions() const
1748 {
1749     return QString();
1750 }
1751 
1752 void DealerScene::setGameOptions(const QString &options)
1753 {
1754     Q_UNUSED(options);
1755 }
1756 
1757 bool DealerScene::allowedToStartNewGame()
1758 {
1759     // Check if the user is already running a game, and if she is,
1760     // then ask if she wants to abort it.
1761     return !m_dealStarted || m_dealWasJustSaved || m_toldAboutWonGame || m_toldAboutLostGame
1762         || KMessageBox::warningContinueCancel(QApplication::activeWindow(),
1763                                               i18n("A new game has been requested, but there is already a game in progress.\n\n"
1764                                                    "A loss will be recorded in the statistics if the current game is abandoned."),
1765                                               i18n("Abandon Current Game?"),
1766                                               KGuiItem(i18n("Abandon Current Game")),
1767                                               KStandardGuiItem::cancel(),
1768                                               QStringLiteral("careaboutstats"))
1769         == KMessageBox::Continue;
1770 }
1771 
1772 void DealerScene::addCardForDeal(KCardPile *pile, KCard *card, bool faceUp, QPointF startPos)
1773 {
1774     Q_ASSERT(card);
1775     Q_ASSERT(pile);
1776 
1777     card->setFaceUp(faceUp);
1778     pile->add(card);
1779     m_initDealPositions.insert(card, startPos);
1780 }
1781 
1782 void DealerScene::startDealAnimation()
1783 {
1784     qreal speed = sqrt(width() * width() + height() * height()) / (DURATION_DEAL);
1785     const auto patPiles = this->patPiles();
1786     for (PatPile *p : patPiles) {
1787         updatePileLayout(p, 0);
1788         const auto cards = p->cards();
1789         for (KCard *c : cards) {
1790             if (!m_initDealPositions.contains(c))
1791                 continue;
1792 
1793             QPointF pos2 = c->pos();
1794             c->setPos(m_initDealPositions.value(c));
1795 
1796             QPointF delta = c->pos() - pos2;
1797             qreal dist = sqrt(delta.x() * delta.x() + delta.y() * delta.y());
1798             int duration = qRound(dist / speed);
1799             c->animate(pos2, c->zValue(), 0, c->isFaceUp(), false, duration);
1800         }
1801     }
1802     m_initDealPositions.clear();
1803 }
1804 
1805 void DealerScene::multiStepMove(const QList<KCard *> &cards,
1806                                 KCardPile *pile,
1807                                 const QList<KCardPile *> &freePiles,
1808                                 const QList<KCardPile *> &freeCells,
1809                                 int duration)
1810 {
1811     Q_ASSERT(cards.size() == 1 || !freePiles.isEmpty() || !freeCells.isEmpty());
1812 
1813     m_multiStepMoves.clear();
1814     m_multiStepDuration = duration;
1815 
1816     multiStepSubMove(cards, pile, freePiles, freeCells);
1817     continueMultiStepMove();
1818 }
1819 
1820 void DealerScene::multiStepSubMove(QList<KCard *> cards, KCardPile *pile, QList<KCardPile *> freePiles, const QList<KCardPile *> &freeCells)
1821 {
1822     // Note that cards and freePiles are passed by value, as we need to make a
1823     // local copy anyway.
1824 
1825     // Using n free cells, we can move a run of n+1 cards. If we want to move
1826     // more than that, we have to recursively move some of our cards to one of
1827     // the free piles temporarily.
1828     const int freeCellsPlusOne = freeCells.size() + 1;
1829     int cardsToSubMove = cards.size() - freeCellsPlusOne;
1830 
1831     QList<QPair<KCardPile *, QList<KCard *>>> tempMoves;
1832     while (cardsToSubMove > 0) {
1833         int tempMoveSize;
1834         if (cardsToSubMove <= freePiles.size() * freeCellsPlusOne) {
1835             // If the cards that have to be submoved can be spread across the
1836             // the free piles without putting more than freeCellsPlusOne cards
1837             // on each one, we do so. This means that none of our submoves will
1838             // need further submoves, which keeps the total move count down. We
1839             // Just to a simple rounding up integer division.
1840             tempMoveSize = (cardsToSubMove + freePiles.size() - 1) / freePiles.size();
1841         } else {
1842             // Otherwise, we use the space optimal method that gets the cards
1843             // moved using a minimal number of piles, but uses more submoves.
1844             tempMoveSize = freeCellsPlusOne;
1845             while (tempMoveSize * 2 < cardsToSubMove)
1846                 tempMoveSize *= 2;
1847         }
1848 
1849         QList<KCard *> subCards;
1850         for (int i = 0; i < tempMoveSize; ++i)
1851             subCards.prepend(cards.takeLast());
1852 
1853         Q_ASSERT(!freePiles.isEmpty());
1854         KCardPile *nextPile = freePiles.takeFirst();
1855 
1856         tempMoves << qMakePair(nextPile, subCards);
1857         multiStepSubMove(subCards, nextPile, freePiles, freeCells);
1858 
1859         cardsToSubMove -= tempMoveSize;
1860     }
1861 
1862     // Move cards to free cells.
1863     for (int i = 0; i < cards.size() - 1; ++i) {
1864         KCard *c = cards.at(cards.size() - 1 - i);
1865         m_multiStepMoves << qMakePair(c, freeCells[i]);
1866     }
1867 
1868     // Move bottom card to destination pile.
1869     m_multiStepMoves << qMakePair(cards.first(), pile);
1870 
1871     // Move cards from free cells to destination pile.
1872     for (int i = 1; i < cards.size(); ++i)
1873         m_multiStepMoves << qMakePair(cards.at(i), pile);
1874 
1875     // If we just moved the bottomost card of the source pile, it must now be
1876     // empty and we won't need it any more. So we return it to the list of free
1877     // piles.
1878     KCardPile *sourcePile = cards.first()->pile();
1879     if (sourcePile->at(0) == cards.first())
1880         freePiles << sourcePile;
1881 
1882     // If we had to do any submoves, we now move those cards from their
1883     // temporary pile to the destination pile and free up their temporary pile.
1884     while (!tempMoves.isEmpty()) {
1885         QPair<KCardPile *, QList<KCard *>> m = tempMoves.takeLast();
1886         multiStepSubMove(m.second, pile, freePiles, freeCells);
1887         freePiles << m.first;
1888     }
1889 }
1890 
1891 void DealerScene::continueMultiStepMove()
1892 {
1893     Q_ASSERT(!m_multiStepMoves.isEmpty());
1894     Q_ASSERT(!isCardAnimationRunning());
1895 
1896     QPair<KCard *, KCardPile *> m = m_multiStepMoves.takeFirst();
1897     KCard *card = m.first;
1898     KCardPile *dest = m.second;
1899     KCardPile *source = card->pile();
1900 
1901     Q_ASSERT(card == source->topCard());
1902     Q_ASSERT(allowedToAdd(dest, QList<KCard *>() << card));
1903 
1904     m_multiStepDuration = qMax<int>(m_multiStepDuration * 0.9, 50);
1905 
1906     dest->add(card);
1907     card->raise();
1908     updatePileLayout(dest, m_multiStepDuration);
1909     updatePileLayout(source, m_multiStepDuration);
1910 
1911     if (m_multiStepMoves.isEmpty())
1912         takeState();
1913 }
1914 
1915 #include "dealer.moc"
1916 #include "moc_dealer.cpp"