1 /*
2 * Copyright (C) 1995 Paul Olav Tvete <paul@troll.no>
3 * Copyright (C) 2000-2009 Stephan Kulow <coolo@kde.org>
4 * Copyright (C) 2009-2010 Parker Coates <coates@kde.org>
5 *
6 * License of original code:
7 * -------------------------------------------------------------------------
8 * Permission to use, copy, modify, and distribute this software and its
9 * documentation for any purpose and without fee is hereby granted,
10 * provided that the above copyright notice appear in all copies and that
11 * both that copyright notice and this permission notice appear in
12 * supporting documentation.
13 *
14 * This file is provided AS IS with no warranties of any kind. The author
15 * shall have no liability with respect to the infringement of copyrights,
16 * trade secrets or any patents by this file or any part thereof. In no
17 * event will the author be liable for any lost revenue or profits or
18 * other special, indirect and consequential damages.
19 * -------------------------------------------------------------------------
20 *
21 * License of modifications/additions made after 2009-01-01:
22 * -------------------------------------------------------------------------
23 * This program is free software; you can redistribute it and/or
24 * modify it under the terms of the GNU General Public License as
25 * published by the Free Software Foundation; either version 2 of
26 * the License, or (at your option) any later version.
27 *
28 * This program is distributed in the hope that it will be useful,
29 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 * GNU General Public License for more details.
32 *
33 * You should have received a copy of the GNU General Public License
34 * along with this program. If not, see <http://www.gnu.org/licenses/>.
35 * -------------------------------------------------------------------------
36 */
37
38 #include "dealer.h"
39
40 // own
41 #include "dealerinfo.h"
42 #include "kpat_debug.h"
43 #include "messagebox.h"
44 #include "renderer.h"
45 #include "shuffle.h"
46 #include "patsolve/solverinterface.h"
47 // KCardGame
48 #include <KCardTheme>
49 // KF
50 #include <KConfigGroup>
51 #include <KLocalizedString>
52 #include <KMessageBox>
53 #include <KSharedConfig>
54 // Qt
55 #include <QRandomGenerator>
56 #include <QThread>
57 #include <QXmlStreamReader>
58 #include <QXmlStreamWriter>
59 #include <QGraphicsSceneMouseEvent>
60 // Std
61 #include <cmath>
62
63
64 namespace
65 {
66 const qreal wonBoxToSceneSizeRatio = 0.7;
67
solverStatusMessage(int status,bool everWinnable)68 QString solverStatusMessage( int status, bool everWinnable )
69 {
70 switch ( status )
71 {
72 case SolverInterface::SolutionExists:
73 return i18n("Solver: This game is winnable.");
74 case SolverInterface::NoSolutionExists:
75 return everWinnable ? i18n("Solver: This game is no longer winnable.")
76 : i18n("Solver: This game cannot be won.");
77 case SolverInterface::UnableToDetermineSolvability:
78 return i18n("Solver: Unable to determine if this game is winnable.");
79 case SolverInterface::SearchAborted:
80 case SolverInterface::MemoryLimitReached:
81 default:
82 return QString();
83 }
84 }
85
readIntAttribute(const QXmlStreamReader & xml,const QString & key,bool * ok=nullptr)86 int readIntAttribute( const QXmlStreamReader & xml, const QString & key, bool * ok = nullptr )
87 {
88 QStringRef value = xml.attributes().value( key );
89 return QString::fromRawData( value.data(), value.length() ).toInt( ok );
90 }
91
suitToString(int suit)92 QString suitToString( int suit )
93 {
94 switch ( suit )
95 {
96 case KCardDeck::Clubs:
97 return QStringLiteral("clubs");
98 case KCardDeck::Diamonds:
99 return QStringLiteral("diamonds");
100 case KCardDeck::Hearts:
101 return QStringLiteral("hearts");
102 case KCardDeck::Spades:
103 return QStringLiteral("spades");
104 default:
105 return QString();
106 }
107 }
108
rankToString(int rank)109 QString rankToString( int rank )
110 {
111 switch ( rank )
112 {
113 case KCardDeck::Ace:
114 return QStringLiteral("ace");
115 case KCardDeck::Two:
116 return QStringLiteral("two");
117 case KCardDeck::Three:
118 return QStringLiteral("three");
119 case KCardDeck::Four:
120 return QStringLiteral("four");
121 case KCardDeck::Five:
122 return QStringLiteral("five");
123 case KCardDeck::Six:
124 return QStringLiteral("six");
125 case KCardDeck::Seven:
126 return QStringLiteral("seven");
127 case KCardDeck::Eight:
128 return QStringLiteral("eight");
129 case KCardDeck::Nine:
130 return QStringLiteral("nine");
131 case KCardDeck::Ten:
132 return QStringLiteral("ten");
133 case KCardDeck::Jack:
134 return QStringLiteral("jack");
135 case KCardDeck::Queen:
136 return QStringLiteral("queen");
137 case KCardDeck::King:
138 return QStringLiteral("king");
139 default:
140 return QString();
141 }
142 }
143 }
144
145
146 class SolverThread : public QThread
147 {
148 Q_OBJECT
149
150 public:
SolverThread(SolverInterface * solver)151 SolverThread( SolverInterface * solver )
152 : m_solver( solver )
153 {
154 }
155
run()156 void run() override
157 {
158 SolverInterface::ExitStatus result = m_solver->patsolve();
159 Q_EMIT finished( result );
160 }
161
abort()162 void abort()
163 {
164 m_solver->stopExecution();
165 wait();
166 }
167
168 Q_SIGNALS:
169 void finished( int result );
170
171 private:
172 SolverInterface * m_solver;
173 };
174
175
moveCount() const176 int DealerScene::moveCount() const
177 {
178 return m_loadedMoveCount + m_undoStack.size();
179 }
180
181
saveLegacyFile(QIODevice * io)182 void DealerScene::saveLegacyFile( QIODevice * io )
183 {
184 QXmlStreamWriter xml( io );
185 xml.setCodec( "UTF-8" );
186 xml.setAutoFormatting( true );
187 xml.setAutoFormattingIndent( -1 );
188 xml.writeStartDocument();
189
190 xml.writeDTD( QStringLiteral("<!DOCTYPE kpat>") );
191
192 xml.writeStartElement( QStringLiteral("dealer") );
193 xml.writeAttribute( QStringLiteral("id"), QString::number( gameId() ) );
194 xml.writeAttribute( QStringLiteral("options"), getGameOptions() );
195 xml.writeAttribute( QStringLiteral("number"), QString::number( gameNumber() ) );
196 xml.writeAttribute( QStringLiteral("moves"), QString::number( moveCount() ) );
197 xml.writeAttribute( QStringLiteral("started"), QString::number( m_dealStarted ) );
198 xml.writeAttribute( QStringLiteral("data"), getGameState() );
199
200 const auto patPiles = this->patPiles();
201 for (const PatPile * p : patPiles) {
202 xml.writeStartElement( QStringLiteral("pile") );
203 xml.writeAttribute( QStringLiteral("index"), QString::number( p->index() ) );
204 xml.writeAttribute( QStringLiteral("z"), QString::number( p->zValue() ) );
205
206 const auto cards = p->cards();
207 for (const KCard * c : cards) {
208 xml.writeStartElement( QStringLiteral("card") );
209 xml.writeAttribute( QStringLiteral("suit"), QString::number( c->suit() ) );
210 xml.writeAttribute( QStringLiteral("value"), QString::number( c->rank() ) );
211 xml.writeAttribute( QStringLiteral("faceup"), QString::number( c->isFaceUp() ) );
212 xml.writeAttribute( QStringLiteral("z"), QString::number( c->zValue() ) );
213 xml.writeEndElement();
214 }
215 xml.writeEndElement();
216 }
217 xml.writeEndElement();
218 xml.writeEndDocument();
219
220 m_dealWasJustSaved = true;
221 }
222
loadLegacyFile(QIODevice * io)223 bool DealerScene::loadLegacyFile( QIODevice * io )
224 {
225 resetInternals();
226
227 QXmlStreamReader xml( io );
228
229 xml.readNextStartElement();
230
231 // Before KDE4.3, KPat didn't store game specific options in the save
232 // file. This could cause crashes when loading a Spider game with a
233 // different number of suits than the current setting. Similarly, in
234 // Klondike the number of cards drawn from the deck was forgotten, but
235 // this never caused crashes. Fortunately, in Spider we can count the
236 // number of suits ourselves. For Klondike, there is no way to recover
237 // that information.
238 QString options = xml.attributes().value( QStringLiteral("options") ).toString();
239 if ( gameId() == 17 && options.isEmpty() )
240 {
241 QSet<int> suits;
242 while ( !xml.atEnd() )
243 if (xml.readNextStartElement() && xml.name() == QLatin1String("card"))
244 suits << readIntAttribute( xml, QStringLiteral("suit") );
245 options = QString::number( suits.size() );
246
247 // "Rewind" back to the <dealer> tag. Yes, this is ugly.
248 xml.clear();
249 io->reset();
250 xml.setDevice( io );
251 xml.readNextStartElement();
252 }
253 setGameOptions( options );
254
255 m_dealNumber = readIntAttribute( xml, QStringLiteral("number") );
256 m_loadedMoveCount = readIntAttribute( xml, QStringLiteral("moves") );
257 m_dealStarted = readIntAttribute( xml, QStringLiteral("started") );
258 QString gameStateData = xml.attributes().value( QStringLiteral("data") ).toString();
259
260 QMultiHash<quint32,KCard*> cards;
261 const auto deckCards = deck()->cards();
262 for (KCard * c : deckCards)
263 cards.insert( (c->id() & 0xffff), c );
264
265 QHash<int,PatPile*> piles;
266 const auto patPiles = this->patPiles();
267 for (PatPile * p : patPiles)
268 piles.insert( p->index(), p );
269
270 // Loop through <pile>s.
271 while ( xml.readNextStartElement() )
272 {
273 if (xml.name() != QLatin1String("pile")) {
274 qCWarning(KPAT_LOG) << "Expected a \"pile\" tag. Found a" << xml.name() << "tag.";
275 return false;
276 }
277
278 bool ok;
279 int index = readIntAttribute( xml, QStringLiteral("index"), &ok );
280 QHash<int,PatPile*>::const_iterator it = piles.constFind( index );
281 if ( !ok || it == piles.constEnd() )
282 {
283 qCWarning(KPAT_LOG) << "Unrecognized pile index:" << xml.attributes().value( QStringLiteral("index") );
284 return false;
285 }
286
287 PatPile * p = it.value();
288 p->clear();
289
290 // Loop through <card>s.
291 while ( xml.readNextStartElement() )
292 {
293 if (xml.name() != QLatin1String("card")) {
294 qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag.";
295 return false;
296 }
297
298 bool suitOk, rankOk, faceUpOk;
299 int suit = readIntAttribute( xml, QStringLiteral("suit"), &suitOk );
300 int rank = readIntAttribute( xml, QStringLiteral("value"), &rankOk );
301 bool faceUp = readIntAttribute( xml, QStringLiteral("faceup"), &faceUpOk );
302
303 quint32 id = KCardDeck::getId( KCardDeck::Suit( suit ), KCardDeck::Rank( rank ), 0 );
304 KCard * card = cards.take( id );
305
306 if ( !suitOk || !rankOk || !faceUpOk || !card )
307 {
308 qCWarning(KPAT_LOG) << "Unrecognized card: suit=" << xml.attributes().value(QStringLiteral("suit"))
309 << " value=" << xml.attributes().value(QStringLiteral("value"))
310 << " faceup=" << xml.attributes().value(QStringLiteral("faceup"));
311 return false;
312 }
313
314 card->setFaceUp( faceUp );
315 p->add( card );
316
317 xml.skipCurrentElement();
318 }
319 updatePileLayout( p, 0 );
320 }
321
322 setGameState( gameStateData );
323
324
325 Q_EMIT updateMoves( moveCount() );
326 takeState();
327
328 return true;
329 }
330
331
saveFile(QIODevice * io)332 void DealerScene::saveFile( QIODevice * io )
333 {
334 QXmlStreamWriter xml( io );
335 xml.setCodec( "UTF-8" );
336 xml.setAutoFormatting( true );
337 xml.setAutoFormattingIndent( -1 );
338 xml.writeStartDocument();
339
340 xml.writeStartElement( QStringLiteral("kpat-game") );
341 xml.writeAttribute( QStringLiteral("game-type"), m_di->baseIdString() );
342 if ( !getGameOptions().isEmpty() )
343 xml.writeAttribute( QStringLiteral("game-type-options"), getGameOptions() );
344 xml.writeAttribute( QStringLiteral("deal-number"), QString::number( gameNumber() ) );
345
346 if (!m_currentState) {
347 deck()->stopAnimations();
348 takeState();
349 }
350
351 QList<GameState*> allStates;
352 for ( int i = 0; i < m_undoStack.size(); ++i )
353 allStates << m_undoStack.at( i );
354 allStates << m_currentState;
355 for ( int i = m_redoStack.size() - 1; i >= 0; --i )
356 allStates << m_redoStack.at( i );
357
358 QString lastGameSpecificState;
359
360 for ( int i = 0; i < allStates.size(); ++i )
361 {
362 const GameState * state = allStates.at( i );
363 xml.writeStartElement( QStringLiteral("state") );
364 if ( state->stateData != lastGameSpecificState )
365 {
366 xml.writeAttribute( QStringLiteral("game-specific-state"), state->stateData );
367 lastGameSpecificState = state->stateData;
368 }
369 if ( i == m_undoStack.size() )
370 xml.writeAttribute( QStringLiteral("current"), QStringLiteral("true") );
371
372 for (const CardStateChange & change : qAsConst(state->changes)) {
373 xml.writeStartElement( QStringLiteral("move") );
374 xml.writeAttribute( QStringLiteral("pile"), change.newState.pile->objectName() );
375 xml.writeAttribute( QStringLiteral("position"), QString::number( change.newState.index ) );
376
377 bool faceChanged = !change.oldState.pile
378 || change.oldState.faceUp != change.newState.faceUp;
379
380 for (const KCard * card : change.cards) {
381 xml.writeStartElement( QStringLiteral("card") );
382 xml.writeAttribute( QStringLiteral("id"), QStringLiteral("%1").arg( card->id(), 7, 10, QLatin1Char('0') ) );
383 xml.writeAttribute( QStringLiteral("suit"), suitToString( card->suit() ) );
384 xml.writeAttribute( QStringLiteral("rank"), rankToString( card->rank() ) );
385 if ( faceChanged )
386 xml.writeAttribute( QStringLiteral("turn"), change.newState.faceUp ? QStringLiteral("face-up") : QStringLiteral("face-down") );
387 xml.writeEndElement();
388 }
389
390 xml.writeEndElement();
391 }
392 xml.writeEndElement();
393 }
394
395 xml.writeEndElement();
396 xml.writeEndDocument();
397
398 m_dealWasJustSaved = true;
399 }
400
401
loadFile(QIODevice * io)402 bool DealerScene::loadFile( QIODevice * io )
403 {
404 resetInternals();
405
406 bool reenableAutoDrop = autoDropEnabled();
407 setAutoDropEnabled( false );
408
409 QXmlStreamReader xml( io );
410
411 xml.readNextStartElement();
412
413 if (xml.name() != QLatin1String("kpat-game")) {
414 qCWarning(KPAT_LOG) << "First tag is not \"kpat-game\"";
415 return false;
416 }
417
418 m_dealNumber = readIntAttribute( xml, QStringLiteral("deal-number") );
419 setGameOptions( xml.attributes().value( QStringLiteral("game-type-options") ).toString() );
420
421 QMultiHash<quint32,KCard*> cardHash;
422 const auto cards = deck()->cards();
423 for (KCard * c : cards)
424 cardHash.insert( c->id(), c );
425
426 QHash<QString,KCardPile*> pileHash;
427 const auto piles = this->piles();
428 for (KCardPile * p : piles)
429 pileHash.insert( p->objectName(), p );
430
431 int undosToDo = -1;
432
433 while( xml.readNextStartElement() )
434 {
435 if (xml.name() != QLatin1String("state")) {
436 qCWarning(KPAT_LOG) << "Expected a \"state\" tag. Found a" << xml.name() << "tag.";
437 return false;
438 }
439
440 if ( xml.attributes().hasAttribute( QStringLiteral("game-specific-state") ) )
441 setGameState( xml.attributes().value( QStringLiteral("game-specific-state") ).toString() );
442
443 if ( undosToDo > -1 )
444 ++undosToDo;
445 else if ( xml.attributes().value( QStringLiteral("current") ) == QLatin1String("true") )
446 undosToDo = 0;
447
448 while( xml.readNextStartElement() )
449 {
450 if (xml.name() != QLatin1String("move")) {
451 qCWarning(KPAT_LOG) << "Expected a \"move\" tag. Found a" << xml.name() << "tag.";
452 return false;
453 }
454
455 QString pileName = xml.attributes().value( QStringLiteral("pile") ).toString();
456 KCardPile * pile = pileHash.value( pileName );
457
458 bool indexOk;
459 int index = readIntAttribute( xml, QStringLiteral("position"), &indexOk );
460
461 if ( !pile || !indexOk )
462 {
463 qCWarning(KPAT_LOG) << "Unrecognized pile or index.";
464 return false;
465 }
466
467 while ( xml.readNextStartElement() )
468 {
469 if (xml.name() != QLatin1String("card")) {
470 qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag.";
471 return false;
472 }
473
474 KCard * card = cardHash.value( readIntAttribute( xml, QStringLiteral("id") ) );
475 if ( !card )
476 {
477 qCWarning(KPAT_LOG) << "Unrecognized card.";
478 return false;
479 }
480
481 if ( xml.attributes().value(QStringLiteral("turn")) == QLatin1String("face-up") )
482 card->setFaceUp( true );
483 else if ( xml.attributes().value(QStringLiteral("turn")) == QLatin1String("face-down") )
484 card->setFaceUp( false );
485
486 pile->insert( index, card );
487
488 ++index;
489 xml.skipCurrentElement();
490 }
491 }
492 takeState();
493 }
494
495 m_loadedMoveCount = 0;
496 m_dealStarted = moveCount() > 0;
497 Q_EMIT updateMoves( moveCount() );
498
499 while ( undosToDo > 0 )
500 {
501 undo();
502 --undosToDo;
503 }
504
505 for (KCardPile * p : piles)
506 updatePileLayout( p, 0 );
507
508 setAutoDropEnabled( reenableAutoDrop );
509
510 return true;
511 }
512
513
DealerScene(const DealerInfo * di)514 DealerScene::DealerScene( const DealerInfo * di )
515 : m_di( di ),
516 m_solver( nullptr ),
517 m_solverThread( nullptr ),
518 m_peekedCard( nullptr ),
519 m_dealNumber( 0 ),
520 m_loadedMoveCount( 0 ),
521 m_neededFutureMoves( 1 ),
522 m_supportedActions( 0 ),
523 m_autoDropEnabled( false ),
524 m_solverEnabled( false ),
525 m_dealStarted( false ),
526 m_dealWasEverWinnable( false ),
527 m_dealHasBeenWon( false ),
528 m_dealWasJustSaved( false ),
529 m_statisticsRecorded( false ),
530 m_playerReceivedHelp( false ),
531 m_toldAboutWonGame( false ),
532 m_toldAboutLostGame( false ),
533 m_dropSpeedFactor( 1 ),
534 m_interruptAutoDrop( false ),
535 m_dealInProgress( false ),
536 m_hintInProgress( false ),
537 m_demoInProgress( false ),
538 m_dropInProgress( false ),
539 m_hintQueued( false ),
540 m_demoQueued( false ),
541 m_dropQueued( false ),
542 m_newCardsQueued( false ),
543 m_takeStateQueued( false ),
544 m_currentState( nullptr )
545 {
546 setItemIndexMethod(QGraphicsScene::NoIndex);
547
548 m_solverUpdateTimer.setInterval( 250 );
549 m_solverUpdateTimer.setSingleShot( true );
550 connect(&m_solverUpdateTimer, &QTimer::timeout, this, &DealerScene::stopAndRestartSolver);
551
552 m_demoTimer.setSingleShot( true );
553 connect(&m_demoTimer, &QTimer::timeout, this, &DealerScene::demo);
554
555 m_dropTimer.setSingleShot( true );
556 connect(&m_dropTimer, &QTimer::timeout, this, &DealerScene::drop);
557
558 m_wonItem = new MessageBox();
559 m_wonItem->setZValue( 2000 );
560 m_wonItem->hide();
561 addItem( m_wonItem );
562
563 connect(this, &DealerScene::cardAnimationDone, this, &DealerScene::animationDone);
564
565 connect(this, &DealerScene::cardDoubleClicked, this, &DealerScene::tryAutomaticMove);
566 // Make rightClick == doubleClick. See bug #151921
567 connect(this, &DealerScene::cardRightClicked, this, &DealerScene::tryAutomaticMove);
568 }
569
~DealerScene()570 DealerScene::~DealerScene()
571 {
572 stop();
573
574 disconnect();
575 if ( m_solverThread )
576 m_solverThread->abort();
577 delete m_solverThread;
578 m_solverThread = nullptr;
579 delete m_solver;
580 m_solver = nullptr;
581 qDeleteAll( m_undoStack );
582 delete m_currentState;
583 qDeleteAll( m_redoStack );
584 delete m_wonItem;
585 }
586
587
addPatPile(PatPile * pile)588 void DealerScene::addPatPile( PatPile * pile )
589 {
590 if ( !m_patPiles.contains( pile ) )
591 m_patPiles.append( pile );
592 }
593
594
removePatPile(PatPile * pile)595 void DealerScene::removePatPile( PatPile * pile )
596 {
597 m_patPiles.removeAll( pile );
598 }
599
600
clearPatPiles()601 void DealerScene::clearPatPiles()
602 {
603 const auto patPiles = this->patPiles();
604 for (PatPile * p : patPiles) {
605 removePatPile( p );
606 removePile( p );
607 }
608 }
609
610
patPiles() const611 QList<PatPile*> DealerScene::patPiles() const
612 {
613 return m_patPiles;
614 }
615
616
cardsMoved(const QList<KCard * > & cards,KCardPile * oldPile,KCardPile * newPile)617 void DealerScene::cardsMoved( const QList<KCard*> & cards, KCardPile * oldPile, KCardPile * newPile )
618 {
619 PatPile * newPatPile = dynamic_cast<PatPile*>( newPile );
620 PatPile * oldPatPile = dynamic_cast<PatPile*>( oldPile );
621
622 if ( oldPatPile && oldPatPile->isFoundation() && newPatPile && !newPatPile->isFoundation() )
623 {
624 for (KCard * c : cards)
625 m_cardsRemovedFromFoundations.insert( c );
626 }
627 else
628 {
629 for (KCard * c : cards)
630 m_cardsRemovedFromFoundations.remove( c );
631 }
632
633 if ( !m_dropInProgress && !m_dealInProgress )
634 {
635 m_dealStarted = true;
636 takeState();
637 }
638 }
639
640
startHint()641 void DealerScene::startHint()
642 {
643 stopDemo();
644 stopDrop();
645
646 if ( isCardAnimationRunning() )
647 {
648 m_hintQueued = true;
649 return;
650 }
651
652 if ( isKeyboardModeActive() )
653 setKeyboardModeActive( false );
654
655 QList<QGraphicsItem*> toHighlight;
656 const auto moveHints = getHints();
657 for (const MoveHint & h : moveHints)
658 toHighlight << h.card();
659
660 if ( !m_winningMoves.isEmpty() )
661 {
662 MOVE m = m_winningMoves.first();
663 MoveHint mh = solver()->translateMove( m );
664 if ( mh.isValid() )
665 toHighlight << mh.card();
666 }
667
668 m_hintInProgress = !toHighlight.isEmpty();
669 setHighlightedItems( toHighlight );
670 Q_EMIT hintActive( m_hintInProgress );
671 }
672
673
stopHint()674 void DealerScene::stopHint()
675 {
676 if ( m_hintInProgress )
677 {
678 m_hintInProgress = false;
679 clearHighlightedItems();
680 Q_EMIT hintActive( false );
681 }
682 }
683
684
isHintActive() const685 bool DealerScene::isHintActive() const
686 {
687 return m_hintInProgress;
688 }
689
getSolverHints()690 QList<MoveHint> DealerScene::getSolverHints()
691 {
692 QList<MoveHint> hintList;
693
694 if ( m_solverThread && m_solverThread->isRunning() )
695 m_solverThread->abort();
696
697 solver()->translate_layout();
698 solver()->patsolve( 1 );
699
700 const auto moves = solver()->firstMoves();
701 for (const MOVE & m : moves) {
702 MoveHint mh = solver()->translateMove( m );
703 hintList << mh;
704 }
705 return hintList;
706 }
707
getHints()708 QList<MoveHint> DealerScene::getHints()
709 {
710 if ( solver() )
711 return getSolverHints();
712
713 QList<MoveHint> hintList;
714 const auto patPiles = this->patPiles();
715 for (PatPile * store : patPiles) {
716 if (store->isFoundation() || store->isEmpty())
717 continue;
718
719 QList<KCard*> cards = store->cards();
720 while (cards.count() && !cards.first()->isFaceUp())
721 cards.erase(cards.begin());
722
723 QList<KCard*>::Iterator iti = cards.begin();
724 while (iti != cards.end())
725 {
726 if (allowedToRemove(store, (*iti)))
727 {
728 for (PatPile * dest : patPiles) {
729 int cardIndex = store->indexOf(*iti);
730 if (cardIndex == 0 && dest->isEmpty() && !dest->isFoundation())
731 continue;
732
733 if (!checkAdd(dest, dest->cards(), cards))
734 continue;
735
736 if (dest->isFoundation())
737 {
738 hintList << MoveHint( *iti, dest, 127 );
739 }
740 else
741 {
742 QList<KCard*> cardsBelow = cards.mid(0, cardIndex);
743
744 // if it could be here as well, then it's no use
745 if ((cardsBelow.isEmpty() && !dest->isEmpty()) || !checkAdd(store, cardsBelow, cards))
746 {
747 hintList << MoveHint( *iti, dest, 0 );
748 }
749 else if (checkPrefering(dest, dest->cards(), cards)
750 && !checkPrefering(store, cardsBelow, cards))
751 { // if checkPrefers says so, we add it nonetheless
752 hintList << MoveHint( *iti, dest, 10 );
753 }
754 }
755 }
756 }
757 cards.erase(iti);
758 iti = cards.begin();
759 }
760 }
761 return hintList;
762 }
763
prioSort(const MoveHint & c1,const MoveHint & c2)764 static bool prioSort(const MoveHint &c1, const MoveHint &c2)
765 {
766 return c1.priority() < c2.priority();
767 }
768
769
chooseHint()770 MoveHint DealerScene::chooseHint()
771 {
772 if ( !m_winningMoves.isEmpty() )
773 {
774 MOVE m = m_winningMoves.takeFirst();
775 MoveHint mh = solver()->translateMove( m );
776
777 #ifdef CHOOSE_HINT_DEBUG
778 if ( m.totype == O_Type )
779 fprintf( stderr, "move from %d out (at %d) Prio: %d\n", m.from,
780 m.turn_index, m.pri );
781 else
782 fprintf( stderr, "move from %d to %d (%d) Prio: %d\n", m.from, m.to,
783 m.turn_index, m.pri );
784 #endif
785
786 return mh;
787 }
788
789 QList<MoveHint> hintList = getHints();
790
791 if ( hintList.isEmpty() )
792 {
793 return MoveHint();
794 }
795 else
796 {
797 // Generate a random number with an exponentional distribution averaging 1/4.
798 qreal randomExp = qMin<qreal>( -log( 1 - QRandomGenerator::global()->generateDouble() ) / 4, 1 );
799 int randomIndex = randomExp * ( hintList.size() - 1 );
800
801 std::sort(hintList.begin(), hintList.end(), prioSort);
802 return hintList.at( randomIndex );
803 }
804 }
805
806
startNew(int dealNumber)807 void DealerScene::startNew( int dealNumber )
808 {
809 if ( dealNumber != -1 )
810 m_dealNumber = qBound( 1, dealNumber, INT_MAX );
811
812 // Only record the statistics and reset gameStarted if we're starting a
813 // new game number or we're restarting a game we've already won.
814 if ( dealNumber != -1 || m_dealHasBeenWon )
815 {
816 recordGameStatistics();
817 m_statisticsRecorded = false;
818 m_dealStarted = false;
819 }
820
821 if ( isCardAnimationRunning() )
822 {
823 QTimer::singleShot( 100, this, SLOT(startNew()) );
824 return;
825 }
826
827 if ( m_solverThread && m_solverThread->isRunning() )
828 m_solverThread->abort();
829
830 resetInternals();
831
832 Q_EMIT updateMoves( 0 );
833
834 const auto piles = this->piles();
835 for (KCardPile * p : piles)
836 p->clear();
837
838 m_dealInProgress = true;
839 restart( KpatShuffle::shuffled<KCard*>( deck()->cards(), m_dealNumber ) );
840 m_dealInProgress = false;
841
842 takeState();
843 update();
844 }
845
resetInternals()846 void DealerScene::resetInternals()
847 {
848 stop();
849
850 setKeyboardModeActive( false );
851
852 m_dealHasBeenWon = false;
853 m_wonItem->hide();
854
855 qDeleteAll( m_undoStack );
856 m_undoStack.clear();
857 delete m_currentState;
858 m_currentState = nullptr;
859 qDeleteAll( m_redoStack );
860 m_redoStack.clear();
861 m_lastKnownCardStates.clear();
862
863 m_dealWasJustSaved = false;
864 m_dealWasEverWinnable = false;
865 m_toldAboutLostGame = false;
866 m_toldAboutWonGame = false;
867 m_loadedMoveCount = 0;
868
869 m_playerReceivedHelp = false;
870
871 m_dealInProgress = false;
872
873 m_dropInProgress = false;
874 m_dropSpeedFactor = 1;
875 m_cardsRemovedFromFoundations.clear();
876
877 const auto cards = deck()->cards();
878 for (KCard * c : cards) {
879 c->disconnect( this );
880 c->stopAnimation();
881 }
882
883 Q_EMIT solverStateChanged( QString() );
884 Q_EMIT gameInProgress( true );
885 }
886
posAlongRect(qreal distOnRect,const QRectF & rect)887 QPointF posAlongRect( qreal distOnRect, const QRectF & rect )
888 {
889 if ( distOnRect < rect.width() )
890 return rect.topLeft() + QPointF( distOnRect, 0 );
891 distOnRect -= rect.width();
892 if ( distOnRect < rect.height() )
893 return rect.topRight() + QPointF( 0, distOnRect );
894 distOnRect -= rect.height();
895 if ( distOnRect < rect.width() )
896 return rect.bottomRight() + QPointF( -distOnRect, 0 );
897 distOnRect -= rect.width();
898 return rect.bottomLeft() + QPointF( 0, -distOnRect );
899 }
900
won()901 void DealerScene::won()
902 {
903 if ( m_dealHasBeenWon )
904 return;
905
906 m_dealHasBeenWon = true;
907 m_toldAboutWonGame = true;
908
909 stopDemo();
910 recordGameStatistics();
911
912 Q_EMIT solverStateChanged( QString() );
913
914 Q_EMIT newCardsPossible( false );
915 Q_EMIT undoPossible( false );
916 Q_EMIT redoPossible( false );
917 Q_EMIT gameInProgress( false );
918
919 setKeyboardModeActive( false );
920
921 qreal speed = sqrt( width() * width() + height() * height() ) / ( DURATION_WON );
922
923 QRectF justOffScreen = sceneRect().adjusted( -deck()->cardWidth(), -deck()->cardHeight(), 0, 0 );
924 qreal spacing = 2 * ( justOffScreen.width() + justOffScreen.height() ) / deck()->cards().size();
925 qreal distOnRect = 0;
926
927 const auto cards = deck()->cards();
928 for (KCard *c : cards) {
929 distOnRect += spacing;
930 QPointF pos2 = posAlongRect( distOnRect, justOffScreen );
931 QPointF delta = c->pos() - pos2;
932 qreal dist = sqrt( delta.x() * delta.x() + delta.y() * delta.y() );
933
934 c->setFaceUp( true );
935 c->animate( pos2, c->zValue(), 0, true, false, dist / speed );
936 }
937
938 connect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage);
939 }
940
showWonMessage()941 void DealerScene::showWonMessage()
942 {
943 disconnect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage);
944
945 // It shouldn't be necessary to stop the demo yet again here, but we
946 // get crashes if we don't. Will have to look into this further.
947 stopDemo();
948
949 // Hide all cards to prevent them from showing up accidentally if the
950 // window is resized.
951 const auto cards = deck()->cards();
952 for (KCard * c : cards)
953 c->hide();
954
955 updateWonItem();
956 m_wonItem->show();
957 }
958
updateWonItem()959 void DealerScene::updateWonItem()
960 {
961 const qreal aspectRatio = Renderer::self()->aspectRatioOfElement(QStringLiteral("message_frame"));
962 int boxWidth;
963 int boxHeight;
964 if ( width() < aspectRatio * height() )
965 {
966 boxWidth = width() * wonBoxToSceneSizeRatio;
967 boxHeight = boxWidth / aspectRatio;
968 }
969 else
970 {
971 boxHeight = height() * wonBoxToSceneSizeRatio;
972 boxWidth = boxHeight * aspectRatio;
973 }
974 m_wonItem->setSize( QSize( boxWidth, boxHeight ) );
975
976 if ( m_playerReceivedHelp )
977 m_wonItem->setMessage( i18n( "Congratulations! We have won." ) );
978 else
979 m_wonItem->setMessage( i18n( "Congratulations! You have won." ) );
980
981 m_wonItem->setPos( QPointF( (width() - boxWidth) / 2, (height() - boxHeight) / 2 )
982 + sceneRect().topLeft() );
983 }
984
985
allowedToAdd(const KCardPile * pile,const QList<KCard * > & cards) const986 bool DealerScene::allowedToAdd( const KCardPile * pile, const QList<KCard*> & cards ) const
987 {
988 if ( !pile->isEmpty() && !pile->topCard()->isFaceUp() )
989 return false;
990
991 const PatPile * p = dynamic_cast<const PatPile*>( pile );
992 return p && checkAdd( p, p->cards(), cards );
993 }
994
995
allowedToRemove(const KCardPile * pile,const KCard * card) const996 bool DealerScene::allowedToRemove( const KCardPile * pile, const KCard * card ) const
997 {
998 const PatPile * p = dynamic_cast<const PatPile*>( pile );
999 QList<KCard*> cards = pile->topCardsDownTo( card );
1000 return p
1001 && card->isFaceUp()
1002 && !cards.isEmpty()
1003 && checkRemove( p, cards );
1004 }
1005
1006
checkAdd(const PatPile * pile,const QList<KCard * > & oldCards,const QList<KCard * > & newCards) const1007 bool DealerScene::checkAdd( const PatPile * pile, const QList<KCard*> & oldCards, const QList<KCard*> & newCards ) const
1008 {
1009 Q_UNUSED( pile )
1010 Q_UNUSED( oldCards )
1011 Q_UNUSED( newCards )
1012 return false;
1013 }
1014
1015
checkRemove(const PatPile * pile,const QList<KCard * > & cards) const1016 bool DealerScene::checkRemove(const PatPile * pile, const QList<KCard*> & cards) const
1017 {
1018 Q_UNUSED( pile )
1019 Q_UNUSED( cards )
1020 return false;
1021 }
1022
1023
checkPrefering(const PatPile * pile,const QList<KCard * > & oldCards,const QList<KCard * > & newCards) const1024 bool DealerScene::checkPrefering( const PatPile * pile, const QList<KCard*> & oldCards, const QList<KCard*> & newCards ) const
1025 {
1026 Q_UNUSED( pile )
1027 Q_UNUSED( oldCards )
1028 Q_UNUSED( newCards )
1029 return false;
1030 }
1031
1032
mousePressEvent(QGraphicsSceneMouseEvent * e)1033 void DealerScene::mousePressEvent( QGraphicsSceneMouseEvent * e )
1034 {
1035 stop();
1036
1037 const QList<QGraphicsItem *> itemsAtPoint = items( e->scenePos() );
1038 KCard * card = qgraphicsitem_cast<KCard*>( itemsAtPoint.isEmpty() ? nullptr : itemsAtPoint.first() );
1039
1040 if ( m_peekedCard )
1041 {
1042 e->accept();
1043 }
1044 else if ( e->button() == Qt::RightButton
1045 && card
1046 && card->pile()
1047 && card != card->pile()->topCard()
1048 && cardsBeingDragged().isEmpty()
1049 && !isCardAnimationRunning() )
1050 {
1051 e->accept();
1052 m_peekedCard = card;
1053 QPointF pos2( card->x() + deck()->cardWidth() / 3.0, card->y() - deck()->cardHeight() / 3.0 );
1054 card->setZValue( card->zValue() + 0.1 );
1055 card->animate( pos2, card->zValue(), 20, card->isFaceUp(), false, DURATION_FANCYSHOW );
1056 }
1057 else
1058 {
1059 KCardScene::mousePressEvent( e );
1060 if ( !cardsBeingDragged().isEmpty() )
1061 Q_EMIT cardsPickedUp();
1062 }
1063 }
1064
1065
mouseReleaseEvent(QGraphicsSceneMouseEvent * e)1066 void DealerScene::mouseReleaseEvent( QGraphicsSceneMouseEvent * e )
1067 {
1068 stop();
1069
1070 if ( e->button() == Qt::RightButton && m_peekedCard && m_peekedCard->pile() )
1071 {
1072 e->accept();
1073 updatePileLayout( m_peekedCard->pile(), DURATION_FANCYSHOW );
1074 m_peekedCard = nullptr;
1075 }
1076 else
1077 {
1078 bool hadCards = !cardsBeingDragged().isEmpty();
1079 KCardScene::mouseReleaseEvent( e );
1080 if ( hadCards && cardsBeingDragged().isEmpty() )
1081 Q_EMIT cardsPutDown();
1082 }
1083 }
1084
1085
mouseDoubleClickEvent(QGraphicsSceneMouseEvent * e)1086 void DealerScene::mouseDoubleClickEvent( QGraphicsSceneMouseEvent * e )
1087 {
1088 if ( !m_dealHasBeenWon )
1089 {
1090 stop();
1091 KCardScene::mouseDoubleClickEvent( e );
1092 }
1093 else
1094 {
1095 Q_EMIT newDeal();
1096 }
1097 }
1098
1099
tryAutomaticMove(KCard * card)1100 bool DealerScene::tryAutomaticMove( KCard * card )
1101 {
1102 if ( !isCardAnimationRunning()
1103 && card
1104 && card->pile()
1105 && card == card->pile()->topCard()
1106 && card->isFaceUp()
1107 && allowedToRemove( card->pile(), card ) )
1108 {
1109 QList<KCard*> cardList = QList<KCard*>() << card;
1110
1111 const auto patPiles = this->patPiles();
1112 for (PatPile * p : patPiles) {
1113 if ( p->isFoundation() && allowedToAdd( p, cardList ) )
1114 {
1115 moveCardToPile( card , p, DURATION_MOVE );
1116 return true;
1117 }
1118 }
1119 }
1120
1121 return false;
1122 }
1123
1124
undo()1125 void DealerScene::undo()
1126 {
1127 undoOrRedo( true );
1128 }
1129
1130
redo()1131 void DealerScene::redo()
1132 {
1133 undoOrRedo( false );
1134 }
1135
1136
undoOrRedo(bool undo)1137 void DealerScene::undoOrRedo( bool undo )
1138 {
1139 stop();
1140
1141 if ( isCardAnimationRunning() )
1142 return;
1143
1144 // The undo and redo actions are almost identical, except for where states
1145 // are pulled from and pushed to, so to keep things generic, we use
1146 // direction dependent const references throughout this code.
1147 QStack<GameState*> & fromStack = undo ? m_undoStack : m_redoStack;
1148 QStack<GameState*> & toStack = undo ? m_redoStack : m_undoStack;
1149
1150 if ( !fromStack.isEmpty() && m_currentState )
1151 {
1152 // If we're undoing, we use the oldStates of the changes of the current
1153 // state. If we're redoing, we use the newStates of the changes of the
1154 // nextState.
1155 const QList<CardStateChange> & changes = undo ? m_currentState->changes
1156 : fromStack.top()->changes;
1157
1158 // Update the currentState pointer and undo/redo stacks.
1159 toStack.push( m_currentState );
1160 m_currentState = fromStack.pop();
1161 setGameState( m_currentState->stateData );
1162
1163 QSet<KCardPile*> pilesAffected;
1164 for (const CardStateChange & change : changes) {
1165 CardState sourceState = undo ? change.newState : change.oldState;
1166 CardState destState = undo ? change.oldState : change.newState;
1167
1168 PatPile * sourcePile = dynamic_cast<PatPile*>( sourceState.pile );
1169 PatPile * destPile = dynamic_cast<PatPile*>( destState.pile );
1170 bool notDroppable = destState.takenDown
1171 || ((sourcePile && sourcePile->isFoundation())
1172 && !(destPile && destPile->isFoundation()));
1173
1174 pilesAffected << sourceState.pile << destState.pile;
1175
1176 for (KCard * c : change.cards) {
1177 m_lastKnownCardStates.insert( c, destState );
1178
1179 c->setFaceUp( destState.faceUp );
1180 destState.pile->insert( destState.index, c );
1181
1182 if ( notDroppable )
1183 m_cardsRemovedFromFoundations.insert( c );
1184 else
1185 m_cardsRemovedFromFoundations.remove( c );
1186
1187 ++sourceState.index;
1188 ++destState.index;
1189 }
1190 }
1191
1192 // At this point all cards should be in the right piles, but not
1193 // necessarily at the right positions within those piles. So we
1194 // run through the piles involved and swap card positions until
1195 // everything is back in its place, then relayout the piles.
1196 for (KCardPile * p : qAsConst(pilesAffected)) {
1197 int i = 0;
1198 while ( i < p->count() )
1199 {
1200 int index = m_lastKnownCardStates.value( p->at( i ) ).index;
1201 if ( i == index )
1202 ++i;
1203 else
1204 p->swapCards( i, index );
1205 }
1206
1207 updatePileLayout( p, 0 );
1208 }
1209
1210 Q_EMIT updateMoves( moveCount() );
1211 Q_EMIT undoPossible( !m_undoStack.isEmpty() );
1212 Q_EMIT redoPossible( !m_redoStack.isEmpty() );
1213
1214 if ( m_toldAboutLostGame ) // everything's possible again
1215 {
1216 gameInProgress( true );
1217 m_toldAboutLostGame = false;
1218 m_toldAboutWonGame = false;
1219 }
1220
1221 int solvability = m_currentState->solvability;
1222 m_winningMoves = m_currentState->winningMoves;
1223
1224 Q_EMIT solverStateChanged( solverStatusMessage( solvability, m_dealWasEverWinnable ) );
1225
1226 if ( m_solver && ( solvability == SolverInterface::SearchAborted
1227 || solvability == SolverInterface::MemoryLimitReached ) )
1228 {
1229 startSolver();
1230 }
1231 }
1232 }
1233
1234
takeState()1235 void DealerScene::takeState()
1236 {
1237 if ( isCardAnimationRunning() )
1238 {
1239 m_takeStateQueued = true;
1240 return;
1241 }
1242
1243 if ( !isDemoActive() )
1244 m_winningMoves.clear();
1245
1246 QList<CardStateChange> changes;
1247
1248 const auto piles = this->piles();
1249 for (KCardPile * p : piles) {
1250 QList<KCard*> currentRun;
1251 CardState oldRunState;
1252 CardState newRunState;
1253
1254 for ( int i = 0; i < p->count(); ++i )
1255 {
1256 KCard * c = p->at( i );
1257
1258 const CardState & oldState = m_lastKnownCardStates.value( c );
1259 CardState newState( p, i, c->isFaceUp(), m_cardsRemovedFromFoundations.contains( c ) );
1260
1261 // The card has changed.
1262 if ( newState != oldState )
1263 {
1264 // There's a run in progress, but this card isn't part of it.
1265 if ( !currentRun.isEmpty()
1266 && (oldState.pile != oldRunState.pile
1267 || (oldState.index != -1 && oldState.index != oldRunState.index + currentRun.size())
1268 || oldState.faceUp != oldRunState.faceUp
1269 || newState.faceUp != newRunState.faceUp
1270 || oldState.takenDown != oldRunState.takenDown
1271 || newState.takenDown != newRunState.takenDown) )
1272 {
1273 changes << CardStateChange( oldRunState, newRunState, currentRun );
1274 currentRun.clear();
1275 }
1276
1277 // This card is the start of a new run.
1278 if ( currentRun.isEmpty() )
1279 {
1280 oldRunState = oldState;
1281 newRunState = newState;
1282 }
1283
1284 currentRun << c;
1285
1286 m_lastKnownCardStates.insert( c, newState );
1287 }
1288 }
1289 // Add the last run, if any.
1290 if ( !currentRun.isEmpty() )
1291 {
1292 changes << CardStateChange( oldRunState, newRunState, currentRun );
1293 }
1294 }
1295
1296 // If nothing has changed, we're done.
1297 if ( changes.isEmpty()
1298 && m_currentState
1299 && m_currentState->stateData == getGameState() )
1300 {
1301 return;
1302 }
1303
1304 if ( m_currentState )
1305 {
1306 m_undoStack.push( m_currentState );
1307 qDeleteAll( m_redoStack );
1308 m_redoStack.clear();
1309 }
1310 m_currentState = new GameState( changes, getGameState() );
1311
1312 Q_EMIT redoPossible( false );
1313 Q_EMIT undoPossible( !m_undoStack.isEmpty() );
1314 Q_EMIT updateMoves( moveCount() );
1315
1316 m_dealWasJustSaved = false;
1317 if ( isGameWon() )
1318 {
1319 won();
1320 return;
1321 }
1322
1323 if ( !m_toldAboutWonGame && !m_toldAboutLostGame && isGameLost() )
1324 {
1325 Q_EMIT gameInProgress( false );
1326 Q_EMIT solverStateChanged( i18n( "Solver: This game is lost." ) );
1327 m_toldAboutLostGame = true;
1328 stopDemo();
1329 return;
1330 }
1331
1332 if ( !isDemoActive() && !isCardAnimationRunning() && m_solver )
1333 startSolver();
1334
1335 if ( autoDropEnabled() && !isDropActive() && !isDemoActive() && m_redoStack.isEmpty() )
1336 {
1337 if ( m_interruptAutoDrop )
1338 m_interruptAutoDrop = false;
1339 else
1340 m_dropQueued = true;
1341 }
1342 }
1343
1344
setSolverEnabled(bool a)1345 void DealerScene::setSolverEnabled(bool a)
1346 {
1347 m_solverEnabled = a;
1348 }
1349
1350
setAutoDropEnabled(bool enabled)1351 void DealerScene::setAutoDropEnabled( bool enabled )
1352 {
1353 m_autoDropEnabled = enabled;
1354 }
1355
1356
autoDropEnabled() const1357 bool DealerScene::autoDropEnabled() const
1358 {
1359 return m_autoDropEnabled;
1360 }
1361
1362
startDrop()1363 void DealerScene::startDrop()
1364 {
1365 stopHint();
1366 stopDemo();
1367
1368 if ( isCardAnimationRunning() )
1369 {
1370 m_dropQueued = true;
1371 return;
1372 }
1373
1374 m_dropInProgress = true;
1375 m_interruptAutoDrop = false;
1376 m_dropSpeedFactor = 1;
1377 Q_EMIT dropActive( true );
1378
1379 drop();
1380 }
1381
1382
stopDrop()1383 void DealerScene::stopDrop()
1384 {
1385 if ( m_dropInProgress )
1386 {
1387 m_dropTimer.stop();
1388 m_dropInProgress = false;
1389 Q_EMIT dropActive( false );
1390
1391 if ( autoDropEnabled() && m_takeStateQueued )
1392 m_interruptAutoDrop = true;
1393 }
1394 }
1395
1396
isDropActive() const1397 bool DealerScene::isDropActive() const
1398 {
1399 return m_dropInProgress;
1400 }
1401
1402
drop()1403 bool DealerScene::drop()
1404 {
1405 const auto moveHints = getHints();
1406 for (const MoveHint & mh : moveHints) {
1407 if ( mh.pile()
1408 && mh.pile()->isFoundation()
1409 && mh.priority() > 120
1410 && !m_cardsRemovedFromFoundations.contains( mh.card() ) )
1411 {
1412 QList<KCard*> cards = mh.card()->pile()->topCardsDownTo( mh.card() );
1413
1414 QMap<KCard*,QPointF> oldPositions;
1415 for (KCard * c : qAsConst(cards))
1416 oldPositions.insert( c, c->pos() );
1417
1418 moveCardsToPile( cards, mh.pile(), DURATION_MOVE );
1419
1420 int count = 0;
1421 for (KCard * c : qAsConst(cards)) {
1422 c->completeAnimation();
1423 QPointF destPos = c->pos();
1424 c->setPos( oldPositions.value( c ) );
1425
1426 int duration = speedUpTime( DURATION_AUTODROP + count * DURATION_AUTODROP / 10 );
1427 c->animate( destPos, c->zValue(), 0, c->isFaceUp(), true, duration );
1428
1429 ++count;
1430 }
1431
1432 m_dropSpeedFactor *= AUTODROP_SPEEDUP_FACTOR;
1433
1434 takeState();
1435
1436 return true;
1437 }
1438 }
1439
1440 m_dropInProgress = false;
1441 Q_EMIT dropActive( false );
1442
1443 return false;
1444 }
1445
speedUpTime(int delay) const1446 int DealerScene::speedUpTime( int delay ) const
1447 {
1448 if ( delay < DURATION_AUTODROP_MINIMUM )
1449 return delay;
1450 else
1451 return qMax<int>( delay * m_dropSpeedFactor, DURATION_AUTODROP_MINIMUM );
1452 }
1453
stopAndRestartSolver()1454 void DealerScene::stopAndRestartSolver()
1455 {
1456 if ( m_toldAboutLostGame || m_toldAboutWonGame ) // who cares?
1457 return;
1458
1459 if ( m_solverThread && m_solverThread->isRunning() )
1460 {
1461 m_solverThread->abort();
1462 }
1463
1464 if ( isCardAnimationRunning() )
1465 {
1466 startSolver();
1467 return;
1468 }
1469
1470 slotSolverEnded();
1471 }
1472
slotSolverEnded()1473 void DealerScene::slotSolverEnded()
1474 {
1475 if ( m_solverThread && m_solverThread->isRunning() )
1476 return;
1477
1478 m_solver->translate_layout();
1479 m_winningMoves.clear();
1480 Q_EMIT solverStateChanged( i18n("Solver: Calculating...") );
1481 if ( !m_solverThread )
1482 {
1483 m_solverThread = new SolverThread( m_solver );
1484 connect(m_solverThread, &SolverThread::finished, this, &DealerScene::slotSolverFinished);
1485 }
1486 m_solverThread->start( m_solverEnabled ? QThread::IdlePriority : QThread::NormalPriority );
1487 }
1488
1489
slotSolverFinished(int result)1490 void DealerScene::slotSolverFinished( int result )
1491 {
1492 if ( result == SolverInterface::SolutionExists )
1493 {
1494 m_winningMoves = m_solver->winMoves();
1495 m_dealWasEverWinnable = true;
1496 }
1497
1498 Q_EMIT solverStateChanged( solverStatusMessage( result, m_dealWasEverWinnable ) );
1499
1500 if ( m_currentState )
1501 {
1502 m_currentState->solvability = static_cast<SolverInterface::ExitStatus>( result );
1503 m_currentState->winningMoves = m_winningMoves;
1504 }
1505
1506 if ( result == SolverInterface::SearchAborted )
1507 startSolver();
1508 }
1509
1510
gameNumber() const1511 int DealerScene::gameNumber() const
1512 {
1513 return m_dealNumber;
1514 }
1515
1516
stop()1517 void DealerScene::stop()
1518 {
1519 stopHint();
1520 stopDemo();
1521 stopDrop();
1522 }
1523
1524
animationDone()1525 void DealerScene::animationDone()
1526 {
1527 Q_ASSERT( !isCardAnimationRunning() );
1528
1529 if ( !m_multiStepMoves.isEmpty() )
1530 {
1531 continueMultiStepMove();
1532 return;
1533 }
1534
1535 if ( m_takeStateQueued )
1536 {
1537 m_takeStateQueued = false;
1538 takeState();
1539 }
1540
1541 if ( m_demoInProgress )
1542 {
1543 m_demoTimer.start( TIME_BETWEEN_MOVES );
1544 }
1545 else if ( m_dropInProgress )
1546 {
1547 m_dropTimer.start( speedUpTime( TIME_BETWEEN_MOVES ) );
1548 }
1549 else if ( m_newCardsQueued )
1550 {
1551 m_newCardsQueued = false;
1552 newCards();
1553 }
1554 else if ( m_hintQueued )
1555 {
1556 m_hintQueued = false;
1557 startHint();
1558 }
1559 else if ( m_demoQueued )
1560 {
1561 m_demoQueued = false;
1562 startDemo();
1563 }
1564 else if ( m_dropQueued )
1565 {
1566 m_dropQueued = false;
1567 startDrop();
1568 }
1569 }
1570
1571
startDemo()1572 void DealerScene::startDemo()
1573 {
1574 stopHint();
1575 stopDrop();
1576
1577 if ( isCardAnimationRunning() )
1578 {
1579 m_demoQueued = true;
1580 return;
1581 }
1582
1583 m_demoInProgress = true;
1584 m_playerReceivedHelp = true;
1585 m_dealStarted = true;
1586
1587 demo();
1588 }
1589
1590
stopDemo()1591 void DealerScene::stopDemo()
1592 {
1593 if ( m_demoInProgress )
1594 {
1595 m_demoTimer.stop();
1596 m_demoInProgress = false;
1597 Q_EMIT demoActive( false );
1598 }
1599 }
1600
1601
isDemoActive() const1602 bool DealerScene::isDemoActive() const
1603 {
1604 return m_demoInProgress;
1605 }
1606
1607
demo()1608 void DealerScene::demo()
1609 {
1610 if ( isCardAnimationRunning() )
1611 {
1612 m_demoQueued = true;
1613 return;
1614 }
1615
1616 m_demoInProgress = true;
1617 m_playerReceivedHelp = true;
1618 m_dealStarted = true;
1619 clearHighlightedItems();
1620
1621 m_demoTimer.stop();
1622
1623 MoveHint mh = chooseHint();
1624 if ( mh.isValid() )
1625 {
1626 KCard * card = mh.card();
1627 Q_ASSERT( card );
1628 KCardPile * sourcePile = mh.card()->pile();
1629 Q_ASSERT( sourcePile );
1630 Q_ASSERT( allowedToRemove( sourcePile, card ) );
1631 PatPile * destPile = mh.pile();
1632 Q_ASSERT( destPile );
1633 Q_ASSERT( sourcePile != destPile );
1634 QList<KCard*> cards = sourcePile->topCardsDownTo( card );
1635 Q_ASSERT( allowedToAdd( destPile, cards ) );
1636
1637 if ( destPile->isEmpty() )
1638 {
1639 qCDebug(KPAT_LOG) << "Moving" << card->objectName()
1640 << "from the" << sourcePile->objectName()
1641 << "pile to the" << destPile->objectName()
1642 << "pile, which is empty";
1643 }
1644 else
1645 {
1646 qCDebug(KPAT_LOG) << "Moving" << card->objectName()
1647 << "from the" << sourcePile->objectName()
1648 << "pile to the" << destPile->objectName()
1649 << "pile, putting it on top of"
1650 << destPile->topCard()->objectName();
1651 }
1652
1653 moveCardsToPile( cards, destPile, DURATION_DEMO );
1654 }
1655 else if ( !newCards() )
1656 {
1657 if (isGameWon())
1658 {
1659 won();
1660 }
1661 else
1662 {
1663 stopDemo();
1664 slotSolverEnded();
1665 }
1666 return;
1667 }
1668
1669 Q_EMIT demoActive( true );
1670 takeState();
1671 }
1672
1673
drawDealRowOrRedeal()1674 void DealerScene::drawDealRowOrRedeal()
1675 {
1676 stop();
1677
1678 if ( isCardAnimationRunning() )
1679 {
1680 m_newCardsQueued = true;
1681 return;
1682 }
1683
1684 m_newCardsQueued = false;
1685 newCards();
1686 }
1687
1688
newCards()1689 bool DealerScene::newCards()
1690 {
1691 return false;
1692 }
1693
1694
setSolver(SolverInterface * s)1695 void DealerScene::setSolver( SolverInterface *s) {
1696 delete m_solver;
1697 delete m_solverThread;
1698 m_solver = s;
1699 m_solverThread = nullptr;
1700 }
1701
isGameWon() const1702 bool DealerScene::isGameWon() const
1703 {
1704 const auto patPiles = this->patPiles();
1705 for (PatPile *p : patPiles) {
1706 if (!p->isFoundation() && !p->isEmpty())
1707 return false;
1708 }
1709 return true;
1710 }
1711
startSolver()1712 void DealerScene::startSolver()
1713 {
1714 if( m_solverEnabled )
1715 m_solverUpdateTimer.start();
1716 }
1717
1718
isGameLost() const1719 bool DealerScene::isGameLost() const
1720 {
1721 if (! m_winningMoves.isEmpty())
1722 {
1723 return false;
1724 }
1725 if ( solver() )
1726 {
1727 if ( m_solverThread && m_solverThread->isRunning() )
1728 m_solverThread->abort();
1729
1730 solver()->translate_layout();
1731 return solver()->patsolve( neededFutureMoves() ) == SolverInterface::NoSolutionExists;
1732 }
1733 return false;
1734 }
1735
recordGameStatistics()1736 void DealerScene::recordGameStatistics()
1737 {
1738 // Don't record the game if it was never started, if it is unchanged since
1739 // it was last saved (allowing the user to close KPat after saving without
1740 // it recording a loss) or if it has already been recorded.// takeState(); // copying it again
1741 if ( m_dealStarted && !m_dealWasJustSaved && !m_statisticsRecorded )
1742 {
1743 int id = oldId();
1744
1745 QString totalPlayedKey = QStringLiteral("total%1").arg( id );
1746 QString wonKey = QStringLiteral("won%1").arg( id );
1747 QString winStreakKey = QStringLiteral("winstreak%1").arg( id );
1748 QString maxWinStreakKey = QStringLiteral("maxwinstreak%1").arg( id );
1749 QString loseStreakKey = QStringLiteral("loosestreak%1").arg( id );
1750 QString maxLoseStreakKey = QStringLiteral("maxloosestreak%1").arg( id );
1751 QString minMovesKey = QStringLiteral("minmoves%1").arg( id );
1752
1753 KConfigGroup config(KSharedConfig::openConfig(), scores_group);
1754
1755 int totalPlayed = config.readEntry( totalPlayedKey, 0 );
1756 int won = config.readEntry( wonKey, 0 );
1757 int winStreak = config.readEntry( winStreakKey, 0 );
1758 int maxWinStreak = config.readEntry( maxWinStreakKey, 0 );
1759 int loseStreak = config.readEntry( loseStreakKey, 0 );
1760 int maxLoseStreak = config.readEntry( maxLoseStreakKey, 0 );
1761 int minMoves = config.readEntry( minMovesKey, -1 );
1762
1763 ++totalPlayed;
1764
1765 if ( m_dealHasBeenWon )
1766 {
1767 ++won;
1768 ++winStreak;
1769 maxWinStreak = qMax( winStreak, maxWinStreak );
1770 loseStreak = 0;
1771 if ( minMoves < 0 )
1772 minMoves = moveCount();
1773 else
1774 minMoves = qMin( minMoves, moveCount() );
1775 }
1776 else
1777 {
1778 ++loseStreak;
1779 maxLoseStreak = qMax( loseStreak, maxLoseStreak );
1780 winStreak = 0;
1781 }
1782
1783 config.writeEntry( totalPlayedKey, totalPlayed );
1784 config.writeEntry( wonKey, won );
1785 config.writeEntry( winStreakKey, winStreak );
1786 config.writeEntry( maxWinStreakKey, maxWinStreak );
1787 config.writeEntry( loseStreakKey, loseStreak );
1788 config.writeEntry( maxLoseStreakKey, maxLoseStreak );
1789 config.writeEntry( minMovesKey, minMoves );
1790
1791 m_statisticsRecorded = true;
1792 }
1793 }
1794
relayoutScene()1795 void DealerScene::relayoutScene()
1796 {
1797 KCardScene::relayoutScene();
1798
1799 if ( m_wonItem->isVisible() )
1800 updateWonItem();
1801 }
1802
1803
gameId() const1804 int DealerScene::gameId() const
1805 {
1806 return m_di->baseId();
1807 }
1808
1809
setActions(int actions)1810 void DealerScene::setActions( int actions )
1811 {
1812 m_supportedActions = actions;
1813 }
1814
1815
actions() const1816 int DealerScene::actions() const
1817 {
1818 return m_supportedActions;
1819 }
1820
1821
configActions() const1822 QList<QAction*> DealerScene::configActions() const
1823 {
1824 return QList<QAction*>();
1825 }
1826
1827
solver() const1828 SolverInterface * DealerScene::solver() const
1829 {
1830 return m_solver;
1831 }
1832
1833
neededFutureMoves() const1834 int DealerScene::neededFutureMoves() const
1835 {
1836 return m_neededFutureMoves;
1837 }
1838
1839
setNeededFutureMoves(int i)1840 void DealerScene::setNeededFutureMoves( int i )
1841 {
1842 m_neededFutureMoves = i;
1843 }
1844
1845
setDeckContents(int copies,const QList<KCardDeck::Suit> & suits)1846 void DealerScene::setDeckContents( int copies, const QList<KCardDeck::Suit> & suits )
1847 {
1848 Q_ASSERT( copies >= 1 );
1849 Q_ASSERT( !suits.isEmpty() );
1850
1851 // Note that the order in which the cards are created can not be changed
1852 // without breaking the game numbering. For historical reasons, KPat
1853 // generates card by rank and then by suit, rather than the more common
1854 // suit then rank ordering.
1855 QList<quint32> ids;
1856 unsigned int number = 0;
1857 for (int i = 0; i < copies; ++i) {
1858 const auto ranks = KCardDeck::standardRanks();
1859 for (const KCardDeck::Rank & r : ranks)
1860 for (const KCardDeck::Suit & s : suits)
1861 ids << KCardDeck::getId( s, r, number++ );
1862 }
1863 deck()->setDeckContents( ids );
1864 }
1865
1866
createDump() const1867 QImage DealerScene::createDump() const
1868 {
1869 const QSize previewSize( 480, 320 );
1870
1871 const auto cards = deck()->cards();
1872 for (KCard * c : cards)
1873 c->completeAnimation();
1874
1875 QMultiMap<qreal,QGraphicsItem*> itemsByZ;
1876 const auto items = this->items();
1877 for (QGraphicsItem * item : items) {
1878 Q_ASSERT( item->zValue() >= 0 );
1879 itemsByZ.insert( item->zValue(), item );
1880 }
1881
1882 QImage img( contentArea().size().toSize(), QImage::Format_ARGB32 );
1883 img.fill( Qt::transparent );
1884 QPainter p( &img );
1885
1886 for (QGraphicsItem * item : qAsConst(itemsByZ)) {
1887 if ( item->isVisible() )
1888 {
1889 p.save();
1890 p.setTransform( item->deviceTransform( p.worldTransform() ), false );
1891 item->paint( &p, nullptr );
1892 p.restore();
1893 }
1894 }
1895
1896 p.end();
1897
1898 img = img.scaled( previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation );
1899
1900 QImage img2( previewSize, QImage::Format_ARGB32 );
1901 img2.fill( Qt::transparent );
1902 QPainter p2( &img2 );
1903 p2.drawImage( (img2.width() - img.width()) / 2, (img2.height() - img.height()) / 2, img );
1904 p2.end();
1905
1906 return img2;
1907 }
1908
1909
mapOldId(int id)1910 void DealerScene::mapOldId( int id )
1911 {
1912 Q_UNUSED( id );
1913 }
1914
1915
oldId() const1916 int DealerScene::oldId() const
1917 {
1918 return gameId();
1919 }
1920
1921
getGameState() const1922 QString DealerScene::getGameState() const
1923 {
1924 return QString();
1925 }
1926
1927
setGameState(const QString & state)1928 void DealerScene::setGameState( const QString & state )
1929 {
1930 Q_UNUSED( state );
1931 }
1932
1933
getGameOptions() const1934 QString DealerScene::getGameOptions() const
1935 {
1936 return QString();
1937 }
1938
1939
setGameOptions(const QString & options)1940 void DealerScene::setGameOptions( const QString & options )
1941 {
1942 Q_UNUSED( options );
1943 }
1944
1945
allowedToStartNewGame()1946 bool DealerScene::allowedToStartNewGame()
1947 {
1948 // Check if the user is already running a game, and if she is,
1949 // then ask if she wants to abort it.
1950 return !m_dealStarted
1951 || m_dealWasJustSaved
1952 || m_toldAboutWonGame
1953 || m_toldAboutLostGame
1954 || KMessageBox::warningContinueCancel(nullptr,
1955 i18n("A new game has been requested, but there is already a game in progress.\n\n"
1956 "A loss will be recorded in the statistics if the current game is abandoned."),
1957 i18n("Abandon Current Game?"),
1958 KGuiItem(i18n("Abandon Current Game")),
1959 KStandardGuiItem::cancel(),
1960 QStringLiteral("careaboutstats")
1961 ) == KMessageBox::Continue;
1962 }
1963
addCardForDeal(KCardPile * pile,KCard * card,bool faceUp,QPointF startPos)1964 void DealerScene::addCardForDeal( KCardPile * pile, KCard * card, bool faceUp, QPointF startPos )
1965 {
1966 Q_ASSERT( card );
1967 Q_ASSERT( pile );
1968
1969 card->setFaceUp( faceUp );
1970 pile->add( card );
1971 m_initDealPositions.insert( card, startPos );
1972 }
1973
1974
startDealAnimation()1975 void DealerScene::startDealAnimation()
1976 {
1977 qreal speed = sqrt( width() * width() + height() * height() ) / ( DURATION_DEAL );
1978 const auto patPiles = this->patPiles();
1979 for (PatPile * p : patPiles) {
1980 updatePileLayout( p, 0 );
1981 const auto cards = p->cards();
1982 for (KCard * c : cards) {
1983 if ( !m_initDealPositions.contains( c ) )
1984 continue;
1985
1986 QPointF pos2 = c->pos();
1987 c->setPos( m_initDealPositions.value( c ) );
1988
1989 QPointF delta = c->pos() - pos2;
1990 qreal dist = sqrt( delta.x() * delta.x() + delta.y() * delta.y() );
1991 int duration = qRound( dist / speed );
1992 c->animate( pos2, c->zValue(), 0, c->isFaceUp(), false, duration );
1993 }
1994 }
1995 m_initDealPositions.clear();
1996 }
1997
1998
multiStepMove(const QList<KCard * > & cards,KCardPile * pile,const QList<KCardPile * > & freePiles,const QList<KCardPile * > & freeCells,int duration)1999 void DealerScene::multiStepMove( const QList<KCard*> & cards,
2000 KCardPile * pile,
2001 const QList<KCardPile*> & freePiles,
2002 const QList<KCardPile*> & freeCells,
2003 int duration )
2004 {
2005 Q_ASSERT( cards.size() == 1 || !freePiles.isEmpty() || !freeCells.isEmpty() );
2006
2007 m_multiStepMoves.clear();
2008 m_multiStepDuration = duration;
2009
2010 multiStepSubMove( cards, pile, freePiles, freeCells );
2011 continueMultiStepMove();
2012 }
2013
2014
multiStepSubMove(QList<KCard * > cards,KCardPile * pile,QList<KCardPile * > freePiles,const QList<KCardPile * > & freeCells)2015 void DealerScene::multiStepSubMove( QList<KCard*> cards,
2016 KCardPile * pile,
2017 QList<KCardPile*> freePiles,
2018 const QList<KCardPile*> & freeCells )
2019 {
2020 // Note that cards and freePiles are passed by value, as we need to make a
2021 // local copy anyway.
2022
2023 // Using n free cells, we can move a run of n+1 cards. If we want to move
2024 // more than that, we have to recursively move some of our cards to one of
2025 // the free piles temporarily.
2026 const int freeCellsPlusOne = freeCells.size() + 1;
2027 int cardsToSubMove = cards.size() - freeCellsPlusOne;
2028
2029 QList<QPair<KCardPile*,QList<KCard*> > > tempMoves;
2030 while ( cardsToSubMove > 0 )
2031 {
2032 int tempMoveSize;
2033 if ( cardsToSubMove <= freePiles.size() * freeCellsPlusOne )
2034 {
2035 // If the cards that have to be submoved can be spread across the
2036 // the free piles without putting more than freeCellsPlusOne cards
2037 // on each one, we do so. This means that none of our submoves will
2038 // need further submoves, which keeps the total move count down. We
2039 // Just to a simple rounding up integer division.
2040 tempMoveSize = (cardsToSubMove + freePiles.size() - 1) / freePiles.size();
2041 }
2042 else
2043 {
2044 // Otherwise, we use the space optimal method that gets the cards
2045 // moved using a minimal number of piles, but uses more submoves.
2046 tempMoveSize = freeCellsPlusOne;
2047 while ( tempMoveSize * 2 < cardsToSubMove )
2048 tempMoveSize *= 2;
2049 }
2050
2051 QList<KCard*> subCards;
2052 for ( int i = 0; i < tempMoveSize; ++i )
2053 subCards.prepend( cards.takeLast() );
2054
2055 Q_ASSERT( !freePiles.isEmpty() );
2056 KCardPile * nextPile = freePiles.takeFirst();
2057
2058 tempMoves << qMakePair( nextPile, subCards );
2059 multiStepSubMove( subCards, nextPile, freePiles, freeCells );
2060
2061 cardsToSubMove -= tempMoveSize;
2062 }
2063
2064 // Move cards to free cells.
2065 for ( int i = 0; i < cards.size() - 1; ++i )
2066 {
2067 KCard * c = cards.at( cards.size() - 1 - i );
2068 m_multiStepMoves << qMakePair( c, freeCells[i] );
2069 }
2070
2071 // Move bottom card to destination pile.
2072 m_multiStepMoves << qMakePair( cards.first(), pile );
2073
2074 // Move cards from free cells to destination pile.
2075 for ( int i = 1; i < cards.size(); ++i )
2076 m_multiStepMoves << qMakePair( cards.at( i ), pile );
2077
2078 // If we just moved the bottomost card of the source pile, it must now be
2079 // empty and we won't need it any more. So we return it to the list of free
2080 // piles.
2081 KCardPile * sourcePile = cards.first()->pile();
2082 if ( sourcePile->at( 0 ) == cards.first() )
2083 freePiles << sourcePile;
2084
2085 // If we had to do any submoves, we now move those cards from their
2086 // temporary pile to the destination pile and free up their temporary pile.
2087 while ( !tempMoves.isEmpty() )
2088 {
2089 QPair<KCardPile*, QList<KCard*> > m = tempMoves.takeLast();
2090 multiStepSubMove( m.second, pile, freePiles, freeCells );
2091 freePiles << m.first;
2092 }
2093 }
2094
2095
continueMultiStepMove()2096 void DealerScene::continueMultiStepMove()
2097 {
2098 Q_ASSERT( !m_multiStepMoves.isEmpty() );
2099 Q_ASSERT( !isCardAnimationRunning() );
2100
2101 QPair<KCard*,KCardPile*> m = m_multiStepMoves.takeFirst();
2102 KCard * card = m.first;
2103 KCardPile * dest = m.second;
2104 KCardPile * source = card->pile();
2105
2106 Q_ASSERT( card == source->topCard() );
2107 Q_ASSERT( allowedToAdd( dest, QList<KCard*>() << card ) );
2108
2109 m_multiStepDuration = qMax<int>( m_multiStepDuration * 0.9, 50 );
2110
2111 dest->add( card );
2112 card->raise();
2113 updatePileLayout( dest, m_multiStepDuration );
2114 updatePileLayout( source, m_multiStepDuration );
2115
2116 if ( m_multiStepMoves.isEmpty() )
2117 takeState();
2118 }
2119
2120
2121 #include "dealer.moc"
2122 #include "moc_dealer.cpp"
2123