Fine tuning save and restore and adding maemo5 style notifications
[impuzzle] / src / gameview.cpp
1 /*
2   Image Puzzle - A set your pieces straight game
3   Copyright (C) 2009  Timo Härkönen
4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation, either version 3 of the License, or
8   (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 #include "gameview.h"
20 #include "puzzleitem.h"
21 #include "defines.h"
22 #include "introitem.h"
23 #include "imageimporter.h"
24 #include "settings.h"
25
26 #include <QGraphicsScene>
27 #include <QDateTime>
28 #include <QTimer>
29 #include <QPropertyAnimation>
30 #include <QParallelAnimationGroup>
31 #include <QFont>
32 #include <QMessageBox>
33 #include <QFile>
34 #include <QDir>
35 #include <QTextStream>
36 #include <QCloseEvent>
37 #include <QFileInfo>
38 #include <QDateTime>
39
40 #ifdef Q_WS_MAEMO_5
41 #include <QMaemo5InformationBox>
42 #endif
43
44 #include <QDebug>
45
46 GameView *GameView::instance_ = 0;
47
48 GameView::GameView(QWidget *parent) :
49         QGraphicsView(parent)
50 {
51     scene_ = new QGraphicsScene;
52     hiddenIndex_ = -1;
53     setScene(scene_);
54
55     introItem_ = new IntroItem;
56     introItem_->setText("Select new game from menu to play");
57
58     verticalStep_ = 0;
59     horizontalStep_ = 0;
60
61     qsrand(QDateTime::currentDateTime().toTime_t());
62
63     if(QFile::exists(QString("%1/%2/%3")
64                     .arg(QDir::homePath()).arg(HOME_DIRECTORY).arg(RESTORE_FILE))) {
65         if(!restoreGame()) {
66             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
67             PuzzleItem::setMoveCount(0);
68         }
69     }
70     else {
71         scene_->addItem(introItem_);
72     }
73 }
74
75 GameView *GameView::instance()
76 {
77     if(!instance_) {
78         instance_ = new GameView;
79     }
80
81     return instance_;
82 }
83
84 QList<PuzzleItem *> GameView::pieces() const
85 {
86     return pieces_;
87 }
88
89 void GameView::setPieces(const QList<PuzzleItem *> pieces, bool shuffle)
90 {
91     if(pieces.isEmpty()) {
92         qDebug() << "Empty list @ GameView::setPieces";
93         return;
94     }
95
96     QList<QGraphicsItem *> previousItems = scene_->items();
97     if(!previousItems.isEmpty()) {
98         foreach(QGraphicsItem *item, previousItems) {
99             scene_->removeItem(item);
100         }
101     }
102
103     pieces_ = pieces;
104
105     int horizontalCount = 0;
106
107     // Find out board size
108     if(pieces_.count() == EASY_PIECE_COUNT) {
109         horizontalCount = EASY_HORIZONTAL_COUNT;
110     }
111     else if(pieces_.count() == HARD_PIECE_COUNT) {
112         horizontalCount = HARD_HORIZONTAL_COUNT;
113     }
114     else {
115         qDebug() << "Invalid piece count @ GameView::setPieces";
116         qDebug() << QString("Count was %1").arg(pieces_.count());
117         return;
118     }
119
120     int verticalCount = pieces_.count() / horizontalCount;
121     horizontalStep_ = IMAGE_WIDTH / horizontalCount + 5;
122     verticalStep_ = IMAGE_HEIGHT / verticalCount + 5;
123
124     int pieceNumber = 0;
125
126     // Set pieces to their correct positions
127     for(int i = 0; i < verticalCount; ++i) {
128         for(int j = 0; j < horizontalCount; ++j) {
129             scene_->addItem(pieces_.at(pieceNumber));
130             QPointF point(j * horizontalStep_, i * verticalStep_);
131             pieces_.at(pieceNumber)->setPos(point);
132             pieces_.at(pieceNumber)->setCorrectPlace(point);
133             pieces_.at(pieceNumber)->setCurrentPlace(point);
134             pieces_.at(pieceNumber)->setDrawNumber(true);
135             pieceNumber++;
136         }
137     }
138
139     // Wait and shuffle if desired
140     if(shuffle) {
141         QTimer::singleShot(750, this, SLOT(shufflePieces()));
142     }
143 }
144
145 void GameView::shufflePieces()
146 {
147     if(pieces_.isEmpty()) {
148         qDebug() << "Empty list @ GameView::shufflePieces";
149         return;
150     }
151
152     // Give pieces ramdom locations
153     int rounds = 5; //TODO
154     for(int j = 0; j < rounds; ++j) {
155         for(int i = 0; i < pieces_.count(); ++i) {
156             QPointF tmp;
157             int changeIndex = 0;
158             while(changeIndex == i) {
159                 changeIndex = qrand() % pieces_.count();
160             }
161             tmp = pieces_.at(changeIndex)->currentPlace();
162             pieces_.at(changeIndex)->setCurrentPlace(pieces_.at(i)->currentPlace());
163             pieces_.at(i)->setCurrentPlace(tmp);
164         }
165     }
166
167     QParallelAnimationGroup *animationGroup = new QParallelAnimationGroup(this);
168     for(int i = 0; i < pieces_.count(); ++i) {
169         QPropertyAnimation *animation = new QPropertyAnimation(pieces_.at(i), "pos");
170         animation->setStartValue(pieces_.at(i)->correctPlace());
171         animation->setEndValue(pieces_.at(i)->currentPlace());
172         animation->setDuration(750);
173         animation->setEasingCurve(QEasingCurve::InOutCirc);
174         animationGroup->addAnimation(animation);
175     }
176     animationGroup->start();
177
178     // Hide random piece
179     int hiddenPiece = qrand() % pieces_.count();
180     emptyPlace_ = pieces_.at(hiddenPiece)->currentPlace();
181     pieces_.at(hiddenPiece)->hide();
182     hiddenIndex_ = hiddenPiece;
183
184     setMovingPieces();
185 }
186
187 QPointF GameView::emptyPlace()
188 {
189     return emptyPlace_;
190 }
191
192 void GameView::setEmptyPlace(const QPointF &place)
193 {
194     emptyPlace_ = place;
195 }
196
197 bool GameView::areAllPiecesOk()
198 {
199     for(int i = 0; i < pieces_.count(); ++i) {
200         // Skip hidden piece
201         if(i == hiddenIndex_) {
202             continue;
203         }
204         // Id piece is not in it's place
205         else if(pieces_.at(i)->correctPlace() != pieces_.at(i)->currentPlace()) {
206             return false;
207         }
208     }
209     // Show hidden piece and move it to it's place
210     pieces_.at(hiddenIndex_)->show();
211     pieces_.at(hiddenIndex_)->moveMeTo(emptyPlace_);
212
213     // Set all pieces not movable and hide numbers
214     for(int i = 0; i < pieces_.count(); ++i) {
215         pieces_.at(i)->setMovable(false);
216         pieces_.at(i)->setDrawNumber(false);
217     }
218
219     // Show dialog with move count
220     QMessageBox::about(const_cast<GameView *>(this), tr("You won"), QString("Puzzle completed with %1 moves").arg(PuzzleItem::moveCount()));
221     emit gameWon();
222
223     return true;
224 }
225
226 void GameView::setMovingPieces()
227 {
228     if(pieces_.isEmpty()) {
229         qDebug() << "Empty list @ GameView::setMovingPieces";
230         return;
231     }
232
233     QPointF point = QPointF();
234     for(int i = 0; i < pieces_.count(); ++i) {
235         point = pieces_.at(i)->currentPlace();
236
237         // Is piece on the left side of the empty space
238         if(emptyPlace_.y() == point.y() && point.x() + horizontalStep_ == emptyPlace_.x()) {
239             pieces_.at(i)->setMovable(true);
240         }
241
242         // Is piece on the right side of the empty space
243         else if(emptyPlace_.y() == point.y() && point.x() - horizontalStep_ == emptyPlace_.x()) {
244             pieces_.at(i)->setMovable(true);
245         }
246
247         // Is piece below the empty space
248         else if(emptyPlace_.x() == point.x() && point.y() - verticalStep_ == emptyPlace_.y()) {
249             pieces_.at(i)->setMovable(true);
250         }
251
252         // Is piece on top of the empty space
253         else if(emptyPlace_.x() == point.x() && point.y() + verticalStep_ == emptyPlace_.y()) {
254             pieces_.at(i)->setMovable(true);
255         }
256
257         // The piece is somewhere else
258         else {
259             pieces_.at(i)->setMovable(false);
260         }
261     }
262 }
263
264 bool GameView::restoreGame()
265 {
266     // Read settings from file
267     QFile file(QString("%1/%2/%3")
268                .arg(QDir::homePath())
269                .arg(HOME_DIRECTORY)
270                .arg(RESTORE_FILE));
271
272     if(!file.open(QIODevice::ReadOnly)) {
273         qDebug() << "Failed to open restore file for reading";
274         return false;
275     }
276
277     QTextStream in(&file);
278
279     QStringList list;
280
281     list = in.readLine().split(";;");
282
283     qDebug() << "restore list count: " << list.count();
284
285     if(!list.isEmpty()) {
286         bool ok = false;
287         int pieces = list.at(0).toInt(&ok);
288         if(!ok) {
289             return false;
290         }
291
292         QString im = list.at(1);
293         if(!QFile::exists(im) && im != "default") {
294             return false;
295         }
296
297         int moveCount = list.at(2).toInt(&ok);
298         if(!ok) {
299             return false;
300         }
301
302         Settings::instance()->setPieceCount(pieces);
303         PuzzleItem::setMoveCount(moveCount);
304
305         if(im == "default" || im.isEmpty()) {
306             Settings::instance()->setImage(0);
307             Settings::instance()->setImagePath("default");
308         }
309         else {
310             Settings::instance()->setImagePath(im);
311             Settings::instance()->setImage(QPixmap(im));
312         }
313
314         setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()), false);
315
316         qDebug() << "pieces_ count after restoring image: " << pieces_.count();
317
318         if(list.count() >= pieces_.count() + 3) {
319             for(int j = 0; j < pieces_.count(); ++j) {
320                 if(!list.at(j + 3).isNull()) {
321                     QStringList points = list.at(j + 3).split("#");
322                     //if(points.count() == 2)
323                     int x = points.at(0).toInt(&ok);
324                     if(!ok) {
325                         return false;
326                     }
327
328                     int y = points.at(1).toInt(&ok);
329                     if(!ok) {
330                         return false;
331                     }
332
333                     QPointF point(x, y);
334
335                     //qDebug() << "Setting piece " << pieces_.at(j)->pieceNumber();
336                     //qDebug() << "x: " << point.x() << " y: " << point.y();
337
338                     pieces_.at(j)->setCurrentPlace(point);
339                 }
340                 else {
341                     return false;
342                 }
343             }
344         }
345         else {
346             file.close();
347             file.remove();
348             return false;
349         }
350
351         QStringList hidden = list.last().split("#");
352
353         if(hidden.count() == 3) {
354             for(int m = 0; m < pieces_.count(); ++m) {
355                 pieces_.at(m)->setPos(pieces_.at(m)->currentPlace());
356                 if(pieces_.at(m)->pieceNumber() == hidden.at(2).toInt()) {
357                     //qDebug() << "Hiding piece number " << hidden;
358                     hiddenIndex_ = m;
359                 }
360             }
361
362             setEmptyPlace(QPointF(hidden.at(0).toInt(), hidden.at(1).toInt()));
363
364             pieces_.at(hiddenIndex_)->setVisible(false);
365
366             setMovingPieces();
367         }
368         else {
369             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
370             file.close();
371             file.remove();
372             return false;
373         }
374     }
375     else {
376         qDebug() << "Invalid restore file";
377         file.close();
378         file.remove();
379         return false;
380     }
381
382     QFileInfo fileInfo(file);
383
384     QDateTime created = fileInfo.created();
385     QString infoTxt = QString("Restored game state from %1")
386                       .arg(created.toString(Qt::TextDate));
387
388 #ifdef Q_WS_MAEMO_5
389     QMaemo5InformationBox::information(this, infoTxt);
390 #endif
391
392     file.close();
393     file.remove();
394
395     return true;
396 }
397
398 bool GameView::saveGame()
399 {
400     if(pieces_.isEmpty() || pieces_.count() < EASY_PIECE_COUNT) {
401         return false;
402     }
403
404     QDir dir;
405     if(!dir.exists(QString("%1/%2")
406                     .arg(QDir::homePath())
407                     .arg(HOME_DIRECTORY))) {
408         dir.mkpath(QString("%1/%2")
409                    .arg(QDir::homePath())
410                    .arg(HOME_DIRECTORY));
411     }
412
413     QFile file(QString("%1/%2/%3")
414                .arg(QDir::homePath())
415                .arg(HOME_DIRECTORY)
416                .arg(RESTORE_FILE));
417
418     if(!file.open(QIODevice::WriteOnly)) {
419         qDebug() << "Failed to open restore file for writing";
420         return false;
421     }
422
423     QTextStream out(&file);
424
425     out << Settings::instance()->pieceCount();
426     out << QString(";;");
427     if(Settings::instance()->imagePath().isEmpty()) {
428         out << QString("default");
429     }
430     else {
431         out << Settings::instance()->imagePath();
432     }
433     out << QString(";;");
434     out << PuzzleItem::moveCount();
435     out << QString(";;");
436
437     // piece positions
438     int number = 0;
439     int hiddenNo = 0;
440
441     while(number != pieces_.count()) {
442         for(int i = 0; i < pieces_.count(); ++i) {
443             if(pieces_.at(i)->pieceNumber() == number + 1) {
444                 out << pieces_.at(i)->currentPlace().x();
445                 out << QString("#");
446                 out << pieces_.at(i)->currentPlace().y();
447                 out << QString(";;");
448                 pieces_.at(i)->pieceNumber();
449                 if(!pieces_.at(i)->isVisible()) {
450                     hiddenNo = number + 1;
451                 }
452                 number++;
453                 break;
454             }
455         }
456     }
457
458     out << QString("%1#%2#%3").arg(emptyPlace().x()).arg(emptyPlace().y()).arg(hiddenNo);
459
460     out << "\n";
461
462     file.close();
463
464     return true;
465 }
466
467 void GameView::closeEvent(QCloseEvent *event)
468 {
469     int answer = QMessageBox::question(this, tr("Save game status?"),
470                                        tr("Saved status will be automatically loaded when you start the application next time"),
471                                        QMessageBox::Yes, QMessageBox::No);
472
473     if(answer == QMessageBox::Yes) {
474         saveGame();
475     }
476
477     event->accept();
478 }