[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    KDE/kdegames/kpat
From:       Parker Coates <parker.coates () kdemail ! net>
Date:       2012-02-15 4:45:07
Message-ID: 20120215044507.1A928AC895 () svn ! kde ! org
[Download RAW message or body]

SVN commit 1280168 by coates:

Add initial support for saving/loading history to/from file.

Instead of saving the just current state of the game, KPat now saves
the entire history, allowing one to undo/redo in the loaded file. I've
been wanting to do this for a while and finally got it finished.

This is a brand new and entirely separate XML format. KPat can open
both kinds of files and when saving the user can choose either one,
although we default to the new format. The old format will stay in
place for at least a few releases, but for simplicity's sake we might
want to remove support for at some point.

Hopefully it'll improve the quality of bug reports, since the user will
be able to attach the exact series of moves that lead up to a problem.
It also means that people won't lose their undo history when closing
KPat with "Remember State on Exit" checked.

In addition to the new history stuff, I've tried to improve the file
format to make it more human readable and therefore, hopefully more
useful for debugging.

I still have a few changes I want to make to the code and to the save
file format, but this is a decent start that seems to work quite well
already.

DIGEST:

 M  +224 -2    dealer.cpp  
 M  +5 -2      dealer.h  
 M  +3 -3      main.cpp  
 M  +37 -20    mainwindow.cpp  


--- trunk/KDE/kdegames/kpat/dealer.cpp #1280167:1280168
@@ -116,9 +116,61 @@
         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 "clubs";
+        case KCardDeck::Diamonds:
+            return "diamonds";
+        case KCardDeck::Hearts:
+            return "hearts";
+        case KCardDeck::Spades:
+            return "spades";
+        default:
+            return QString();
 }
+    }
 
+    QString rankToString( int rank )
+    {
+        switch ( rank )
+        {
+        case KCardDeck::Ace:
+            return "ace";
+        case KCardDeck::Two:
+            return "two";
+        case KCardDeck::Three:
+            return "three";
+        case KCardDeck::Four:
+            return "four";
+        case KCardDeck::Five:
+            return "five";
+        case KCardDeck::Six:
+            return "six";
+        case KCardDeck::Seven:
+            return "seven";
+        case KCardDeck::Eight:
+            return "eight";
+        case KCardDeck::Nine:
+            return "nine";
+        case KCardDeck::Ten:
+            return "ten";
+        case KCardDeck::Jack:
+            return "jack";
+        case KCardDeck::Queen:
+            return "queen";
+        case KCardDeck::King:
+            return "king";
+        default:
+            return QString();
+        }
+    }
+}
 
+
 class SolverThread : public QThread
 {
     Q_OBJECT
@@ -221,13 +273,14 @@
 }
 
 
-void DealerScene::saveGame( QIODevice * io )
+void DealerScene::saveGameState( QIODevice * io )
 {
     QXmlStreamWriter xml( io );
     xml.setCodec( "UTF-8" );
     xml.setAutoFormatting( true );
     xml.setAutoFormattingIndent( -1 );
     xml.writeStartDocument();
+
     xml.writeDTD( "<!DOCTYPE kpat>" );
 
     xml.writeStartElement( "dealer" );
@@ -261,7 +314,7 @@
     d->gameWasJustSaved = true;
 }
 
-bool DealerScene::openGame( QIODevice * io )
+bool DealerScene::loadGameState( QIODevice * io )
 {
     resetInternals();
 
@@ -370,6 +423,175 @@
 }
 
 
+void DealerScene::saveGameHistory( QIODevice * io )
+{
+    QXmlStreamWriter xml( io );
+    xml.setCodec( "UTF-8" );
+    xml.setAutoFormatting( true );
+    xml.setAutoFormattingIndent( -1 );
+    xml.writeStartDocument();
+
+    xml.writeStartElement( "kpat-game" );
+    xml.writeAttribute( "game-type", QString::number( gameId() ) );
+    if ( !getGameOptions().isEmpty() )
+        xml.writeAttribute( "game-type-options", getGameOptions() );
+    xml.writeAttribute( "deal-number", QString::number( gameNumber() ) );
+
+    QList<GameState*> allStates;
+    for ( int i = 0; i < d->undoStack.size(); ++i )
+        allStates << d->undoStack.at( i );
+    allStates << d->currentState;
+    for ( int i = d->redoStack.size() - 1; i >= 0; --i )
+        allStates << d->redoStack.at( i );
+
+    for ( int i = 0; i < allStates.size(); ++i )
+    {
+        const GameState * state = allStates.at( i );
+        xml.writeStartElement( "state" );
+        if ( !state->stateData.isEmpty() )
+            xml.writeAttribute( "game-specific-state", state->stateData );
+        if ( i == d->undoStack.size() )
+            xml.writeAttribute( "current", "true" );
+
+        foreach ( const CardStateChange & change, state->changes )
+        {
+            xml.writeStartElement( "move" );
+            xml.writeAttribute( "pile", change.newState.pile->objectName() );
+            xml.writeAttribute( "position", QString::number( change.newState.index ) );
+            xml.writeAttribute( "face", change.newState.faceUp ? "up" : "down" );
+
+            foreach ( const KCard * card, change.cards )
+            {
+                xml.writeStartElement( "card" );
+                xml.writeAttribute( "suit", suitToString( card->suit() ) );
+                xml.writeAttribute( "rank", rankToString( card->rank() ) );
+                xml.writeAttribute( "id", QString::number( card->id() ) );
+                xml.writeEndElement();
+            }
+
+            xml.writeEndElement();
+        }
+        xml.writeEndElement();
+    }
+
+    xml.writeEndElement();
+    xml.writeEndDocument();
+
+    d->gameWasJustSaved = true;
+}
+
+
+bool DealerScene::loadGameHistory( QIODevice * io )
+{
+    resetInternals();
+
+    bool reenableAutoDrop = autoDropEnabled();
+    setAutoDropEnabled( false );
+
+    QXmlStreamReader xml( io );
+
+    xml.readNextStartElement();
+
+    if ( xml.name() != "kpat-game" )
+    {
+        kWarning() << "First tag is not \"kpat-game\"";
+        return false;
+    }
+
+    d->gameNumber = readIntAttribute( xml, "deal-number" );
+
+    setGameOptions( xml.attributes().value( "game-type-options" ).toString() );
+
+    QMultiHash<quint32,KCard*> cardHash;
+    foreach ( KCard * c, deck()->cards() )
+        cardHash.insert( c->id(), c );
+
+    QHash<QString,KCardPile*> pileHash;
+    foreach ( KCardPile * p, piles() )
+        pileHash.insert( p->objectName(), p );
+
+    int undosToDo = -1;
+
+    while( xml.readNextStartElement() )
+    {
+        if ( xml.name() != "state" )
+        {
+            kWarning() << "Expected a \"state\" tag. Found a" << xml.name() << "tag.";
+            return false;
+        }
+
+        setGameState( xml.attributes().value( "game-specific-state" ).toString() );
+
+        if ( undosToDo > -1 )
+            ++undosToDo;
+        else if ( xml.attributes().value( "current" ) == "true" )
+            undosToDo = 0;
+        
+        while( xml.readNextStartElement() )
+        {
+            if ( xml.name() != "move" )
+            {
+                kWarning() << "Expected a \"move\" tag. Found a" << xml.name() << "tag.";
+                return false;
+            }
+
+            QString pileName = xml.attributes().value( "pile" ).toString();
+            KCardPile * pile = pileHash.value( pileName );
+
+            bool indexOk;
+            int index = readIntAttribute( xml, "position", &indexOk );
+
+            bool faceUp = xml.attributes().value( "face" ) == "up";
+
+            if ( !pile || !indexOk )
+            {
+                kWarning() << "Unrecognized pile or index.";
+                return false;
+            }
+
+            while ( xml.readNextStartElement() )
+            {
+                if ( xml.name() != "card" )
+                {
+                    kWarning() << "Expected a \"card\" tag. Found a" << xml.name() << "tag.";
+                    return false;
+                }
+
+                KCard * card = cardHash.value( readIntAttribute( xml, "id" ) );
+                if ( !card )
+                {
+                    kWarning() << "Unrecognized card.";
+                    return false;
+                }
+                card->setFaceUp( faceUp );
+                pile->insert( card, index );
+
+                ++index;
+                xml.skipCurrentElement();
+            }
+        }
+        takeState();
+    }
+    
+    d->loadedMoveCount = 0;
+    d->gameStarted = moveCount() > 0;
+    emit updateMoves( moveCount() );
+
+    while ( undosToDo > 0 )
+    {
+        undo();
+        --undosToDo;
+    }
+    
+    foreach ( KCardPile * p, piles() )
+        updatePileLayout( p, 0 );
+
+    setAutoDropEnabled( reenableAutoDrop );
+
+    return true;
+}
+
+
 DealerScene::DealerScene()
 {
     setItemIndexMethod(QGraphicsScene::NoIndex);
--- trunk/KDE/kdegames/kpat/dealer.h #1280167:1280168
@@ -109,8 +109,11 @@
     bool allowedToStartNewGame();
     int moveCount() const;
 
-    void saveGame( QIODevice * io );
-    bool openGame( QIODevice * io );
+    void saveGameState( QIODevice * io );
+    bool loadGameState( QIODevice * io );
+    void saveGameHistory( QIODevice * io );
+    bool loadGameHistory( QIODevice * io );
+    
     virtual void mapOldId(int id);
     virtual int oldId() const;
     void recordGameStatistics();
--- trunk/KDE/kdegames/kpat/main.cpp #1280167:1280168
@@ -200,7 +200,7 @@
 
         DealerScene *f = getDealer( doc.documentElement().attribute("id").toInt() );
 
-        f->openGame( &of );
+        f->loadGameState( &of );
         f->solver()->translate_layout();
         int ret = f->solver()->patsolve();
         if ( ret == Solver::SolutionExists )
@@ -234,14 +234,14 @@
                    count--;
                    QFile file(QString("%1/%2-%3-1").arg(testdir).arg(dealer).arg(i));
                    file.open( QFile::WriteOnly );
-                   f->saveGame( &file );
+                   f->saveGameState( &file );
                 }
                 else if ( ret == Solver::NoSolutionExists ) {
                    fprintf( stdout, "%d: %d lost (%d ms)\n", dealer, i, mytime.elapsed()  );
                    count--;
                    QFile file(QString("%1/%2-%3-0").arg(testdir).arg(dealer).arg(i));
                    file.open( QFile::WriteOnly );
-                   f->saveGame( &file );
+                   f->saveGameState( &file );
                 } else {
                    fprintf( stdout, "%d: %d unknown (%d ms)\n", dealer, i, mytime.elapsed() );
                 }
--- trunk/KDE/kdegames/kpat/mainwindow.cpp #1280167:1280168
@@ -747,7 +747,7 @@
         if ( Settings::rememberStateOnExit() && !m_dealer->isGameWon() )
         {
             stateFile.open( QFile::WriteOnly | QFile::Truncate );
-            m_dealer->saveGame( &stateFile );
+            m_dealer->saveGameState( &stateFile );
         }
         else
         {
@@ -810,9 +810,9 @@
 
 bool MainWindow::loadGame( const KUrl & url, bool addToRecentFiles )
 {
+    // Some trickery to be able to use the same code to handle local and remote files.
     KTemporaryFile tempFile;
     QFile localFile;
-
     if ( url.isLocalFile() )
     {
         localFile.setFileName( url.toLocalFile() );
@@ -827,9 +827,8 @@
             return false;
         }
     }
+    QFile & file = url.isLocalFile() ? localFile : tempFile;
 
-
-    QFile & file = url.isLocalFile() ? localFile : tempFile;
     if ( !file.open( QIODevice::ReadOnly ) )
     {
         KMessageBox::error( this, i18n("Opening file failed.") );
@@ -837,27 +836,33 @@
     }
 
     QXmlStreamReader xml( &file );
-
-    while ( !xml.hasError() && xml.tokenType() != QXmlStreamReader::DTD )
-        xml.readNext();
-
-    QString dtdName = xml.dtdName().toString();
-
     if ( !xml.readNextStartElement() )
     {
         KMessageBox::error( this, i18n("Error reading XML file: ") + xml.errorString() );
         return false;
     }
 
-    if ( dtdName != "kpat" || xml.name() != "dealer" )
+    bool idOk;
+    int gameId = -1;
+    bool isOldStyleFile;
+
+    if ( xml.name() == "dealer" )
     {
-        KMessageBox::error( this, i18n("File is not a valid KPat save.") );
+        isOldStyleFile = true;
+        gameId = xml.attributes().value("id").toString().toInt( &idOk );
+    }
+    else if ( xml.name() == "kpat-game" )
+    {
+        isOldStyleFile = false;
+        gameId = xml.attributes().value("game-type").toString().toInt( &idOk );
+    }
+    else
+    {
+        KMessageBox::error( this, i18n("XML file is not a KPat save.") );
         return false;
     }
 
-    bool ok;
-    int gameId = xml.attributes().value("id").toString().toInt( &ok );
-    if ( !ok || !m_dealer_map.contains( gameId ) )
+    if ( !idOk || !m_dealer_map.contains( gameId ) )
     {
         KMessageBox::error( this, i18n("Unrecognized game id.") );
         return false;
@@ -868,18 +873,21 @@
     if ( m_dealer && !m_dealer->allowedToStartNewGame() )
         return false;
 
+    setGameType( gameId );
+    
     xml.clear();
     file.reset();
 
-    setGameType( gameId );
-    if ( !m_dealer->openGame( &file ) )
+    bool success = isOldStyleFile ? m_dealer->loadGameState( &file )
+                                  : m_dealer->loadGameHistory( &file );
+    if ( !success )
     {
         KMessageBox::error( this, i18n("Errors encountered while parsing file.") );
         slotShowGameSelectionScreen();
         return false;
     }
 
-    file.close();
+    setGameCaption();
 
     if ( addToRecentFiles )
         m_recentFilesAction->addUrl( url );
@@ -920,6 +928,8 @@
     if ( url.isEmpty() )
         return;
 
+    KMimeType::Ptr mimeType = dialog.currentFilterMimeType();
+
     if ( url.isLocalFile() )
     {
         QFile file( url.toLocalFile() );
@@ -928,7 +938,11 @@
             KMessageBox::error( this, i18n("Error opening file for writing. Saving failed.") );
             return;
         }
-        m_dealer->saveGame( &file );
+
+        if ( mimeType->is( savedStateMimeType ) )
+            m_dealer->saveGameState( &file );
+        else
+            m_dealer->saveGameHistory( &file );
     }
     else
     {
@@ -939,7 +953,10 @@
             return;
         }
 
-        m_dealer->saveGame( &tempFile );
+        if ( mimeType->is( savedStateMimeType ) )
+            m_dealer->saveGameState( &tempFile );
+        else
+            m_dealer->saveGameHistory( &tempFile );
 
         if ( !KIO::NetAccess::upload( tempFile.fileName(), url, this ) )
         {
[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic