diff --git a/dealer.cpp b/dealer.cpp index a8fe2b4..d154deb 100644 --- a/dealer.cpp +++ b/dealer.cpp @@ -1,2111 +1,2111 @@ /* * Copyright (C) 1995 Paul Olav Tvete * Copyright (C) 2000-2009 Stephan Kulow * Copyright (C) 2009-2010 Parker Coates * * License of original code: * ------------------------------------------------------------------------- * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose and without fee is hereby granted, * provided that the above copyright notice appear in all copies and that * both that copyright notice and this permission notice appear in * supporting documentation. * * This file is provided AS IS with no warranties of any kind. The author * shall have no liability with respect to the infringement of copyrights, * trade secrets or any patents by this file or any part thereof. In no * event will the author be liable for any lost revenue or profits or * other special, indirect and consequential damages. * ------------------------------------------------------------------------- * * License of modifications/additions made after 2009-01-01: * ------------------------------------------------------------------------- * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ------------------------------------------------------------------------- */ #include "dealer.h" #include "dealerinfo.h" #include "kpat_debug.h" #include "messagebox.h" #include "renderer.h" #include "shuffle.h" #include "speeds.h" #include "patsolve/solverinterface.h" #include "view.h" #include "KCardTheme" #include #include #include #include #include #include #include #include #include #include namespace { const qreal wonBoxToSceneSizeRatio = 0.7; QString solverStatusMessage( int status, bool everWinnable ) { switch ( status ) { case SolverInterface::SolutionExists: return i18n("Solver: This game is winnable."); case SolverInterface::NoSolutionExists: return everWinnable ? i18n("Solver: This game is no longer winnable.") : i18n("Solver: This game cannot be won."); case SolverInterface::UnableToDetermineSolvability: return i18n("Solver: Unable to determine if this game is winnable."); case SolverInterface::SearchAborted: case SolverInterface::MemoryLimitReached: default: return QString(); } } int readIntAttribute( const QXmlStreamReader & xml, const QString & key, bool * ok = nullptr ) { QStringRef value = xml.attributes().value( key ); return QString::fromRawData( value.data(), value.length() ).toInt( ok ); } QString suitToString( int suit ) { switch ( suit ) { case KCardDeck::Clubs: return QStringLiteral("clubs"); case KCardDeck::Diamonds: return QStringLiteral("diamonds"); case KCardDeck::Hearts: return QStringLiteral("hearts"); case KCardDeck::Spades: return QStringLiteral("spades"); default: return QString(); } } QString rankToString( int rank ) { switch ( rank ) { case KCardDeck::Ace: return QStringLiteral("ace"); case KCardDeck::Two: return QStringLiteral("two"); case KCardDeck::Three: return QStringLiteral("three"); case KCardDeck::Four: return QStringLiteral("four"); case KCardDeck::Five: return QStringLiteral("five"); case KCardDeck::Six: return QStringLiteral("six"); case KCardDeck::Seven: return QStringLiteral("seven"); case KCardDeck::Eight: return QStringLiteral("eight"); case KCardDeck::Nine: return QStringLiteral("nine"); case KCardDeck::Ten: return QStringLiteral("ten"); case KCardDeck::Jack: return QStringLiteral("jack"); case KCardDeck::Queen: return QStringLiteral("queen"); case KCardDeck::King: return QStringLiteral("king"); default: return QString(); } } } class SolverThread : public QThread { Q_OBJECT public: SolverThread( SolverInterface * solver ) : m_solver( solver ) { } void run() override { SolverInterface::ExitStatus result = m_solver->patsolve(); emit finished( result ); } void abort() { m_solver->stopExecution(); wait(); } signals: void finished( int result ); private: SolverInterface * m_solver; }; int DealerScene::moveCount() const { return m_loadedMoveCount + m_undoStack.size(); } void DealerScene::saveLegacyFile( QIODevice * io ) { QXmlStreamWriter xml( io ); xml.setCodec( "UTF-8" ); xml.setAutoFormatting( true ); xml.setAutoFormattingIndent( -1 ); xml.writeStartDocument(); xml.writeDTD( QStringLiteral("") ); xml.writeStartElement( QStringLiteral("dealer") ); xml.writeAttribute( QStringLiteral("id"), QString::number( gameId() ) ); xml.writeAttribute( QStringLiteral("options"), getGameOptions() ); xml.writeAttribute( QStringLiteral("number"), QString::number( gameNumber() ) ); xml.writeAttribute( QStringLiteral("moves"), QString::number( moveCount() ) ); xml.writeAttribute( QStringLiteral("started"), QString::number( m_dealStarted ) ); xml.writeAttribute( QStringLiteral("data"), getGameState() ); foreach( const PatPile * p, patPiles() ) { xml.writeStartElement( QStringLiteral("pile") ); xml.writeAttribute( QStringLiteral("index"), QString::number( p->index() ) ); xml.writeAttribute( QStringLiteral("z"), QString::number( p->zValue() ) ); foreach( const KCard * c, p->cards() ) { xml.writeStartElement( QStringLiteral("card") ); xml.writeAttribute( QStringLiteral("suit"), QString::number( c->suit() ) ); xml.writeAttribute( QStringLiteral("value"), QString::number( c->rank() ) ); xml.writeAttribute( QStringLiteral("faceup"), QString::number( c->isFaceUp() ) ); xml.writeAttribute( QStringLiteral("z"), QString::number( c->zValue() ) ); xml.writeEndElement(); } xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndDocument(); m_dealWasJustSaved = true; } bool DealerScene::loadLegacyFile( QIODevice * io ) { resetInternals(); QXmlStreamReader xml( io ); xml.readNextStartElement(); // Before KDE4.3, KPat didn't store game specific options in the save // file. This could cause crashes when loading a Spider game with a // different number of suits than the current setting. Similarly, in // Klondike the number of cards drawn from the deck was forgotten, but // this never caused crashes. Fortunately, in Spider we can count the // number of suits ourselves. For Klondike, there is no way to recover // that information. QString options = xml.attributes().value( QStringLiteral("options") ).toString(); if ( gameId() == 17 && options.isEmpty() ) { QSet suits; while ( !xml.atEnd() ) if ( xml.readNextStartElement() && xml.name() == "card" ) suits << readIntAttribute( xml, QStringLiteral("suit") ); options = QString::number( suits.size() ); // "Rewind" back to the tag. Yes, this is ugly. xml.clear(); io->reset(); xml.setDevice( io ); xml.readNextStartElement(); } setGameOptions( options ); m_dealNumber = readIntAttribute( xml, QStringLiteral("number") ); m_loadedMoveCount = readIntAttribute( xml, QStringLiteral("moves") ); m_dealStarted = readIntAttribute( xml, QStringLiteral("started") ); QString gameStateData = xml.attributes().value( QStringLiteral("data") ).toString(); QMultiHash cards; foreach ( KCard * c, deck()->cards() ) cards.insert( (c->id() & 0xffff), c ); QHash piles; foreach ( PatPile * p, patPiles() ) piles.insert( p->index(), p ); // Loop through s. while ( xml.readNextStartElement() ) { if ( xml.name() != "pile" ) { qCWarning(KPAT_LOG) << "Expected a \"pile\" tag. Found a" << xml.name() << "tag."; return false; } bool ok; int index = readIntAttribute( xml, QStringLiteral("index"), &ok ); QHash::const_iterator it = piles.constFind( index ); if ( !ok || it == piles.constEnd() ) { qCWarning(KPAT_LOG) << "Unrecognized pile index:" << xml.attributes().value( QStringLiteral("index") ); return false; } PatPile * p = it.value(); p->clear(); // Loop through s. while ( xml.readNextStartElement() ) { if ( xml.name() != "card" ) { qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag."; return false; } bool suitOk, rankOk, faceUpOk; int suit = readIntAttribute( xml, QStringLiteral("suit"), &suitOk ); int rank = readIntAttribute( xml, QStringLiteral("value"), &rankOk ); bool faceUp = readIntAttribute( xml, QStringLiteral("faceup"), &faceUpOk ); quint32 id = KCardDeck::getId( KCardDeck::Suit( suit ), KCardDeck::Rank( rank ), 0 ); KCard * card = cards.take( id ); if ( !suitOk || !rankOk || !faceUpOk || !card ) { qCWarning(KPAT_LOG) << "Unrecognized card: suit=" << xml.attributes().value(QStringLiteral("suit")) << " value=" << xml.attributes().value(QStringLiteral("value")) << " faceup=" << xml.attributes().value(QStringLiteral("faceup")); return false; } card->setFaceUp( faceUp ); p->add( card ); xml.skipCurrentElement(); } updatePileLayout( p, 0 ); } setGameState( gameStateData ); emit updateMoves( moveCount() ); takeState(); return true; } void DealerScene::saveFile( QIODevice * io ) { QXmlStreamWriter xml( io ); xml.setCodec( "UTF-8" ); xml.setAutoFormatting( true ); xml.setAutoFormattingIndent( -1 ); xml.writeStartDocument(); xml.writeStartElement( QStringLiteral("kpat-game") ); xml.writeAttribute( QStringLiteral("game-type"), m_di->baseIdString() ); if ( !getGameOptions().isEmpty() ) xml.writeAttribute( QStringLiteral("game-type-options"), getGameOptions() ); xml.writeAttribute( QStringLiteral("deal-number"), QString::number( gameNumber() ) ); if (!m_currentState) { deck()->stopAnimations(); takeState(); } QList allStates; for ( int i = 0; i < m_undoStack.size(); ++i ) allStates << m_undoStack.at( i ); allStates << m_currentState; for ( int i = m_redoStack.size() - 1; i >= 0; --i ) allStates << m_redoStack.at( i ); QString lastGameSpecificState; for ( int i = 0; i < allStates.size(); ++i ) { const GameState * state = allStates.at( i ); xml.writeStartElement( QStringLiteral("state") ); if ( state->stateData != lastGameSpecificState ) { xml.writeAttribute( QStringLiteral("game-specific-state"), state->stateData ); lastGameSpecificState = state->stateData; } if ( i == m_undoStack.size() ) xml.writeAttribute( QStringLiteral("current"), QStringLiteral("true") ); foreach ( const CardStateChange & change, state->changes ) { xml.writeStartElement( QStringLiteral("move") ); xml.writeAttribute( QStringLiteral("pile"), change.newState.pile->objectName() ); xml.writeAttribute( QStringLiteral("position"), QString::number( change.newState.index ) ); bool faceChanged = !change.oldState.pile || change.oldState.faceUp != change.newState.faceUp; foreach ( const KCard * card, change.cards ) { xml.writeStartElement( QStringLiteral("card") ); xml.writeAttribute( QStringLiteral("id"), QStringLiteral("%1").arg( card->id(), 7, 10, QChar('0') ) ); xml.writeAttribute( QStringLiteral("suit"), suitToString( card->suit() ) ); xml.writeAttribute( QStringLiteral("rank"), rankToString( card->rank() ) ); if ( faceChanged ) xml.writeAttribute( QStringLiteral("turn"), change.newState.faceUp ? "face-up" : "face-down" ); xml.writeEndElement(); } xml.writeEndElement(); } xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndDocument(); m_dealWasJustSaved = true; } bool DealerScene::loadFile( QIODevice * io ) { resetInternals(); bool reenableAutoDrop = autoDropEnabled(); setAutoDropEnabled( false ); QXmlStreamReader xml( io ); xml.readNextStartElement(); if ( xml.name() != "kpat-game" ) { qCWarning(KPAT_LOG) << "First tag is not \"kpat-game\""; return false; } m_dealNumber = readIntAttribute( xml, QStringLiteral("deal-number") ); setGameOptions( xml.attributes().value( QStringLiteral("game-type-options") ).toString() ); QMultiHash cardHash; foreach ( KCard * c, deck()->cards() ) cardHash.insert( c->id(), c ); QHash pileHash; foreach ( KCardPile * p, piles() ) pileHash.insert( p->objectName(), p ); int undosToDo = -1; while( xml.readNextStartElement() ) { if ( xml.name() != "state" ) { qCWarning(KPAT_LOG) << "Expected a \"state\" tag. Found a" << xml.name() << "tag."; return false; } if ( xml.attributes().hasAttribute( QStringLiteral("game-specific-state") ) ) setGameState( xml.attributes().value( QStringLiteral("game-specific-state") ).toString() ); if ( undosToDo > -1 ) ++undosToDo; else if ( xml.attributes().value( QStringLiteral("current") ) == "true" ) undosToDo = 0; while( xml.readNextStartElement() ) { if ( xml.name() != "move" ) { qCWarning(KPAT_LOG) << "Expected a \"move\" tag. Found a" << xml.name() << "tag."; return false; } QString pileName = xml.attributes().value( QStringLiteral("pile") ).toString(); KCardPile * pile = pileHash.value( pileName ); bool indexOk; int index = readIntAttribute( xml, QStringLiteral("position"), &indexOk ); if ( !pile || !indexOk ) { qCWarning(KPAT_LOG) << "Unrecognized pile or index."; return false; } while ( xml.readNextStartElement() ) { if ( xml.name() != "card" ) { qCWarning(KPAT_LOG) << "Expected a \"card\" tag. Found a" << xml.name() << "tag."; return false; } KCard * card = cardHash.value( readIntAttribute( xml, QStringLiteral("id") ) ); if ( !card ) { qCWarning(KPAT_LOG) << "Unrecognized card."; return false; } if ( xml.attributes().value(QStringLiteral("turn")) == "face-up" ) card->setFaceUp( true ); else if ( xml.attributes().value(QStringLiteral("turn")) == "face-down" ) card->setFaceUp( false ); pile->insert( index, card ); ++index; xml.skipCurrentElement(); } } takeState(); } m_loadedMoveCount = 0; m_dealStarted = moveCount() > 0; emit updateMoves( moveCount() ); while ( undosToDo > 0 ) { undo(); --undosToDo; } foreach ( KCardPile * p, piles() ) updatePileLayout( p, 0 ); setAutoDropEnabled( reenableAutoDrop ); return true; } DealerScene::DealerScene( const DealerInfo * di ) : m_di( di ), m_solver( nullptr ), m_solverThread( nullptr ), m_peekedCard( nullptr ), m_dealNumber( 0 ), m_loadedMoveCount( 0 ), m_neededFutureMoves( 1 ), m_supportedActions( 0 ), m_autoDropEnabled( false ), m_solverEnabled( false ), m_dealStarted( false ), m_dealWasEverWinnable( false ), m_dealHasBeenWon( false ), m_dealWasJustSaved( false ), m_statisticsRecorded( false ), m_playerReceivedHelp( false ), m_toldAboutWonGame( false ), m_toldAboutLostGame( false ), m_dropSpeedFactor( 1 ), m_interruptAutoDrop( false ), m_dealInProgress( false ), m_hintInProgress( false ), m_demoInProgress( false ), m_dropInProgress( false ), m_hintQueued( false ), m_demoQueued( false ), m_dropQueued( false ), m_newCardsQueued( false ), m_takeStateQueued( false ), m_currentState( nullptr ) { setItemIndexMethod(QGraphicsScene::NoIndex); m_solverUpdateTimer.setInterval( 250 ); m_solverUpdateTimer.setSingleShot( true ); connect(&m_solverUpdateTimer, &QTimer::timeout, this, &DealerScene::stopAndRestartSolver); m_demoTimer.setSingleShot( true ); connect(&m_demoTimer, &QTimer::timeout, this, &DealerScene::demo); m_dropTimer.setSingleShot( true ); connect(&m_dropTimer, &QTimer::timeout, this, &DealerScene::drop); m_wonItem = new MessageBox(); m_wonItem->setZValue( 2000 ); m_wonItem->hide(); addItem( m_wonItem ); connect(this, &DealerScene::cardAnimationDone, this, &DealerScene::animationDone); connect(this, &DealerScene::cardDoubleClicked, this, &DealerScene::tryAutomaticMove); // Make rightClick == doubleClick. See bug #151921 connect(this, &DealerScene::cardRightClicked, this, &DealerScene::tryAutomaticMove); } DealerScene::~DealerScene() { stop(); disconnect(); if ( m_solverThread ) m_solverThread->abort(); delete m_solverThread; m_solverThread = nullptr; delete m_solver; m_solver = nullptr; qDeleteAll( m_undoStack ); delete m_currentState; qDeleteAll( m_redoStack ); delete m_wonItem; } void DealerScene::addPatPile( PatPile * pile ) { if ( !m_patPiles.contains( pile ) ) m_patPiles.append( pile ); } void DealerScene::removePatPile( PatPile * pile ) { m_patPiles.removeAll( pile ); } QList DealerScene::patPiles() const { return m_patPiles; } void DealerScene::cardsMoved( const QList & cards, KCardPile * oldPile, KCardPile * newPile ) { PatPile * newPatPile = dynamic_cast( newPile ); PatPile * oldPatPile = dynamic_cast( oldPile ); if ( oldPatPile && oldPatPile->isFoundation() && newPatPile && !newPatPile->isFoundation() ) { foreach ( KCard * c, cards ) m_cardsRemovedFromFoundations.insert( c ); } else { foreach ( KCard * c, cards ) m_cardsRemovedFromFoundations.remove( c ); } if ( !m_dropInProgress && !m_dealInProgress ) { m_dealStarted = true; takeState(); } } void DealerScene::startHint() { stopDemo(); stopDrop(); if ( isCardAnimationRunning() ) { m_hintQueued = true; return; } if ( isKeyboardModeActive() ) setKeyboardModeActive( false ); QList toHighlight; foreach ( const MoveHint & h, getHints() ) toHighlight << h.card(); if ( !m_winningMoves.isEmpty() ) { MOVE m = m_winningMoves.first(); MoveHint mh = solver()->translateMove( m ); if ( mh.isValid() ) toHighlight << mh.card(); } m_hintInProgress = !toHighlight.isEmpty(); setHighlightedItems( toHighlight ); emit hintActive( m_hintInProgress ); } void DealerScene::stopHint() { if ( m_hintInProgress ) { m_hintInProgress = false; clearHighlightedItems(); emit hintActive( false ); } } bool DealerScene::isHintActive() const { return m_hintInProgress; } QList DealerScene::getSolverHints() { QList hintList; if ( m_solverThread && m_solverThread->isRunning() ) m_solverThread->abort(); solver()->translate_layout(); solver()->patsolve( 1 ); foreach ( const MOVE & m, solver()->firstMoves() ) { MoveHint mh = solver()->translateMove( m ); hintList << mh; } return hintList; } QList DealerScene::getHints() { if ( solver() ) return getSolverHints(); QList hintList; foreach (PatPile * store, patPiles()) { if (store->isFoundation() || store->isEmpty()) continue; QList cards = store->cards(); while (cards.count() && !cards.first()->isFaceUp()) cards.erase(cards.begin()); QList::Iterator iti = cards.begin(); while (iti != cards.end()) { if (allowedToRemove(store, (*iti))) { foreach (PatPile * dest, patPiles()) { int cardIndex = store->indexOf(*iti); if (cardIndex == 0 && dest->isEmpty() && !dest->isFoundation()) continue; if (!checkAdd(dest, dest->cards(), cards)) continue; if (dest->isFoundation()) { hintList << MoveHint( *iti, dest, 127 ); } else { QList cardsBelow = cards.mid(0, cardIndex); // if it could be here as well, then it's no use if ((cardsBelow.isEmpty() && !dest->isEmpty()) || !checkAdd(store, cardsBelow, cards)) { hintList << MoveHint( *iti, dest, 0 ); } else if (checkPrefering(dest, dest->cards(), cards) && !checkPrefering(store, cardsBelow, cards)) { // if checkPrefers says so, we add it nonetheless hintList << MoveHint( *iti, dest, 10 ); } } } } cards.erase(iti); iti = cards.begin(); } } return hintList; } static bool prioSort(const MoveHint &c1, const MoveHint &c2) { return c1.priority() < c2.priority(); } MoveHint DealerScene::chooseHint() { if ( !m_winningMoves.isEmpty() ) { MOVE m = m_winningMoves.takeFirst(); MoveHint mh = solver()->translateMove( m ); #ifndef NDEBUG if ( m.totype == O_Type ) fprintf( stderr, "move from %d out (at %d) Prio: %d\n", m.from, m.turn_index, m.pri ); else fprintf( stderr, "move from %d to %d (%d) Prio: %d\n", m.from, m.to, m.turn_index, m.pri ); #endif return mh; } QList hintList = getHints(); if ( hintList.isEmpty() ) { return MoveHint(); } else { // Generate a random number with an exponentional distribution averaging 1/4. - qreal randomExp = qMin( -log( 1 - qreal( QRandomGenerator::global()->generate() ) / RAND_MAX ) / 4, 1 ); + qreal randomExp = qMin( -log( 1 - QRandomGenerator::global()->generateDouble() ) / 4, 1 ); int randomIndex = randomExp * ( hintList.size() - 1 ); std::sort(hintList.begin(), hintList.end(), prioSort); return hintList.at( randomIndex ); } } void DealerScene::startNew( int dealNumber ) { if ( dealNumber != -1 ) m_dealNumber = qBound( 1, dealNumber, INT_MAX ); // Only record the statistics and reset gameStarted if we're starting a // new game number or we're restarting a game we've already won. if ( dealNumber != -1 || m_dealHasBeenWon ) { recordGameStatistics(); m_statisticsRecorded = false; m_dealStarted = false; } if ( isCardAnimationRunning() ) { QTimer::singleShot( 100, this, SLOT(startNew()) ); return; } if ( m_solverThread && m_solverThread->isRunning() ) m_solverThread->abort(); resetInternals(); emit updateMoves( 0 ); foreach( KCardPile * p, piles() ) p->clear(); m_dealInProgress = true; restart( KpatShuffle::shuffled( deck()->cards(), m_dealNumber ) ); m_dealInProgress = false; takeState(); update(); } void DealerScene::resetInternals() { stop(); setKeyboardModeActive( false ); m_dealHasBeenWon = false; m_wonItem->hide(); qDeleteAll( m_undoStack ); m_undoStack.clear(); delete m_currentState; m_currentState = nullptr; qDeleteAll( m_redoStack ); m_redoStack.clear(); m_lastKnownCardStates.clear(); m_dealWasJustSaved = false; m_dealWasEverWinnable = false; m_toldAboutLostGame = false; m_toldAboutWonGame = false; m_loadedMoveCount = 0; m_playerReceivedHelp = false; m_dealInProgress = false; m_dropInProgress = false; m_dropSpeedFactor = 1; m_cardsRemovedFromFoundations.clear(); foreach (KCard * c, deck()->cards()) { c->disconnect( this ); c->stopAnimation(); } emit solverStateChanged( QString() ); emit gameInProgress( true ); } QPointF posAlongRect( qreal distOnRect, const QRectF & rect ) { if ( distOnRect < rect.width() ) return rect.topLeft() + QPointF( distOnRect, 0 ); distOnRect -= rect.width(); if ( distOnRect < rect.height() ) return rect.topRight() + QPointF( 0, distOnRect ); distOnRect -= rect.height(); if ( distOnRect < rect.width() ) return rect.bottomRight() + QPointF( -distOnRect, 0 ); distOnRect -= rect.width(); return rect.bottomLeft() + QPointF( 0, -distOnRect ); } void DealerScene::won() { if ( m_dealHasBeenWon ) return; m_dealHasBeenWon = true; m_toldAboutWonGame = true; stopDemo(); recordGameStatistics(); emit solverStateChanged( QString() ); emit newCardsPossible( false ); emit undoPossible( false ); emit redoPossible( false ); emit gameInProgress( false ); setKeyboardModeActive( false ); qreal speed = sqrt( width() * width() + height() * height() ) / ( DURATION_WON ); QRectF justOffScreen = sceneRect().adjusted( -deck()->cardWidth(), -deck()->cardHeight(), 0, 0 ); qreal spacing = 2 * ( justOffScreen.width() + justOffScreen.height() ) / deck()->cards().size(); qreal distOnRect = 0; foreach ( KCard *c, deck()->cards() ) { distOnRect += spacing; QPointF pos2 = posAlongRect( distOnRect, justOffScreen ); QPointF delta = c->pos() - pos2; qreal dist = sqrt( delta.x() * delta.x() + delta.y() * delta.y() ); c->setFaceUp( true ); c->animate( pos2, c->zValue(), 0, true, false, dist / speed ); } connect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage); } void DealerScene::showWonMessage() { disconnect(deck(), &KAbstractCardDeck::cardAnimationDone, this, &DealerScene::showWonMessage); // It shouldn't be necessary to stop the demo yet again here, but we // get crashes if we don't. Will have to look into this further. stopDemo(); // Hide all cards to prevent them from showing up accidentally if the // window is resized. foreach ( KCard * c, deck()->cards() ) c->hide(); updateWonItem(); m_wonItem->show(); } void DealerScene::updateWonItem() { const qreal aspectRatio = Renderer::self()->aspectRatioOfElement(QStringLiteral("message_frame")); int boxWidth; int boxHeight; if ( width() < aspectRatio * height() ) { boxWidth = width() * wonBoxToSceneSizeRatio; boxHeight = boxWidth / aspectRatio; } else { boxHeight = height() * wonBoxToSceneSizeRatio; boxWidth = boxHeight * aspectRatio; } m_wonItem->setSize( QSize( boxWidth, boxHeight ) ); if ( m_playerReceivedHelp ) m_wonItem->setMessage( i18n( "Congratulations! We have won." ) ); else m_wonItem->setMessage( i18n( "Congratulations! You have won." ) ); m_wonItem->setPos( QPointF( (width() - boxWidth) / 2, (height() - boxHeight) / 2 ) + sceneRect().topLeft() ); } bool DealerScene::allowedToAdd( const KCardPile * pile, const QList & cards ) const { if ( !pile->isEmpty() && !pile->topCard()->isFaceUp() ) return false; const PatPile * p = dynamic_cast( pile ); return p && checkAdd( p, p->cards(), cards ); } bool DealerScene::allowedToRemove( const KCardPile * pile, const KCard * card ) const { const PatPile * p = dynamic_cast( pile ); QList cards = pile->topCardsDownTo( card ); return p && card->isFaceUp() && !cards.isEmpty() && checkRemove( p, cards ); } bool DealerScene::checkAdd( const PatPile * pile, const QList & oldCards, const QList & newCards ) const { Q_UNUSED( pile ) Q_UNUSED( oldCards ) Q_UNUSED( newCards ) return false; } bool DealerScene::checkRemove(const PatPile * pile, const QList & cards) const { Q_UNUSED( pile ) Q_UNUSED( cards ) return false; } bool DealerScene::checkPrefering( const PatPile * pile, const QList & oldCards, const QList & newCards ) const { Q_UNUSED( pile ) Q_UNUSED( oldCards ) Q_UNUSED( newCards ) return false; } void DealerScene::mousePressEvent( QGraphicsSceneMouseEvent * e ) { stop(); const QList itemsAtPoint = items( e->scenePos() ); KCard * card = qgraphicsitem_cast( itemsAtPoint.isEmpty() ? 0 : itemsAtPoint.first() ); if ( m_peekedCard ) { e->accept(); } else if ( e->button() == Qt::RightButton && card && card->pile() && card != card->pile()->topCard() && cardsBeingDragged().isEmpty() && !isCardAnimationRunning() ) { e->accept(); m_peekedCard = card; QPointF pos2( card->x() + deck()->cardWidth() / 3.0, card->y() - deck()->cardHeight() / 3.0 ); card->setZValue( card->zValue() + 0.1 ); card->animate( pos2, card->zValue(), 20, card->isFaceUp(), false, DURATION_FANCYSHOW ); } else { KCardScene::mousePressEvent( e ); if ( !cardsBeingDragged().isEmpty() ) emit cardsPickedUp(); } } void DealerScene::mouseReleaseEvent( QGraphicsSceneMouseEvent * e ) { stop(); if ( e->button() == Qt::RightButton && m_peekedCard && m_peekedCard->pile() ) { e->accept(); updatePileLayout( m_peekedCard->pile(), DURATION_FANCYSHOW ); m_peekedCard = nullptr; } else { bool hadCards = !cardsBeingDragged().isEmpty(); KCardScene::mouseReleaseEvent( e ); if ( hadCards && cardsBeingDragged().isEmpty() ) emit cardsPutDown(); } } void DealerScene::mouseDoubleClickEvent( QGraphicsSceneMouseEvent * e ) { stop(); KCardScene::mouseDoubleClickEvent( e ); } bool DealerScene::tryAutomaticMove( KCard * card ) { if ( !isCardAnimationRunning() && card && card->pile() && card == card->pile()->topCard() && card->isFaceUp() && allowedToRemove( card->pile(), card ) ) { QList cardList = QList() << card; foreach ( PatPile * p, patPiles() ) { if ( p->isFoundation() && allowedToAdd( p, cardList ) ) { moveCardToPile( card , p, DURATION_MOVE ); return true; } } } return false; } void DealerScene::undo() { undoOrRedo( true ); } void DealerScene::redo() { undoOrRedo( false ); } void DealerScene::undoOrRedo( bool undo ) { stop(); if ( isCardAnimationRunning() ) return; // The undo and redo actions are almost identical, except for where states // are pulled from and pushed to, so to keep things generic, we use // direction dependent const references throughout this code. QStack & fromStack = undo ? m_undoStack : m_redoStack; QStack & toStack = undo ? m_redoStack : m_undoStack; if ( !fromStack.isEmpty() && m_currentState ) { // If we're undoing, we use the oldStates of the changes of the current // state. If we're redoing, we use the newStates of the changes of the // nextState. const QList & changes = undo ? m_currentState->changes : fromStack.top()->changes; // Update the currentState pointer and undo/redo stacks. toStack.push( m_currentState ); m_currentState = fromStack.pop(); setGameState( m_currentState->stateData ); QSet pilesAffected; foreach ( const CardStateChange & change, changes ) { CardState sourceState = undo ? change.newState : change.oldState; CardState destState = undo ? change.oldState : change.newState; PatPile * sourcePile = dynamic_cast( sourceState.pile ); PatPile * destPile = dynamic_cast( destState.pile ); bool notDroppable = destState.takenDown || ((sourcePile && sourcePile->isFoundation()) && !(destPile && destPile->isFoundation())); pilesAffected << sourceState.pile << destState.pile; foreach ( KCard * c, change.cards ) { m_lastKnownCardStates.insert( c, destState ); c->setFaceUp( destState.faceUp ); destState.pile->insert( destState.index, c ); if ( notDroppable ) m_cardsRemovedFromFoundations.insert( c ); else m_cardsRemovedFromFoundations.remove( c ); ++sourceState.index; ++destState.index; } } // At this point all cards should be in the right piles, but not // necessarily at the right positions within those piles. So we // run through the piles involved and swap card positions until // everything is back in its place, then relayout the piles. foreach( KCardPile * p, pilesAffected ) { int i = 0; while ( i < p->count() ) { int index = m_lastKnownCardStates.value( p->at( i ) ).index; if ( i == index ) ++i; else p->swapCards( i, index ); } updatePileLayout( p, 0 ); } emit updateMoves( moveCount() ); emit undoPossible( !m_undoStack.isEmpty() ); emit redoPossible( !m_redoStack.isEmpty() ); if ( m_toldAboutLostGame ) // everything's possible again { gameInProgress( true ); m_toldAboutLostGame = false; m_toldAboutWonGame = false; } int solvability = m_currentState->solvability; m_winningMoves = m_currentState->winningMoves; emit solverStateChanged( solverStatusMessage( solvability, m_dealWasEverWinnable ) ); if ( m_solver && ( solvability == SolverInterface::SearchAborted || solvability == SolverInterface::MemoryLimitReached ) ) { startSolver(); } } } void DealerScene::takeState() { if ( isCardAnimationRunning() ) { m_takeStateQueued = true; return; } if ( !isDemoActive() ) m_winningMoves.clear(); QList changes; foreach ( KCardPile * p, piles() ) { QList currentRun; CardState oldRunState; CardState newRunState; for ( int i = 0; i < p->count(); ++i ) { KCard * c = p->at( i ); const CardState & oldState = m_lastKnownCardStates.value( c ); CardState newState( p, i, c->isFaceUp(), m_cardsRemovedFromFoundations.contains( c ) ); // The card has changed. if ( newState != oldState ) { // There's a run in progress, but this card isn't part of it. if ( !currentRun.isEmpty() && (oldState.pile != oldRunState.pile || (oldState.index != -1 && oldState.index != oldRunState.index + currentRun.size()) || oldState.faceUp != oldRunState.faceUp || newState.faceUp != newRunState.faceUp || oldState.takenDown != oldRunState.takenDown || newState.takenDown != newRunState.takenDown) ) { changes << CardStateChange( oldRunState, newRunState, currentRun ); currentRun.clear(); } // This card is the start of a new run. if ( currentRun.isEmpty() ) { oldRunState = oldState; newRunState = newState; } currentRun << c; m_lastKnownCardStates.insert( c, newState ); } } // Add the last run, if any. if ( !currentRun.isEmpty() ) { changes << CardStateChange( oldRunState, newRunState, currentRun ); } } // If nothing has changed, we're done. if ( changes.isEmpty() && m_currentState && m_currentState->stateData == getGameState() ) { return; } if ( m_currentState ) { m_undoStack.push( m_currentState ); qDeleteAll( m_redoStack ); m_redoStack.clear(); } m_currentState = new GameState( changes, getGameState() ); emit redoPossible( false ); emit undoPossible( !m_undoStack.isEmpty() ); emit updateMoves( moveCount() ); m_dealWasJustSaved = false; if ( isGameWon() ) { won(); return; } if ( !m_toldAboutWonGame && !m_toldAboutLostGame && isGameLost() ) { emit gameInProgress( false ); emit solverStateChanged( i18n( "Solver: This game is lost." ) ); m_toldAboutLostGame = true; stopDemo(); return; } if ( !isDemoActive() && !isCardAnimationRunning() && m_solver ) startSolver(); if ( autoDropEnabled() && !isDropActive() && !isDemoActive() && m_redoStack.isEmpty() ) { if ( m_interruptAutoDrop ) m_interruptAutoDrop = false; else startDrop(); } } void DealerScene::setSolverEnabled(bool a) { m_solverEnabled = a; } void DealerScene::setAutoDropEnabled( bool enabled ) { m_autoDropEnabled = enabled; } bool DealerScene::autoDropEnabled() const { return m_autoDropEnabled; } void DealerScene::startDrop() { stopHint(); stopDemo(); if ( isCardAnimationRunning() ) { m_dropQueued = true; return; } m_dropInProgress = true; m_interruptAutoDrop = false; m_dropSpeedFactor = 1; emit dropActive( true ); drop(); } void DealerScene::stopDrop() { if ( m_dropInProgress ) { m_dropTimer.stop(); m_dropInProgress = false; emit dropActive( false ); if ( autoDropEnabled() && m_takeStateQueued ) m_interruptAutoDrop = true; } } bool DealerScene::isDropActive() const { return m_dropInProgress; } bool DealerScene::drop() { foreach ( const MoveHint & mh, getHints() ) { if ( mh.pile() && mh.pile()->isFoundation() && mh.priority() > 120 && !m_cardsRemovedFromFoundations.contains( mh.card() ) ) { QList cards = mh.card()->pile()->topCardsDownTo( mh.card() ); QMap oldPositions; foreach ( KCard * c, cards ) oldPositions.insert( c, c->pos() ); moveCardsToPile( cards, mh.pile(), DURATION_MOVE ); int count = 0; foreach ( KCard * c, cards ) { c->completeAnimation(); QPointF destPos = c->pos(); c->setPos( oldPositions.value( c ) ); int duration = speedUpTime( DURATION_AUTODROP + count * DURATION_AUTODROP / 10 ); c->animate( destPos, c->zValue(), 0, c->isFaceUp(), true, duration ); ++count; } m_dropSpeedFactor *= AUTODROP_SPEEDUP_FACTOR; takeState(); return true; } } m_dropInProgress = false; emit dropActive( false ); return false; } int DealerScene::speedUpTime( int delay ) const { if ( delay < DURATION_AUTODROP_MINIMUM ) return delay; else return qMax( delay * m_dropSpeedFactor, DURATION_AUTODROP_MINIMUM ); } void DealerScene::stopAndRestartSolver() { if ( m_toldAboutLostGame || m_toldAboutWonGame ) // who cares? return; if ( m_solverThread && m_solverThread->isRunning() ) { m_solverThread->abort(); } if ( isCardAnimationRunning() ) { startSolver(); return; } slotSolverEnded(); } void DealerScene::slotSolverEnded() { if ( m_solverThread && m_solverThread->isRunning() ) return; m_solver->translate_layout(); m_winningMoves.clear(); emit solverStateChanged( i18n("Solver: Calculating...") ); if ( !m_solverThread ) { m_solverThread = new SolverThread( m_solver ); connect(m_solverThread, &SolverThread::finished, this, &DealerScene::slotSolverFinished); } m_solverThread->start( m_solverEnabled ? QThread::IdlePriority : QThread::NormalPriority ); } void DealerScene::slotSolverFinished( int result ) { if ( result == SolverInterface::SolutionExists ) { m_winningMoves = m_solver->winMoves(); m_dealWasEverWinnable = true; } emit solverStateChanged( solverStatusMessage( result, m_dealWasEverWinnable ) ); if ( m_currentState ) { m_currentState->solvability = static_cast( result ); m_currentState->winningMoves = m_winningMoves; } if ( result == SolverInterface::SearchAborted ) startSolver(); } int DealerScene::gameNumber() const { return m_dealNumber; } void DealerScene::stop() { stopHint(); stopDemo(); stopDrop(); } void DealerScene::animationDone() { Q_ASSERT( !isCardAnimationRunning() ); if ( !m_multiStepMoves.isEmpty() ) { continueMultiStepMove(); return; } if ( m_takeStateQueued ) { m_takeStateQueued = false; takeState(); } if ( m_demoInProgress ) { m_demoTimer.start( TIME_BETWEEN_MOVES ); } else if ( m_dropInProgress ) { m_dropTimer.start( speedUpTime( TIME_BETWEEN_MOVES ) ); } else if ( m_newCardsQueued ) { m_newCardsQueued = false; newCards(); } else if ( m_hintQueued ) { m_hintQueued = false; startHint(); } else if ( m_demoQueued ) { m_demoQueued = false; startDemo(); } else if ( m_dropQueued ) { m_dropQueued = false; startDrop(); } } void DealerScene::startDemo() { stopHint(); stopDrop(); if ( isCardAnimationRunning() ) { m_demoQueued = true; return; } m_demoInProgress = true; m_playerReceivedHelp = true; m_dealStarted = true; demo(); } void DealerScene::stopDemo() { if ( m_demoInProgress ) { m_demoTimer.stop(); m_demoInProgress = false; emit demoActive( false ); } } bool DealerScene::isDemoActive() const { return m_demoInProgress; } void DealerScene::demo() { if ( isCardAnimationRunning() ) { m_demoQueued = true; return; } m_demoInProgress = true; m_playerReceivedHelp = true; m_dealStarted = true; clearHighlightedItems(); m_demoTimer.stop(); MoveHint mh = chooseHint(); if ( mh.isValid() ) { KCard * card = mh.card(); Q_ASSERT( card ); KCardPile * sourcePile = mh.card()->pile(); Q_ASSERT( sourcePile ); Q_ASSERT( allowedToRemove( sourcePile, card ) ); PatPile * destPile = mh.pile(); Q_ASSERT( destPile ); Q_ASSERT( sourcePile != destPile ); QList cards = sourcePile->topCardsDownTo( card ); Q_ASSERT( allowedToAdd( destPile, cards ) ); if ( destPile->isEmpty() ) { qCDebug(KPAT_LOG) << "Moving" << card->objectName() << "from the" << sourcePile->objectName() << "pile to the" << destPile->objectName() << "pile, which is empty"; } else { qCDebug(KPAT_LOG) << "Moving" << card->objectName() << "from the" << sourcePile->objectName() << "pile to the" << destPile->objectName() << "pile, putting it on top of" << destPile->topCard()->objectName(); } moveCardsToPile( cards, destPile, DURATION_DEMO ); } else if ( !newCards() ) { if (isGameWon()) { won(); } else { stopDemo(); slotSolverEnded(); } return; } emit demoActive( true ); takeState(); } void DealerScene::drawDealRowOrRedeal() { stop(); if ( isCardAnimationRunning() ) { m_newCardsQueued = true; return; } m_newCardsQueued = false; newCards(); } bool DealerScene::newCards() { return false; } void DealerScene::setSolver( SolverInterface *s) { delete m_solver; delete m_solverThread; m_solver = s; m_solverThread = nullptr; } bool DealerScene::isGameWon() const { foreach (PatPile *p, patPiles()) { if (!p->isFoundation() && !p->isEmpty()) return false; } return true; } void DealerScene::startSolver() { if( m_solverEnabled ) m_solverUpdateTimer.start(); } bool DealerScene::isGameLost() const { if (! m_winningMoves.isEmpty()) { return false; } if ( solver() ) { if ( m_solverThread && m_solverThread->isRunning() ) m_solverThread->abort(); solver()->translate_layout(); return solver()->patsolve( neededFutureMoves() ) == SolverInterface::NoSolutionExists; } return false; } void DealerScene::recordGameStatistics() { // Don't record the game if it was never started, if it is unchanged since // it was last saved (allowing the user to close KPat after saving without // it recording a loss) or if it has already been recorded.// takeState(); // copying it again if ( m_dealStarted && !m_dealWasJustSaved && !m_statisticsRecorded ) { int id = oldId(); QString totalPlayedKey = QStringLiteral("total%1").arg( id ); QString wonKey = QStringLiteral("won%1").arg( id ); QString winStreakKey = QStringLiteral("winstreak%1").arg( id ); QString maxWinStreakKey = QStringLiteral("maxwinstreak%1").arg( id ); QString loseStreakKey = QStringLiteral("loosestreak%1").arg( id ); QString maxLoseStreakKey = QStringLiteral("maxloosestreak%1").arg( id ); QString minMovesKey = QStringLiteral("minmoves%1").arg( id ); KConfigGroup config(KSharedConfig::openConfig(), scores_group); int totalPlayed = config.readEntry( totalPlayedKey, 0 ); int won = config.readEntry( wonKey, 0 ); int winStreak = config.readEntry( winStreakKey, 0 ); int maxWinStreak = config.readEntry( maxWinStreakKey, 0 ); int loseStreak = config.readEntry( loseStreakKey, 0 ); int maxLoseStreak = config.readEntry( maxLoseStreakKey, 0 ); int minMoves = config.readEntry( minMovesKey, -1 ); ++totalPlayed; if ( m_dealHasBeenWon ) { ++won; ++winStreak; maxWinStreak = qMax( winStreak, maxWinStreak ); loseStreak = 0; if ( minMoves < 0 ) minMoves = moveCount(); else minMoves = qMin( minMoves, moveCount() ); } else { ++loseStreak; maxLoseStreak = qMax( loseStreak, maxLoseStreak ); winStreak = 0; } config.writeEntry( totalPlayedKey, totalPlayed ); config.writeEntry( wonKey, won ); config.writeEntry( winStreakKey, winStreak ); config.writeEntry( maxWinStreakKey, maxWinStreak ); config.writeEntry( loseStreakKey, loseStreak ); config.writeEntry( maxLoseStreakKey, maxLoseStreak ); config.writeEntry( minMovesKey, minMoves ); m_statisticsRecorded = true; } } void DealerScene::relayoutScene() { KCardScene::relayoutScene(); if ( m_wonItem->isVisible() ) updateWonItem(); } int DealerScene::gameId() const { return m_di->baseId(); } void DealerScene::setActions( int actions ) { m_supportedActions = actions; } int DealerScene::actions() const { return m_supportedActions; } QList DealerScene::configActions() const { return QList(); } SolverInterface * DealerScene::solver() const { return m_solver; } int DealerScene::neededFutureMoves() const { return m_neededFutureMoves; } void DealerScene::setNeededFutureMoves( int i ) { m_neededFutureMoves = i; } void DealerScene::setDeckContents( int copies, const QList & suits ) { Q_ASSERT( copies >= 1 ); Q_ASSERT( !suits.isEmpty() ); // Note that the order in which the cards are created can not be changed // without breaking the game numbering. For historical reasons, KPat // generates card by rank and then by suit, rather than the more common // suit then rank ordering. QList ids; unsigned int number = 0; for ( int i = 0; i < copies; ++i ) foreach ( const KCardDeck::Rank & r, KCardDeck::standardRanks() ) foreach ( const KCardDeck::Suit & s, suits ) ids << KCardDeck::getId( s, r, number++ ); deck()->setDeckContents( ids ); } QImage DealerScene::createDump() const { const QSize previewSize( 480, 320 ); foreach ( KCard * c, deck()->cards() ) c->completeAnimation(); QMultiMap itemsByZ; foreach ( QGraphicsItem * item, items() ) { Q_ASSERT( item->zValue() >= 0 ); itemsByZ.insert( item->zValue(), item ); } QImage img( contentArea().size().toSize(), QImage::Format_ARGB32 ); img.fill( Qt::transparent ); QPainter p( &img ); foreach ( QGraphicsItem * item, itemsByZ ) { if ( item->isVisible() ) { p.save(); p.setTransform( item->deviceTransform( p.worldTransform() ), false ); item->paint( &p, nullptr ); p.restore(); } } p.end(); img = img.scaled( previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation ); QImage img2( previewSize, QImage::Format_ARGB32 ); img2.fill( Qt::transparent ); QPainter p2( &img2 ); p2.drawImage( (img2.width() - img.width()) / 2, (img2.height() - img.height()) / 2, img ); p2.end(); return img2; } void DealerScene::mapOldId( int id ) { Q_UNUSED( id ); } int DealerScene::oldId() const { return gameId(); } QString DealerScene::getGameState() const { return QString(); } void DealerScene::setGameState( const QString & state ) { Q_UNUSED( state ); } QString DealerScene::getGameOptions() const { return QString(); } void DealerScene::setGameOptions( const QString & options ) { Q_UNUSED( options ); } bool DealerScene::allowedToStartNewGame() { // Check if the user is already running a game, and if she is, // then ask if she wants to abort it. return !m_dealStarted || m_dealWasJustSaved || m_toldAboutWonGame || m_toldAboutLostGame || KMessageBox::warningContinueCancel(nullptr, i18n("A new game has been requested, but there is already a game in progress.\n\n" "A loss will be recorded in the statistics if the current game is abandoned."), i18n("Abandon Current Game?"), KGuiItem(i18n("Abandon Current Game")), KStandardGuiItem::cancel(), QStringLiteral("careaboutstats") ) == KMessageBox::Continue; } void DealerScene::addCardForDeal( KCardPile * pile, KCard * card, bool faceUp, QPointF startPos ) { Q_ASSERT( card ); Q_ASSERT( pile ); card->setFaceUp( faceUp ); pile->add( card ); m_initDealPositions.insert( card, startPos ); } void DealerScene::startDealAnimation() { qreal speed = sqrt( width() * width() + height() * height() ) / ( DURATION_DEAL ); foreach ( PatPile * p, patPiles() ) { updatePileLayout( p, 0 ); foreach ( KCard * c, p->cards() ) { if ( !m_initDealPositions.contains( c ) ) continue; QPointF pos2 = c->pos(); c->setPos( m_initDealPositions.value( c ) ); QPointF delta = c->pos() - pos2; qreal dist = sqrt( delta.x() * delta.x() + delta.y() * delta.y() ); int duration = qRound( dist / speed ); c->animate( pos2, c->zValue(), 0, c->isFaceUp(), false, duration ); } } m_initDealPositions.clear(); } void DealerScene::multiStepMove( const QList & cards, KCardPile * pile, const QList & freePiles, const QList & freeCells, int duration ) { Q_ASSERT( cards.size() == 1 || !freePiles.isEmpty() || !freeCells.isEmpty() ); m_multiStepMoves.clear(); m_multiStepDuration = duration; multiStepSubMove( cards, pile, freePiles, freeCells ); continueMultiStepMove(); } void DealerScene::multiStepSubMove( QList cards, KCardPile * pile, QList freePiles, const QList & freeCells ) { // Note that cards and freePiles are passed by value, as we need to make a // local copy anyway. // Using n free cells, we can move a run of n+1 cards. If we want to move // more than that, we have to recursively move some of our cards to one of // the free piles temporarily. const int freeCellsPlusOne = freeCells.size() + 1; int cardsToSubMove = cards.size() - freeCellsPlusOne; QList > > tempMoves; while ( cardsToSubMove > 0 ) { int tempMoveSize; if ( cardsToSubMove <= freePiles.size() * freeCellsPlusOne ) { // If the cards that have to be submoved can be spread across the // the free piles without putting more than freeCellsPlusOne cards // on each one, we do so. This means that none of our submoves will // need further submoves, which keeps the total move count down. We // Just to a simple rounding up integer division. tempMoveSize = (cardsToSubMove + freePiles.size() - 1) / freePiles.size(); } else { // Otherwise, we use the space optimal method that gets the cards // moved using a minimal number of piles, but uses more submoves. tempMoveSize = freeCellsPlusOne; while ( tempMoveSize * 2 < cardsToSubMove ) tempMoveSize *= 2; } QList subCards; for ( int i = 0; i < tempMoveSize; ++i ) subCards.prepend( cards.takeLast() ); Q_ASSERT( !freePiles.isEmpty() ); KCardPile * nextPile = freePiles.takeFirst(); tempMoves << qMakePair( nextPile, subCards ); multiStepSubMove( subCards, nextPile, freePiles, freeCells ); cardsToSubMove -= tempMoveSize; } // Move cards to free cells. for ( int i = 0; i < cards.size() - 1; ++i ) { KCard * c = cards.at( cards.size() - 1 - i ); m_multiStepMoves << qMakePair( c, freeCells[i] ); } // Move bottom card to destination pile. m_multiStepMoves << qMakePair( cards.first(), pile ); // Move cards from free cells to destination pile. for ( int i = 1; i < cards.size(); ++i ) m_multiStepMoves << qMakePair( cards.at( i ), pile ); // If we just moved the bottomost card of the source pile, it must now be // empty and we won't need it any more. So we return it to the list of free // piles. KCardPile * sourcePile = cards.first()->pile(); if ( sourcePile->at( 0 ) == cards.first() ) freePiles << sourcePile; // If we had to do any submoves, we now move those cards from their // temporary pile to the destination pile and free up their temporary pile. while ( !tempMoves.isEmpty() ) { QPair > m = tempMoves.takeLast(); multiStepSubMove( m.second, pile, freePiles, freeCells ); freePiles << m.first; } } void DealerScene::continueMultiStepMove() { Q_ASSERT( !m_multiStepMoves.isEmpty() ); Q_ASSERT( !isCardAnimationRunning() ); QPair m = m_multiStepMoves.takeFirst(); KCard * card = m.first; KCardPile * dest = m.second; KCardPile * source = card->pile(); Q_ASSERT( card == source->topCard() ); Q_ASSERT( allowedToAdd( dest, QList() << card ) ); m_multiStepDuration = qMax( m_multiStepDuration * 0.9, 50 ); dest->add( card ); card->raise(); updatePileLayout( dest, m_multiStepDuration ); updatePileLayout( source, m_multiStepDuration ); if ( m_multiStepMoves.isEmpty() ) takeState(); } #include "dealer.moc" #include "moc_dealer.cpp" diff --git a/mainwindow.cpp b/mainwindow.cpp index ab430e6..7226fe0 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,1081 +1,1081 @@ /* * Copyright (C) 1995 Paul Olav Tvete * Copyright (C) 1997 Mario Weilguni * Copyright (C) 2000-2009 Stephan Kulow * Copyright (C) 2009-2010 Parker Coates * * License of original code: * ------------------------------------------------------------------------- * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose and without fee is hereby granted, * provided that the above copyright notice appear in all copies and that * both that copyright notice and this permission notice appear in * supporting documentation. * * This file is provided AS IS with no warranties of any kind. The author * shall have no liability with respect to the infringement of copyrights, * trade secrets or any patents by this file or any part thereof. In no * event will the author be liable for any lost revenue or profits or * other special, indirect and consequential damages. * ------------------------------------------------------------------------- * * License of modifications/additions made after 2009-01-01: * ------------------------------------------------------------------------- * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ------------------------------------------------------------------------- */ #include "mainwindow.h" #include "dealer.h" #include "dealerinfo.h" #include "gameselectionscene.h" #include "kpat_debug.h" #include "numbereddealdialog.h" #include "renderer.h" #include "settings.h" #include "soundengine.h" #include "statisticsdialog.h" #include "view.h" #include "KCardTheme" #include "KCardThemeWidget" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { const QUrl dialogUrl( QStringLiteral("kfiledialog:///kpat") ); const QString saveFileMimeType( QStringLiteral("application/vnd.kde.kpatience.savedgame") ); const QString legacySaveFileMimeType( QStringLiteral("application/vnd.kde.kpatience.savedstate") ); } MainWindow::MainWindow() : KXmlGuiWindow( nullptr ), m_view( nullptr ), m_dealer( nullptr ), m_selector( nullptr ), m_cardDeck( nullptr ), m_soundEngine( nullptr ), m_dealDialog( nullptr ) { setObjectName( QStringLiteral( "MainWindow" ) ); // KCrash::setEmergencySaveFunction(::saveGame); setupActions(); foreach( const DealerInfo * di, DealerInfoList::self()->games() ) { m_dealer_map.insert( di->baseId(), di ); foreach( int id, di->subtypeIds() ) m_dealer_map.insert( id, di ); } m_dealer_it = m_dealer_map.constEnd(); m_view = new PatienceView( this ); setCentralWidget( m_view ); const QRect screenSize = QApplication::desktop()->screenGeometry(this); QSize defaultSize = screenSize.size() * 0.7; setupGUI(defaultSize, Create | Save | ToolBar | StatusBar | Keys); m_solverStatusLabel = new QLabel(QString(), statusBar()); m_solverStatusLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); statusBar()->addWidget( m_solverStatusLabel, 1); m_moveCountStatusLabel = new QLabel(QString(), statusBar()); m_moveCountStatusLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); statusBar()->addWidget( m_moveCountStatusLabel, 0); m_showMenubarAction->setChecked(!menuBar()->isHidden()); } MainWindow::~MainWindow() { m_recentFilesAction->saveEntries(KSharedConfig::openConfig()->group( QString() )); Settings::self()->save(); delete m_dealer; delete m_view; Renderer::deleteSelf(); } void MainWindow::setupActions() { QAction *a; // Game Menu a = actionCollection()->addAction( QStringLiteral( "new_game" )); a->setText(i18nc("Start a new game of a different type","New &Game...")); a->setIcon( QIcon::fromTheme( QStringLiteral( "document-new" )) ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::SHIFT + Qt::Key_N); connect(a, &QAction::triggered, this, &MainWindow::slotShowGameSelectionScreen); a = actionCollection()->addAction( QStringLiteral( "new_deal" )); a->setText(i18nc("Start a new game of without changing the game type", "New &Deal")); a->setIcon( QIcon::fromTheme( QStringLiteral( "document-new" )) ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_N); connect(a, &QAction::triggered, this, &MainWindow::newGame); a = actionCollection()->addAction( QStringLiteral( "new_numbered_deal" )); a->setText(i18nc("Start a game by giving its particular number", "New &Numbered Deal...")); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_D); connect(a, &QAction::triggered, this, &MainWindow::newNumberedDeal); a = KStandardGameAction::restart(this, SLOT(restart()), actionCollection()); a->setText(i18nc("Replay the current deal from the start", "Restart Deal")); // Note that this action is not shown in the menu or toolbar. It is // only provided for advanced users who can use it by shorcut or add it to // the toolbar if they wish. a = actionCollection()->addAction( QStringLiteral( "next_deal" )); a->setText(i18nc("Start the game with the number one greater than the current one", "Next Deal")); a->setIcon( QIcon::fromTheme( QStringLiteral( "go-next" )) ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Plus); connect(a, &QAction::triggered, this, &MainWindow::nextDeal); // Note that this action is not shown in the menu or toolbar. It is // only provided for advanced users who can use it by shorcut or add it to // the toolbar if they wish. a = actionCollection()->addAction( QStringLiteral( "previous_deal" )); a->setText(i18nc("Start the game with the number one less than the current one", "Previous Deal")); a->setIcon( QIcon::fromTheme( QStringLiteral( "go-previous" )) ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Minus); connect(a, &QAction::triggered, this, &MainWindow::previousDeal); KStandardGameAction::load( this, SLOT(loadGame()), actionCollection() ); m_recentFilesAction = KStandardGameAction::loadRecent( this, SLOT(loadGame(QUrl)), actionCollection() ); m_recentFilesAction->loadEntries(KSharedConfig::openConfig()->group( QString() )); m_saveAction = KStandardGameAction::saveAs(this, SLOT(saveGame()), actionCollection()); actionCollection()->setDefaultShortcut(m_saveAction, Qt::CTRL + Qt::Key_S); a = actionCollection()->addAction( QStringLiteral( "game_stats" )); a->setText(i18n("Statistics")); a->setIcon( QIcon::fromTheme( QStringLiteral( "games-highscores" )) ); connect(a, &QAction::triggered, this, &MainWindow::showStats); KStandardGameAction::quit(this, SLOT(close()), actionCollection()); // Move Menu m_undoAction = KStandardGameAction::undo(this, SLOT(undoMove()), actionCollection()); m_redoAction = KStandardGameAction::redo(this, SLOT(redoMove()), actionCollection()); m_demoAction = KStandardGameAction::demo( this, SLOT(toggleDemo()), actionCollection() ); // KStandardGameAction::hint is a regular action, but we want a toggle // action, so we must create a new action and copy all the standard // properties over one by one. m_hintAction = new KToggleAction( actionCollection() ); a = KStandardGameAction::hint( nullptr, nullptr, nullptr ); m_hintAction->setText( a->text() ); m_hintAction->setIcon( a->icon() ); actionCollection()->setDefaultShortcut( m_hintAction, a->shortcut() ); m_hintAction->setToolTip( a->toolTip() ); m_hintAction->setWhatsThis( a->whatsThis() ); delete a; QString actionName( KStandardGameAction::name( KStandardGameAction::Hint ) ); actionCollection()->addAction( actionName, m_hintAction ); connect(m_hintAction, &QAction::triggered, this, &MainWindow::toggleHints); m_drawAction = actionCollection()->addAction( QStringLiteral( "move_draw" )); m_drawAction->setText( i18nc("Take one or more cards from the deck, flip them, and place them in play", "Dra&w") ); m_drawAction->setIcon( QIcon::fromTheme( QStringLiteral( "kpat" )) ); actionCollection()->setDefaultShortcut(m_drawAction, Qt::Key_Tab ); m_dealAction = actionCollection()->addAction( QStringLiteral( "move_deal" )); m_dealAction->setText( i18nc("Deal a new row of cards from the deck", "Dea&l Row") ); m_dealAction->setIcon( QIcon::fromTheme( QStringLiteral( "kpat" )) ); actionCollection()->setDefaultShortcut(m_dealAction, Qt::Key_Return ); m_redealAction = actionCollection()->addAction( QStringLiteral( "move_redeal" )); m_redealAction->setText( i18nc("Collect the cards in play, shuffle them and redeal them", "&Redeal") ); m_redealAction->setIcon( QIcon::fromTheme( QStringLiteral( "roll" )) ); actionCollection()->setDefaultShortcut(m_redealAction, Qt::Key_R ); m_dropAction = new KToggleAction( actionCollection() ); m_dropAction->setText( i18nc("Automatically move cards to the foundation piles", "Dro&p") ); m_dropAction->setIcon( QIcon::fromTheme( QStringLiteral( "games-endturn" )) ); actionCollection()->setDefaultShortcut(m_dropAction, Qt::Key_P ); actionCollection()->addAction( QStringLiteral( "move_drop" ), m_dropAction ); connect(m_dropAction, &QAction::triggered, this, &MainWindow::toggleDrop); // Settings Menu a = actionCollection()->addAction( QStringLiteral( "select_deck" )); a->setText(i18n("Change Appearance...")); connect(a, &QAction::triggered, this, &MainWindow::configureAppearance); actionCollection()->setDefaultShortcut(a, Qt::Key_F10 ); m_autoDropEnabledAction = new KToggleAction(i18n("&Enable Autodrop"), this); actionCollection()->addAction( QStringLiteral( "enable_autodrop" ), m_autoDropEnabledAction ); connect(m_autoDropEnabledAction, &KToggleAction::triggered, this, &MainWindow::setAutoDropEnabled); m_autoDropEnabledAction->setChecked( Settings::autoDropEnabled() ); m_solverEnabledAction = new KToggleAction(i18n("E&nable Solver"), this); actionCollection()->addAction( QStringLiteral( "enable_solver" ), m_solverEnabledAction ); connect(m_solverEnabledAction, &KToggleAction::triggered, this, &MainWindow::enableSolver); m_solverEnabledAction->setChecked( Settings::solverEnabled() ); m_playSoundsAction = new KToggleAction( QIcon::fromTheme( QStringLiteral( "preferences-desktop-sound") ), i18n("Play &Sounds" ), this ); actionCollection()->addAction( QStringLiteral( "play_sounds" ), m_playSoundsAction ); connect(m_playSoundsAction, &KToggleAction::triggered, this, &MainWindow::enableSounds); m_playSoundsAction->setChecked( Settings::playSounds() ); m_rememberStateAction = new KToggleAction(i18n("&Remember State on Exit"), this); actionCollection()->addAction( QStringLiteral( "remember_state" ), m_rememberStateAction ); connect(m_rememberStateAction, &KToggleAction::triggered, this, &MainWindow::enableRememberState); m_rememberStateAction->setChecked( Settings::rememberStateOnExit() ); // Help Menu m_gameHelpAction = actionCollection()->addAction( QStringLiteral( "help_game" )); m_gameHelpAction->setIcon( QIcon::fromTheme( QStringLiteral( "help-browser" )) ); connect(m_gameHelpAction, &QAction::triggered, this, &MainWindow::helpGame); actionCollection()->setDefaultShortcut(m_gameHelpAction, Qt::CTRL + Qt::SHIFT + Qt::Key_F1 ); // Hidden actions if (!qEnvironmentVariableIsEmpty("KDE_DEBUG")) // developer shortcut { a = actionCollection()->addAction( QStringLiteral( "themePreview" )); a->setText(i18n("Generate a theme preview image")); connect(a, &QAction::triggered, this, &MainWindow::generateThemePreview); actionCollection()->setDefaultShortcut(a, Qt::Key_F7 ); a = actionCollection()->addAction( QStringLiteral( "snapshot" )); a->setText(i18n("Take Game Preview Snapshots")); connect(a, &QAction::triggered, this, &MainWindow::slotSnapshot); actionCollection()->setDefaultShortcut(a, Qt::Key_F8); a = actionCollection()->addAction( QStringLiteral( "random_set" )); a->setText(i18n("Random Cards")); connect(a, &QAction::triggered, this, &MainWindow::slotPickRandom); actionCollection()->setDefaultShortcut(a, Qt::Key_F9); } // Keyboard navigation actions m_leftAction = actionCollection()->addAction( QStringLiteral( "focus_left" )); m_leftAction->setText(i18n("Move Focus to Previous Pile")); actionCollection()->setDefaultShortcut(m_leftAction, Qt::Key_Left ); m_rightAction = actionCollection()->addAction( QStringLiteral( "focus_right" )); m_rightAction->setText(i18n("Move Focus to Next Pile")); actionCollection()->setDefaultShortcut(m_rightAction, Qt::Key_Right ); m_upAction = actionCollection()->addAction( QStringLiteral( "focus_up" )); m_upAction->setText(i18n("Move Focus to Card Below")); actionCollection()->setDefaultShortcut(m_upAction, Qt::Key_Up ); m_downAction = actionCollection()->addAction( QStringLiteral( "focus_down" )); m_downAction->setText(i18n("Move Focus to Card Above")); actionCollection()->setDefaultShortcut(m_downAction, Qt::Key_Down ); m_cancelAction = actionCollection()->addAction( QStringLiteral( "focus_cancel" )); m_cancelAction->setText(i18n("Cancel Focus")); actionCollection()->setDefaultShortcut(m_cancelAction, Qt::Key_Escape ); m_pickUpSetDownAction = actionCollection()->addAction( QStringLiteral( "focus_activate" )); m_pickUpSetDownAction->setText(i18n("Pick Up or Set Down Focus")); actionCollection()->setDefaultShortcut(m_pickUpSetDownAction, Qt::Key_Space ); // showMenubar isn't a part of KStandardGameAction m_showMenubarAction = KStandardAction::showMenubar(this, SLOT(toggleMenubar()), actionCollection()); } void MainWindow::undoMove() { if( m_dealer ) m_dealer->undo(); } void MainWindow::redoMove() { if( m_dealer ) m_dealer->redo(); } void MainWindow::helpGame() { if (m_dealer && m_dealer_map.contains(m_dealer->gameId())) { const DealerInfo * di = m_dealer_map.value(m_dealer->gameId()); QString anchor = QString::fromUtf8( di->untranslatedBaseName() ); anchor = anchor.toLower(); anchor = anchor.remove('\'').replace('&', QLatin1String("and")).replace(' ', '-'); KHelpClient::invokeHelp(QLatin1String("rules-specific.html#") + anchor); } } void MainWindow::setAutoDropEnabled( bool enabled ) { Settings::setAutoDropEnabled( enabled ); if ( m_dealer ) { m_dealer->setAutoDropEnabled( enabled ); if ( enabled ) m_dealer->startDrop(); else m_dealer->stopDrop(); } updateGameActionList(); } void MainWindow::enableSolver(bool enable) { Settings::setSolverEnabled( enable ); m_solverStatusLabel->setText( QString() ); if (m_dealer) { m_dealer->setSolverEnabled(enable); if(enable) m_dealer->startSolver(); } } void MainWindow::enableSounds( bool enable ) { Settings::setPlaySounds( enable ); updateSoundEngine(); } void MainWindow::enableRememberState(bool enable) { Settings::setRememberStateOnExit( enable ); } void MainWindow::newGame() { if (m_dealer && m_dealer->allowedToStartNewGame()) startRandom(); } void MainWindow::restart() { startNew(-1); } void MainWindow::startRandom() { const auto seed = QRandomGenerator::global()->generate(); const int signed_seed = (int)(seed & (~(((quint32)1) << 31))); startNew(signed_seed); } void MainWindow::startNew(int gameNumber) { m_dealer->startNew(gameNumber); setGameCaption(); } void MainWindow::slotPickRandom() { QList themes = KCardTheme::findAll(); - KCardTheme theme = themes.at( QRandomGenerator::global()->generate() % themes.size() ); + KCardTheme theme = themes.at( QRandomGenerator::global()->bounded( themes.size() ) ); Settings::setCardTheme( theme.dirName() ); appearanceChanged(); } void MainWindow::configureAppearance() { const QString previewFormat = QStringLiteral("back;10_spade,jack_diamond,queen_club,king_heart;1_spade"); const QSet features = QSet() << QStringLiteral("AngloAmerican") << QStringLiteral("Backs1"); if ( !KConfigDialog::showDialog(QStringLiteral("KPatAppearanceDialog")) ) { KConfigDialog * dialog = new KConfigDialog( this, QStringLiteral("KPatAppearanceDialog"), Settings::self() ); dialog->addPage( new KCardThemeWidget( features, previewFormat ), i18n("Card Deck"), QStringLiteral("games-config-theme"), i18n("Select a card deck") ); KgThemeProvider* provider = Renderer::self()->themeProvider(); dialog->addPage( new KgThemeSelector(provider), i18n("Game Theme"), QStringLiteral("games-config-theme"), i18n("Select a theme for non-card game elements") ); connect(provider, &KgThemeProvider::currentThemeChanged, this, &MainWindow::appearanceChanged); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::appearanceChanged); dialog->show(); } } void MainWindow::appearanceChanged() { if ( m_cardDeck && Settings::cardTheme() != m_cardDeck->theme().dirName() ) { KCardTheme theme( Settings::cardTheme() ); if ( theme.isValid() ) { m_cardDeck->setTheme( KCardTheme( theme ) ); if ( m_dealer ) m_dealer->relayoutScene(); } } } void MainWindow::setGameCaption() { QString caption; if ( m_dealer ) { const DealerInfo * di = m_dealer_map.value( m_dealer->gameId() ); caption = QStringLiteral("%1 - %2").arg( di->baseName() ).arg(m_dealer->gameNumber()); } setCaption( caption ); } void MainWindow::slotGameSelected(int id) { if ( m_dealer_map.contains(id) ) { setGameType( id ); QTimer::singleShot(0 , this, &MainWindow::startRandom ); } } void MainWindow::setGameType(int id) { // Only bother calling creating a new DealerScene if we don't already have // the right DealerScene open. if ( m_dealer && m_dealer_map.value( id ) == m_dealer_map.value( m_dealer->gameId() ) ) { m_dealer->recordGameStatistics(); m_dealer->mapOldId( id ); return; } // If we're replacing an existing DealerScene, record the stats of the // game already in progress. if ( m_dealer ) { m_dealer->recordGameStatistics(); delete m_dealer; m_view->setScene(nullptr); m_dealer = nullptr; } if ( !m_cardDeck ) { KCardTheme theme = KCardTheme( Settings::cardTheme() ); if ( !theme.isValid() ) theme = KCardTheme( Settings::defaultCardThemeValue() ); m_cardDeck = new KCardDeck( theme, this ); } const DealerInfo * di = m_dealer_map.value(id, DealerInfoList::self()->games().first()); m_dealer = di->createGame(); m_dealer->setDeck( m_cardDeck ); m_dealer->initialize(); m_dealer->mapOldId( id ); m_dealer->setSolverEnabled( m_solverEnabledAction->isChecked() ); m_dealer->setAutoDropEnabled( m_autoDropEnabledAction->isChecked() ); m_view->setScene( m_dealer ); m_gameHelpAction->setText(i18nc("Is disabled and changes to \"Help &with Current Game\" when" " there is no current game.", "Help &with %1", di->baseName().replace('&', QLatin1String("&&")))); connect(m_dealer, &DealerScene::solverStateChanged, this, &MainWindow::updateSolverDescription); connect(m_dealer, &DealerScene::updateMoves, this, &MainWindow::slotUpdateMoves); m_solverStatusLabel->setText(QString()); m_solverStatusLabel->setVisible(true); m_moveCountStatusLabel->setText(QString()); m_moveCountStatusLabel->setVisible(true); updateActions(); updateSoundEngine(); } void MainWindow::slotShowGameSelectionScreen() { if (!m_dealer || m_dealer->allowedToStartNewGame()) { if (m_dealer) { m_dealer->recordGameStatistics(); delete m_dealer; m_view->setScene(nullptr); m_dealer = nullptr; } if (!m_selector) { m_selector = new GameSelectionScene(this); connect(m_selector, &GameSelectionScene::gameSelected, this, &MainWindow::slotGameSelected); } m_view->setScene(m_selector); m_gameHelpAction->setText(i18nc("Shown when there is no game open. Is always disabled.", "Help &with Current Game")); updateActions(); setGameCaption(); m_solverStatusLabel->setVisible(false); m_moveCountStatusLabel->setVisible(false); } } void MainWindow::updateActions() { // Enable/disable application actions that aren't appropriate on game // selection screen. actionCollection()->action( QStringLiteral("new_game") )->setEnabled( m_dealer ); actionCollection()->action( QStringLiteral("new_deal") )->setEnabled( m_dealer ); actionCollection()->action( QStringLiteral("game_restart") )->setEnabled( m_dealer ); actionCollection()->action( QStringLiteral("game_save_as") )->setEnabled( m_dealer ); m_gameHelpAction->setEnabled( m_dealer ); // Initially disable game actions. They'll be reenabled through signals // if/when appropriate. m_undoAction->setEnabled( false ); m_redoAction->setEnabled( false ); m_hintAction->setEnabled( false ); m_demoAction->setEnabled( false ); m_dropAction->setEnabled( false ); m_drawAction->setEnabled( false ); m_dealAction->setEnabled( false ); m_redealAction->setEnabled( false ); // If a dealer exists, connect the game actions to it. if ( m_dealer ) { connect(m_dealer, &DealerScene::undoPossible, m_undoAction, &QAction::setEnabled); connect(m_dealer, &DealerScene::redoPossible, m_redoAction, &QAction::setEnabled); connect(m_dealer, &DealerScene::hintActive, m_hintAction, &QAction::setChecked); connect(m_dealer, &DealerScene::gameInProgress, m_hintAction, &QAction::setEnabled); connect(m_dealer, &DealerScene::demoActive, this, &MainWindow::toggleDemoAction); connect(m_dealer, &DealerScene::gameInProgress, m_demoAction, &QAction::setEnabled); connect(m_dealer, &DealerScene::dropActive, m_dropAction, &QAction::setChecked); connect(m_dealer, &DealerScene::gameInProgress, m_dropAction, &QAction::setEnabled); connect(m_dealer, &DealerScene::gameInProgress, m_saveAction, &QAction::setEnabled); connect(m_leftAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusLeft); connect(m_rightAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusRight); connect(m_upAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusUp); connect(m_downAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusDown); connect(m_cancelAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusCancel); connect(m_pickUpSetDownAction, &QAction::triggered, m_dealer, &DealerScene::keyboardFocusSelect); if ( m_dealer->actions() & DealerScene::Draw ) { connect(m_drawAction, &QAction::triggered, m_dealer, &DealerScene::drawDealRowOrRedeal); connect(m_dealer, &DealerScene::newCardsPossible, m_drawAction, &QAction::setEnabled); } else if ( m_dealer->actions() & DealerScene::Deal ) { connect(m_dealAction, &QAction::triggered, m_dealer, &DealerScene::drawDealRowOrRedeal); connect(m_dealer, &DealerScene::newCardsPossible, m_dealAction, &QAction::setEnabled); } else if ( m_dealer->actions() & DealerScene::Redeal ) { connect(m_redealAction, &QAction::triggered, m_dealer, &DealerScene::drawDealRowOrRedeal); connect(m_dealer, &DealerScene::newCardsPossible, m_redealAction, &QAction::setEnabled); } guiFactory()->unplugActionList( this, QStringLiteral("dealer_options") ); guiFactory()->plugActionList( this, QStringLiteral("dealer_options"), m_dealer->configActions() ); } updateGameActionList(); } void MainWindow::updateGameActionList() { guiFactory()->unplugActionList( this, QStringLiteral("game_actions") ); m_dropAction->setEnabled( m_dealer && !m_dealer->autoDropEnabled() ); if ( m_dealer ) { QList actionList; if ( m_dealer->actions() & DealerScene::Hint ) actionList.append( m_hintAction ); if ( m_dealer->actions() & DealerScene::Demo ) actionList.append( m_demoAction ); if ( m_dealer->actions() & DealerScene::Draw ) actionList.append( m_drawAction ); if ( m_dealer->actions() & DealerScene::Deal ) actionList.append( m_dealAction ); if ( m_dealer->actions() & DealerScene::Redeal ) actionList.append( m_redealAction ); if ( !m_dealer->autoDropEnabled() ) actionList.append( m_dropAction ); guiFactory()->plugActionList( this, QStringLiteral("game_actions"), actionList ); } } void MainWindow::updateSoundEngine() { if ( m_dealer ) { if ( Settings::playSounds() ) { if ( !m_soundEngine ) m_soundEngine = new SoundEngine( this ); connect(m_dealer, &DealerScene::cardsPickedUp, m_soundEngine, &SoundEngine::cardsPickedUp); connect(m_dealer, &DealerScene::cardsPutDown, m_soundEngine, &SoundEngine::cardsPutDown); } else if ( m_soundEngine ) { disconnect( m_dealer, nullptr, m_soundEngine, nullptr ); } } } void MainWindow::toggleDrop() { if ( m_dealer ) { if ( !m_dealer->isDropActive() ) m_dealer->startDrop(); else m_dealer->stopDrop(); } } void MainWindow::toggleHints() { if ( m_dealer ) { if ( !m_dealer->isHintActive() ) m_dealer->startHint(); else m_dealer->stop(); } } void MainWindow::toggleDemo() { if ( m_dealer ) { if ( !m_dealer->isDemoActive() ) m_dealer->startDemo(); else m_dealer->stop(); } } void MainWindow::toggleDemoAction(bool active) { m_demoAction->setChecked( active ); m_demoAction->setIcon( QIcon::fromTheme( QLatin1String( active ? "media-playback-pause" : "media-playback-start" ) ) ); } void MainWindow::toggleMenubar() { if(m_showMenubarAction->isChecked()) menuBar()->show(); else if(KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to hide the menubar? The current shortcut to show it again is %1.", m_showMenubarAction->shortcut().toString(QKeySequence::NativeText)), i18n("Hide Menubar"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("MenubarWarning")) == KMessageBox::Continue) menuBar()->hide(); else m_showMenubarAction->setChecked(true); } void MainWindow::saveNewToolbarConfig() { KXmlGuiWindow::saveNewToolbarConfig(); updateGameActionList(); } void MainWindow::closeEvent(QCloseEvent *e) { QString stateDirName = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QString stateFileName = stateDirName + QLatin1Char('/') + saved_state_file ; QDir stateFileDir(stateDirName); if(!stateFileDir.exists()) { //create the directory if it doesn't exist (bug#350160) stateFileDir.mkpath(QStringLiteral(".")); } QFile stateFile( stateFileName ); // Remove the existing state file, if any. stateFile.remove(); if ( m_dealer ) { if ( Settings::rememberStateOnExit() && !m_dealer->isGameWon() ) { stateFile.open( QFile::WriteOnly | QFile::Truncate ); m_dealer->saveFile( &stateFile ); } else { // If there's a game in progress and we aren't going to save it // then record its statistics, since the DealerScene will be destroyed // shortly. m_dealer->recordGameStatistics(); } } KXmlGuiWindow::closeEvent(e); } void MainWindow::newNumberedDeal() { if ( !m_dealDialog ) { m_dealDialog = new NumberedDealDialog( this ); connect(m_dealDialog, &NumberedDealDialog::dealChosen, this, &MainWindow::startNumbered); } if ( m_dealer ) { m_dealDialog->setGameType( m_dealer->oldId() ); m_dealDialog->setDealNumber( m_dealer->gameNumber() ); } m_dealDialog->show(); } void MainWindow::startNumbered( int gameId, int dealNumber ) { if ( !m_dealer || m_dealer->allowedToStartNewGame() ) { setGameType( gameId ); startNew( dealNumber ); } } void MainWindow::nextDeal() { if ( !m_dealer ) newNumberedDeal(); else if ( m_dealer->allowedToStartNewGame() ) startNew( m_dealer->gameNumber() == INT_MAX ? 1 : m_dealer->gameNumber() + 1 ); } void MainWindow::previousDeal() { if ( !m_dealer ) newNumberedDeal(); else if ( m_dealer->allowedToStartNewGame() ) startNew( m_dealer->gameNumber() == 1 ? INT_MAX : m_dealer->gameNumber() - 1 ); } bool MainWindow::loadGame( const QUrl & url, bool addToRecentFiles ) { KIO::StoredTransferJob *job = KIO::storedGet( url ); if( !job->exec() ) { KMessageBox::error( this, i18n("Downloading file failed: %1", job->errorString()) ); return false; } QXmlStreamReader xml( job->data() ); if ( !xml.readNextStartElement() ) { KMessageBox::error( this, i18n("Error reading XML file: ") + xml.errorString() ); return false; } int gameId = -1; bool isLegacyFile; if ( xml.name() == "dealer" ) { isLegacyFile = true; bool ok; int id = xml.attributes().value(QStringLiteral("id")).toString().toInt( &ok ); if ( ok ) gameId = id; } else if ( xml.name() == "kpat-game" ) { isLegacyFile = false; QStringRef gameType = xml.attributes().value(QStringLiteral("game-type")); foreach ( const DealerInfo * di, DealerInfoList::self()->games() ) { if ( di->baseIdString() == gameType ) { gameId = di->baseId(); break; } } } else { KMessageBox::error( this, i18n("XML file is not a KPat save.") ); return false; } if ( !m_dealer_map.contains( gameId ) ) { KMessageBox::error( this, i18n("Unrecognized game id.") ); return false; } // Only bother the user to ask for permission after we've determined the // save game file is at least somewhat valid. if ( m_dealer && !m_dealer->allowedToStartNewGame() ) return false; setGameType( gameId ); xml.clear(); QBuffer buffer; buffer.setData( job->data() ); buffer.open( QBuffer::ReadOnly ); bool success = isLegacyFile ? m_dealer->loadLegacyFile( &buffer ) : m_dealer->loadFile( &buffer ); if ( !success ) { KMessageBox::error( this, i18n("Errors encountered while parsing file.") ); slotShowGameSelectionScreen(); return false; } setGameCaption(); if ( addToRecentFiles ) m_recentFilesAction->addUrl( url ); return true; } void MainWindow::loadGame() { QPointer dialog = new QFileDialog(this); dialog->selectUrl(dialogUrl); dialog->setAcceptMode( QFileDialog::AcceptOpen ); dialog->setMimeTypeFilters( QStringList() << saveFileMimeType << legacySaveFileMimeType << QStringLiteral("all/allfiles") ); dialog->setWindowTitle( i18n("Load") ); if ( dialog->exec() == QFileDialog::Accepted ) { if ( dialog ) { QUrl url = dialog->selectedUrls().at(0); if ( !url.isEmpty() ) loadGame( url, true ); } } delete dialog; } void MainWindow::saveGame() { if ( !m_dealer ) return; QPointer dialog = new QFileDialog( this ); dialog->selectUrl(dialogUrl); dialog->setAcceptMode( QFileDialog::AcceptSave ); dialog->setMimeTypeFilters( QStringList() << saveFileMimeType << legacySaveFileMimeType ); dialog->setOption(QFileDialog::DontConfirmOverwrite, false); dialog->setWindowTitle( i18n("Save") ); if ( dialog->exec() != QFileDialog::Accepted ) return; QUrl url; if ( dialog ) { url = dialog->selectedUrls().at(0); if ( url.isEmpty() ) return; } QFile localFile; QTemporaryFile tempFile; if ( url.isLocalFile() ) { localFile.setFileName( url.toLocalFile() ); if ( !localFile.open( QFile::WriteOnly ) ) { KMessageBox::error( this, i18n("Error opening file for writing. Saving failed.") ); return; } } else { if ( !tempFile.open() ) { KMessageBox::error( this, i18n("Unable to create temporary file. Saving failed.") ); return; } } QFile & file = url.isLocalFile() ? localFile : tempFile; if ( dialog && dialog->selectedNameFilter() == legacySaveFileMimeType ) { m_dealer->saveLegacyFile( &file ); } else { m_dealer->saveFile( &file ); } file.close(); if ( !url.isLocalFile() ) { tempFile.open(); KIO::StoredTransferJob *job = KIO::storedPut( &tempFile, url, -1 ); if( !job->exec() ) { KMessageBox::error( this, i18n("Error uploading file. Saving failed: %1", job->errorString()) ); return; } } m_recentFilesAction->addUrl( url ); } void MainWindow::showStats() { QPointer dlg = new StatisticsDialog(this); if (m_dealer) dlg->showGameType(m_dealer->oldId()); dlg->exec(); delete dlg; } void MainWindow::updateSolverDescription( const QString & text ) { m_solverStatusLabel->setText( text ); } void MainWindow::slotUpdateMoves(int moves) { m_moveCountStatusLabel->setText(i18np("1 move", "%1 moves", moves)); } void MainWindow::slotSnapshot() { if ( m_dealer_it == m_dealer_map.constEnd() ) { // first call m_dealer_it = m_dealer_map.constBegin(); } setGameType( m_dealer_it.key() ); m_dealer->setAutoDropEnabled( false ); startRandom(); QTimer::singleShot( 1000, this, &MainWindow::slotSnapshot2 ); } void MainWindow::slotSnapshot2() { m_dealer->createDump().save( QStringLiteral( "%1.png" ).arg( m_dealer->gameId() ) ); ++m_dealer_it; if ( m_dealer_it != m_dealer_map.constEnd() ) QTimer::singleShot( 0, this, &MainWindow::slotSnapshot ); } void MainWindow::generateThemePreview() { const QSize previewSize( 240, 160 ); m_view->setMinimumSize( previewSize ); m_view->setMaximumSize( previewSize ); QImage img( m_view->contentsRect().size(), QImage::Format_ARGB32 ); img.fill( Qt::transparent ); QPainter p( &img ); foreach ( KCard * c, m_cardDeck->cards() ) c->completeAnimation(); m_view->render( &p ); slotShowGameSelectionScreen(); QRect leftHalf( 0, 0, m_view->contentsRect().width() / 2, m_view->contentsRect().height() ); m_view->render( &p, leftHalf, leftHalf ); p.end(); img.save( QStringLiteral("preview.png") ); m_view->setMinimumSize( 0, 0 ); m_view->setMaximumSize( QWIDGETSIZE_MAX, QWIDGETSIZE_MAX ); } diff --git a/spider.cpp b/spider.cpp index f6dbc57..42849b8 100644 --- a/spider.cpp +++ b/spider.cpp @@ -1,512 +1,512 @@ /* * Copyright (C) 2003 Josh Metzler * Copyright (C) 2000-2009 Stephan Kulow * Copyright (C) 2010 Parker Coates * * License of original code: * ------------------------------------------------------------------------- * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose and without fee is hereby granted, * provided that the above copyright notice appear in all copies and that * both that copyright notice and this permission notice appear in * supporting documentation. * * This file is provided AS IS with no warranties of any kind. The author * shall have no liability with respect to the infringement of copyrights, * trade secrets or any patents by this file or any part thereof. In no * event will the author be liable for any lost revenue or profits or * other special, indirect and consequential damages. * ------------------------------------------------------------------------- * * License of modifications/additions made after 2009-01-01: * ------------------------------------------------------------------------- * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ------------------------------------------------------------------------- */ #include "spider.h" #include "dealerinfo.h" #include "pileutils.h" #include "settings.h" #include "speeds.h" #include "patsolve/spidersolver.h" #include #include #include class InvisiblePile : public PatPile { public: InvisiblePile( DealerScene * scene, int index, const QString & objectName = QString() ) : PatPile( scene, index, objectName ) { }; protected: void paintGraphic( QPainter * painter, qreal highlightedness ) override { Q_UNUSED( painter ); Q_UNUSED( highlightedness ); }; }; Spider::Spider( const DealerInfo * di ) : DealerScene( di ) { } void Spider::initialize() { m_leg = 0; m_redeal = 0; const qreal dist_x = 1.12; const qreal smallNeg = -1e-6; m_suits = Settings::spiderSuitCount(); m_stackFaceup = Settings::spiderStackFaceup(); createDeck(); // Dealing the cards out into 5 piles so the user can see how many // sets of 10 cards are left to be dealt out for( int column = 0; column < 5; ++column ) { redeals[column] = new InvisiblePile( this, column + 1, QStringLiteral( "redeals%1" ).arg( column ) ); redeals[column]->setPileRole(PatPile::Stock); redeals[column]->setLayoutPos( dist_x * (9 - (4.0 - column) / 3), smallNeg ); redeals[column]->setZValue(12 * ( 5-column )); redeals[column]->setSpread(0, 0); redeals[column]->setKeyboardSelectHint( KCardPile::NeverFocus ); redeals[column]->setKeyboardDropHint( KCardPile::NeverFocus ); connect( redeals[column], &KCardPile::clicked, this, &DealerScene::drawDealRowOrRedeal ); } // The 10 playing piles for( int column = 0; column < 10; ++column ) { stack[column] = new PatPile( this, column + 6, QStringLiteral( "stack%1" ).arg( column ) ); stack[column]->setPileRole(PatPile::Tableau); stack[column]->setLayoutPos(dist_x * column, 0); stack[column]->setZValue(20); stack[column]->setAutoTurnTop(true); stack[column]->setBottomPadding( 1.5 ); stack[column]->setHeightPolicy( KCardPile::GrowDown ); stack[column]->setKeyboardSelectHint( KCardPile::AutoFocusDeepestRemovable ); stack[column]->setKeyboardDropHint( KCardPile::AutoFocusTop ); } // The 8 'legs' so named by me because spiders have 8 legs - why // else the name Spider? for( int column = 0; column < 8; ++column ) { legs[column] = new InvisiblePile( this, column + 16, QStringLiteral( "legs%1" ).arg( column ) ); legs[column]->setPileRole(PatPile::Foundation); legs[column]->setLayoutPos(dist_x / 3 * column, smallNeg); legs[column]->setZValue(column+1); legs[column]->setSpread(0, 0); legs[column]->setZValue(14 * column); legs[column]->setVisible( false ); legs[column]->setKeyboardSelectHint( KCardPile::NeverFocus ); legs[column]->setKeyboardDropHint( KCardPile::NeverFocus ); } // Moving an A-K run to a leg is not really an autoDrop - the // user should have no choice. setAutoDropEnabled(false); setActions(DealerScene::Hint | DealerScene::Demo | DealerScene::Deal); setSolver( new SpiderSolver( this ) ); options = new KSelectAction(i18n("Spider &Options"), this ); options->addAction( i18n("1 Suit (Easy)") ); options->addAction( i18n("2 Suits (Medium)") ); options->addAction( i18n("4 Suits (Hard)") ); if ( m_suits == 1 ) options->setCurrentItem( 0 ); else if ( m_suits == 2 ) options->setCurrentItem( 1 ); else options->setCurrentItem( 2 ); connect(options, static_cast(&KSelectAction::triggered), this, &Spider::gameTypeChanged); m_stackFaceupOption = new KSelectAction(i18n("S&tack Options"), this ); m_stackFaceupOption->addAction( i18n("Face &Down (harder)") ); m_stackFaceupOption->addAction( i18n("Face &Up (easier)") ); m_stackFaceupOption->setCurrentItem( m_stackFaceup ); connect(m_stackFaceupOption, static_cast(&KSelectAction::triggered), this, &Spider::gameTypeChanged); } QList Spider::configActions() const { return QList() << options << m_stackFaceupOption; } void Spider::gameTypeChanged() { stopDemo(); if ( allowedToStartNewGame() ) { if ( options->currentItem() == 0 ) setSuits( 1 ); else if ( options->currentItem() == 1 ) setSuits( 2 ); else setSuits( 4 ); if ( m_stackFaceup != m_stackFaceupOption->currentItem() ) { m_stackFaceup = m_stackFaceupOption->currentItem(); Settings::setSpiderStackFaceup( m_stackFaceup ); } startNew( gameNumber() ); } else { // If we're not allowed, reset the option to // the current number of suits. if ( m_suits == 1 ) options->setCurrentItem( 0 ); else if ( m_suits == 2 ) options->setCurrentItem( 1 ); else options->setCurrentItem( 2 ); m_stackFaceupOption->setCurrentItem( m_stackFaceup ); } } void Spider::setSuits(int suits) { if ( suits != m_suits ) { m_suits = suits; stopDemo(); clearHighlightedItems(); setKeyboardModeActive( false ); int cardWidth = deck()->cardWidth(); createDeck(); deck()->setCardWidth( cardWidth ); Settings::setSpiderSuitCount( m_suits ); if ( m_suits == 1 ) options->setCurrentItem( 0 ); else if ( m_suits == 2 ) options->setCurrentItem( 1 ); else options->setCurrentItem( 2 ); } } void Spider::createDeck() { // These look a bit weird, but are needed to keep the game numbering // from breaking. The original logic always created groupings of 4 // suits, while the new logic is more flexible. We maintain the card // ordering by always passing a list of 4 suits even if we really only // have one or two. QList suits; if ( m_suits == 1 ) suits << KCardDeck::Spades << KCardDeck::Spades << KCardDeck::Spades << KCardDeck::Spades; else if ( m_suits == 2 ) suits << KCardDeck::Hearts << KCardDeck::Spades << KCardDeck::Hearts << KCardDeck::Spades; else suits << KCardDeck::Clubs << KCardDeck::Diamonds << KCardDeck::Hearts << KCardDeck::Spades; setDeckContents( 2, suits ); } bool Spider::checkAdd(const PatPile * pile, const QList & oldCards, const QList & newCards) const { // assuming the cardlist is a valid unit, since I allowed // it to be removed - can drop any card on empty pile or // on any suit card of one higher rank return pile->pileRole() == PatPile::Tableau && ( oldCards.isEmpty() || oldCards.last()->rank() == newCards.first()->rank() + 1 ); } bool Spider::checkRemove(const PatPile * pile, const QList & cards) const { return pile->pileRole() == PatPile::Tableau && isSameSuitDescending(cards); } QString Spider::getGameState() const { return QString::number(m_leg*10 + m_redeal); } void Spider::setGameState( const QString & state ) { int n = state.toInt(); int numLegs = n / 10; int numRedeals = n % 10; if ( numRedeals != m_redeal || numLegs != m_leg ) { m_redeal = numRedeals; for ( int i = 0; i < 5; ++i ) redeals[i]->setVisible( i >= m_redeal ); m_leg = numLegs; for ( int i = 0; i < 8; ++i ) legs[i]->setVisible( i < m_leg ); recalculatePileLayouts(); foreach ( KCardPile * p, piles() ) updatePileLayout( p, 0 ); emit newCardsPossible(m_redeal <= 4); } } QString Spider::getGameOptions() const { return QString::number(m_suits); } void Spider::setGameOptions( const QString & options ) { setSuits(options.toInt()); } void Spider::restart( const QList & cards ) { m_pilesWithRuns.clear(); // make the redeal piles visible for (int i = 0; i < 5; ++i ) redeals[i]->setVisible( true ); // make the leg piles invisible for (int i = 0; i < 8; ++i ) legs[i]->setVisible( false ); recalculatePileLayouts(); m_leg = 0; m_redeal = 0; QList cardList = cards; int column = 0; // deal face down cards (5 to first 4 piles, 4 to last 6) for ( int i = 0; i < 44; ++i ) { addCardForDeal( stack[column], cardList.takeLast(), m_stackFaceup == 1, randomPos() ); column = (column + 1) % 10; } // deal face up cards, one to each pile for ( int i = 0; i < 10; ++i ) { addCardForDeal( stack[column], cardList.takeLast(), true, randomPos() ); column = (column + 1) % 10; } // deal the remaining cards into 5 'redeal' piles for ( int column = 0; column < 5; ++column ) for ( int i = 0; i < 10; ++i ) addCardForDeal( redeals[column], cardList.takeLast(), false, randomPos() ); startDealAnimation(); emit newCardsPossible(true); } void Spider::cardsMoved( const QList & cards, KCardPile * oldPile, KCardPile * newPile ) { PatPile * p = dynamic_cast( newPile ); // The solver treats the removal of complete runs from the table as a // separate move, so we don't do it automatically when the demo is active. if ( !isDemoActive() && p && p->pileRole() == PatPile::Tableau && pileHasFullRun( p ) ) { m_pilesWithRuns << p; } DealerScene::cardsMoved( cards, oldPile, newPile ); } bool Spider::pileHasFullRun( KCardPile * pile ) { QList potentialRun = pile->topCards( 13 ); return pile->count() >= 13 && potentialRun.first()->isFaceUp() && isSameSuitDescending( potentialRun ); } void Spider::moveFullRunToLeg( KCardPile * pile ) { QList run = pile->topCards( 13 ); PatPile * leg = legs[m_leg]; ++m_leg; leg->setVisible( true ); recalculatePileLayouts(); for ( int i = 0; i < 10; ++i ) if ( stack[i] != pile ) updatePileLayout( stack[i], DURATION_RELAYOUT ); for ( int i = 0; i < run.size(); ++i ) { KCard * c = run.at( i ); leg->add( c ); int duration = DURATION_AUTODROP * (0.7 + i / 10.0); c->animate( leg->pos(), leg->zValue() + i, 0, true, true, duration ); } updatePileLayout( pile, DURATION_RELAYOUT ); } QPointF Spider::randomPos() { QRectF rect = sceneRect(); - qreal x = rect.left() + qreal(QRandomGenerator::global()->generate()) / RAND_MAX * (rect.width() - deck()->cardWidth()); - qreal y = rect.top() + qreal(QRandomGenerator::global()->generate()) / RAND_MAX * (rect.height() - deck()->cardHeight()); + qreal x = rect.left() + QRandomGenerator::global()->bounded( rect.width() - deck()->cardWidth() ); + qreal y = rect.top() + QRandomGenerator::global()->bounded( rect.height() - deck()->cardHeight() ); return QPointF( x, y ); } bool Spider::newCards() { // The solver doesn't distinguish between dealing a new row of cards and // removing complete runs from the tableau. So it we're in demo mode and // newCards() is called, we should check to see if there are any complete // runs to move before dealing a new row. if ( isDemoActive() ) { for ( int i = 0; i < 10; ++i ) { if ( pileHasFullRun( stack[i] ) ) { moveFullRunToLeg( stack[i] ); return true; } } } if ( m_redeal > 4 ) return false; redeals[m_redeal]->setVisible(false); recalculatePileLayouts(); for ( int column = 0; column < 10; ++column ) { KCard * c = redeals[m_redeal]->topCard(); if ( !c ) break; flipCardToPileAtSpeed( c, stack[column], DEAL_SPEED ); c->setZValue( c->zValue() + 10 - column ); } ++m_redeal; if (m_redeal > 4) emit newCardsPossible(false); return true; } void Spider::animationDone() { if ( !m_pilesWithRuns.isEmpty() ) moveFullRunToLeg( m_pilesWithRuns.takeFirst() ); else DealerScene::animationDone(); } void Spider::mapOldId(int id) { switch (id) { case DealerInfo::SpiderOneSuitId : setSuits(1); break; case DealerInfo::SpiderTwoSuitId : setSuits(2); break; case DealerInfo::SpiderFourSuitId : setSuits(4); break; } } int Spider::oldId() const { switch (m_suits) { case 1 : return DealerInfo::SpiderOneSuitId; case 2 : return DealerInfo::SpiderTwoSuitId; case 4 : default : return DealerInfo::SpiderFourSuitId; } } static class SpideDealerInfo : public DealerInfo { public: SpideDealerInfo() : DealerInfo(I18N_NOOP("Spider"), SpiderGeneralId) { addSubtype( SpiderOneSuitId, I18N_NOOP( "Spider (1 Suit)" ) ); addSubtype( SpiderTwoSuitId, I18N_NOOP( "Spider (2 Suit)" ) ); addSubtype( SpiderFourSuitId, I18N_NOOP( "Spider (4 Suit)" ) ); } DealerScene *createGame() const override { return new Spider( this ); } } spideDealerInfo;