File indexing completed on 2024-05-19 04:06:15
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"