save game query on exit if game is incomplete
[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 <QFileInfo>
37 #include <QDateTime>
38 #include <QTimer>
39 #include <QApplication>
40 #include <QDateTime>
41
42 #ifdef Q_WS_MAEMO_5
43 #include <QMaemo5InformationBox>
44 #endif
45
46 #include <QDebug>
47
48 GameView *GameView::instance_ = 0;
49
50 GameView::GameView(QWidget *parent) :
51         QGraphicsView(parent)
52 {
53     setBackgroundBrush(Qt::black);
54     qsrand(QDateTime::currentDateTime().toTime_t());
55     scene_ = new QGraphicsScene;
56     hiddenIndex_ = -1;
57     setScene(scene_);
58
59     introItem_ = new IntroItem;
60     introItem_->setText("- ImPuzzle -");
61
62     verticalStep_ = 0;
63     horizontalStep_ = 0;
64
65     qsrand(QDateTime::currentDateTime().toTime_t());
66
67     if(QFile::exists(QString("%1/%2/%3")
68                     .arg(QDir::homePath()).arg(HOME_DIRECTORY).arg(RESTORE_FILE))) {
69         if(!restoreGame()) {
70             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
71             PuzzleItem::setMoveCount(0);
72         }
73         else {
74             QTimer::singleShot(0, this, SIGNAL(gameRestored()));
75         }
76     }
77     else {
78         scene_->addItem(introItem_);
79     }
80 }
81
82 GameView *GameView::instance()
83 {
84     if(!instance_) {
85         instance_ = new GameView;
86     }
87
88     return instance_;
89 }
90
91 QList<PuzzleItem *> GameView::pieces() const
92 {
93     return pieces_;
94 }
95
96 void GameView::setPieces(const QList<PuzzleItem *> pieces, bool shuffle)
97 {
98     PuzzleItem::setManuallyMovable(false);
99
100     if(pieces.isEmpty()) {
101         qDebug() << "Empty list @ GameView::setPieces";
102         return;
103     }
104
105     QList<QGraphicsItem *> previousItems = scene_->items();
106     if(!previousItems.isEmpty()) {
107         foreach(QGraphicsItem *item, previousItems) {
108             scene_->removeItem(item);
109         }
110     }
111
112     pieces_ = pieces;
113
114     int horizontalCount = 0;
115
116     // Find out board size
117     if(pieces_.count() == EASY_PIECE_COUNT) {
118         horizontalCount = EASY_HORIZONTAL_COUNT;
119     }
120     else if(pieces_.count() == HARD_PIECE_COUNT) {
121         horizontalCount = HARD_HORIZONTAL_COUNT;
122     }
123     else {
124         qDebug() << "Invalid piece count @ GameView::setPieces";
125         qDebug() << QString("Count was %1").arg(pieces_.count());
126         return;
127     }
128
129     int verticalCount = pieces_.count() / horizontalCount;
130     horizontalStep_ = IMAGE_WIDTH / horizontalCount + 5;
131     verticalStep_ = IMAGE_HEIGHT / verticalCount + 5;
132
133     int pieceNumber = 0;
134
135     // Set pieces to their correct positions
136     for(int i = 0; i < verticalCount; ++i) {
137         for(int j = 0; j < horizontalCount; ++j) {
138             scene_->addItem(pieces_.at(pieceNumber));
139             QPointF point(j * horizontalStep_, i * verticalStep_);
140             pieces_.at(pieceNumber)->setPos(point);
141             pieces_.at(pieceNumber)->setCorrectPlace(point);
142             pieces_.at(pieceNumber)->setCurrentPlace(point);
143             pieces_.at(pieceNumber)->setDrawNumber(true);
144             pieceNumber++;
145         }
146     }
147
148     // Wait and shuffle if desired
149     if(shuffle) {
150         QTimer::singleShot(750, this, SLOT(shufflePieces()));
151     }
152     else {
153         PuzzleItem::setManuallyMovable(true);
154     }
155 }
156
157 //TODO: fixme!
158 void GameView::shufflePieces()
159 {
160     if(pieces_.isEmpty()) {
161         qDebug() << "Empty list @ GameView::shufflePieces";
162         return;
163     }
164
165     // Give pieces ramdom locations
166     hiddenIndex_ = qrand() % pieces_.count();
167     emptyPlace_ = pieces_.at(hiddenIndex_)->currentPlace();
168
169     QPointF topLeft = pieces_.at(0)->correctPlace();
170     QPointF bottomRight = pieces_.last()->correctPlace();
171
172     int moveCount = pieces_.count() * 10;
173     int movesMade = 0;
174
175     PuzzleItem *item = 0;
176
177     for(int i = 0; i < moveCount; ++i) {
178         int rand = qrand() % 4;
179
180         switch(rand) {
181         // up
182         case 0:
183             if(pieces_.at(hiddenIndex_)->currentPlace().y() > topLeft.y()) {
184                 QPointF tmp = pieces_.at(hiddenIndex_)->currentPlace();
185                 QGraphicsItem *graphicsItem = scene()->itemAt(tmp + QPointF(0, -verticalStep_));
186                 if(graphicsItem) {
187                     item = dynamic_cast<PuzzleItem *>(graphicsItem);
188                     if(item->movable()) {
189                         emptyPlace_ = item->currentPlace();
190                         pieces_.at(hiddenIndex_)->setCurrentPlace(item->currentPlace());
191                         pieces_.at(hiddenIndex_)->setPos(item->currentPlace());
192                         item->setCurrentPlace(tmp);
193                         item->setPos(tmp);
194                         invalidateScene();
195                         scene()->update();
196                         setMovingPieces();
197                         movesMade++;
198                     }
199                     else {
200                         qDebug() << "Item right of hidden piece not movable";
201                     }
202                 }
203             }
204             else {
205                 --i;
206             }
207             break;
208         // down
209         case 1:
210             if(pieces_.at(hiddenIndex_)->currentPlace().y() < bottomRight.y()) {
211                 QPointF tmp = pieces_.at(hiddenIndex_)->currentPlace();
212                 QGraphicsItem *graphicsItem = scene()->itemAt(tmp + QPointF(0, verticalStep_));
213                 if(graphicsItem) {
214                     item = dynamic_cast<PuzzleItem *>(graphicsItem);
215                     if(item->movable()) {
216                     emptyPlace_ = item->currentPlace();
217                         pieces_.at(hiddenIndex_)->setCurrentPlace(item->currentPlace());
218                         pieces_.at(hiddenIndex_)->setPos(item->currentPlace());
219                         item->setCurrentPlace(tmp);
220                         item->setPos(tmp);
221                         setMovingPieces();
222                         movesMade++;
223                     }
224                     else {
225                         qDebug() << "Item down of hidden piece not movable";
226                     }
227                 }
228             }
229             else {
230                 --i;
231             }
232             break;
233         // left
234         case 2:
235             if(pieces_.at(hiddenIndex_)->currentPlace().x() > topLeft.x()) {
236                 QPointF tmp = pieces_.at(hiddenIndex_)->currentPlace();
237                 QGraphicsItem *graphicsItem = scene()->itemAt(tmp + QPointF(-horizontalStep_, 0));
238                 if(graphicsItem) {
239                     item = dynamic_cast<PuzzleItem *>(graphicsItem);
240                     if(item->movable()) {
241                         emptyPlace_ = item->currentPlace();
242                         pieces_.at(hiddenIndex_)->setCurrentPlace(item->currentPlace());
243                         pieces_.at(hiddenIndex_)->setPos(item->currentPlace());
244                         item->setCurrentPlace(tmp);
245                         item->setPos(tmp);
246                         setMovingPieces();
247                         movesMade++;
248                     }
249                     else {
250                         qDebug() << "Item left of hidden piece not movable";
251                     }
252                 }
253             }
254             else {
255                 --i;
256             }
257             break;
258         // right
259         case 3:
260             if(pieces_.at(hiddenIndex_)->currentPlace().x() < bottomRight.x()) {
261                 QPointF tmp = pieces_.at(hiddenIndex_)->currentPlace();
262                 QGraphicsItem *graphicsItem = scene()->itemAt(tmp + QPointF(horizontalStep_, 0));
263                 if(graphicsItem) {
264                     item = dynamic_cast<PuzzleItem *>(graphicsItem);
265                     if(item->movable()) {
266                         emptyPlace_ = item->currentPlace();
267                         pieces_.at(hiddenIndex_)->setCurrentPlace(item->currentPlace());
268                         pieces_.at(hiddenIndex_)->setPos(item->currentPlace());
269                         item->setCurrentPlace(tmp);
270                         item->setPos(tmp);
271                         setMovingPieces();
272                         movesMade++;
273                     }
274                     else {
275                         qDebug() << "Item up of hidden piece not movable";
276                     }
277                 }
278             }
279             else {
280                 --i;
281             }
282             break;
283         default:
284             qDebug() << "WTF?";
285             break;
286         }
287     }
288
289     qDebug() << QString("Shuffle moves: %1/%2").arg(movesMade).arg(moveCount);
290
291     QParallelAnimationGroup *animationGroup = new QParallelAnimationGroup(this);
292     connect(animationGroup, SIGNAL(finished()), this, SLOT(shuffleAnimationFinished()));
293     for(int i = 0; i < pieces_.count(); ++i) {
294         QPropertyAnimation *animation = new QPropertyAnimation(pieces_.at(i), "pos");
295         animation->setStartValue(pieces_.at(i)->correctPlace());
296         animation->setEndValue(pieces_.at(i)->currentPlace());
297         animation->setDuration(750);
298         animation->setEasingCurve(QEasingCurve::InOutCirc);
299         animationGroup->addAnimation(animation);
300     }
301     animationGroup->start();
302     pieces_.at(hiddenIndex_)->hide();
303 }
304
305 void GameView::shuffleAnimationFinished()
306 {
307     setMovingPieces();
308     PuzzleItem::setManuallyMovable(true);
309 }
310
311 QPointF GameView::emptyPlace()
312 {
313     return emptyPlace_;
314 }
315
316 void GameView::setEmptyPlace(const QPointF &place)
317 {
318     emptyPlace_ = place;
319 }
320
321 bool GameView::areAllPiecesOk()
322 {
323     for(int i = 0; i < pieces_.count(); ++i) {
324         // Skip hidden piece
325         if(i == hiddenIndex_) {
326             continue;
327         }
328         // Id piece is not in it's place
329         else if(pieces_.at(i)->correctPlace() != pieces_.at(i)->currentPlace()) {
330             return false;
331         }
332     }
333     // Show hidden piece and move it to it's place
334     pieces_.at(hiddenIndex_)->show();
335     pieces_.at(hiddenIndex_)->moveMeTo(emptyPlace_);
336
337     // Set all pieces not movable and hide numbers
338     for(int i = 0; i < pieces_.count(); ++i) {
339         pieces_.at(i)->setMovable(false);
340         pieces_.at(i)->setDrawNumber(false);
341     }
342
343     // Show dialog with move count
344     QMessageBox::about(const_cast<GameView *>(this), tr("You won"), QString("Puzzle completed with %1 moves").arg(PuzzleItem::moveCount()));
345     emit gameWon();
346
347     return true;
348 }
349
350 void GameView::setMovingPieces()
351 {
352     if(pieces_.isEmpty()) {
353         qDebug() << "Empty list @ GameView::setMovingPieces";
354         return;
355     }
356
357     QPointF point = QPointF();
358     for(int i = 0; i < pieces_.count(); ++i) {
359         point = pieces_.at(i)->currentPlace();
360
361         // Is piece on the left side of the empty space
362         if(emptyPlace_.y() == point.y() && point.x() + horizontalStep_ == emptyPlace_.x()) {
363             pieces_.at(i)->setMovable(true);
364         }
365
366         // Is piece on the right side of the empty space
367         else if(emptyPlace_.y() == point.y() && point.x() - horizontalStep_ == emptyPlace_.x()) {
368             pieces_.at(i)->setMovable(true);
369         }
370
371         // Is piece below the empty space
372         else if(emptyPlace_.x() == point.x() && point.y() - verticalStep_ == emptyPlace_.y()) {
373             pieces_.at(i)->setMovable(true);
374         }
375
376         // Is piece on top of the empty space
377         else if(emptyPlace_.x() == point.x() && point.y() + verticalStep_ == emptyPlace_.y()) {
378             pieces_.at(i)->setMovable(true);
379         }
380
381         // The piece is somewhere else
382         else {
383             pieces_.at(i)->setMovable(false);
384         }
385     }
386 }
387
388 bool GameView::restoreGame()
389 {
390     // Read settings from file
391     QFile file(QString("%1/%2/%3")
392                .arg(QDir::homePath())
393                .arg(HOME_DIRECTORY)
394                .arg(RESTORE_FILE));
395
396     if(!file.open(QIODevice::ReadOnly)) {
397         qDebug() << "Failed to open restore file for reading";
398         return false;
399     }
400
401     QTextStream in(&file);
402
403     QStringList list;
404
405     list = in.readLine().split(";;");
406
407     qDebug() << "restore list count: " << list.count();
408
409     if(!list.isEmpty()) {
410         bool ok = false;
411         int pieces = list.at(0).toInt(&ok);
412         if(!ok) {
413             return false;
414         }
415
416         QString im = list.at(1);
417         if(!QFile::exists(im) && im != "default") {
418             return false;
419         }
420
421         int moveCount = list.at(2).toInt(&ok);
422         if(!ok) {
423             return false;
424         }
425
426         Settings::instance()->setPieceCount(pieces);
427         PuzzleItem::setMoveCount(moveCount);
428
429         if(im == "default" || im.isEmpty()) {
430             Settings::instance()->setImage(0);
431             Settings::instance()->setImagePath("default");
432         }
433         else {
434             Settings::instance()->setImagePath(im);
435             Settings::instance()->setImage(QPixmap(im));
436         }
437
438         setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()), false);
439
440         qDebug() << "pieces_ count after restoring image: " << pieces_.count();
441
442         if(list.count() >= pieces_.count() + 3) {
443             for(int j = 0; j < pieces_.count(); ++j) {
444                 if(!list.at(j + 3).isNull()) {
445                     QStringList points = list.at(j + 3).split("#");
446
447                     int x = points.at(0).toInt(&ok);
448                     if(!ok) {
449                         return false;
450                     }
451
452                     int y = points.at(1).toInt(&ok);
453                     if(!ok) {
454                         return false;
455                     }
456
457                     QPointF point(x, y);
458
459                     //qDebug() << "Setting piece " << pieces_.at(j)->pieceNumber();
460                     //qDebug() << "x: " << point.x() << " y: " << point.y();
461
462                     pieces_.at(j)->setCurrentPlace(point);
463                 }
464                 else {
465                     return false;
466                 }
467             }
468         }
469         else {
470             file.close();
471             file.remove();
472             return false;
473         }
474
475         QStringList hidden = list.last().split("#");
476
477         if(hidden.count() == 3) {
478             for(int m = 0; m < pieces_.count(); ++m) {
479                 pieces_.at(m)->setPos(pieces_.at(m)->currentPlace());
480                 if(pieces_.at(m)->pieceNumber() == hidden.at(2).toInt()) {
481                     //qDebug() << "Hiding piece number " << hidden;
482                     hiddenIndex_ = m;
483                 }
484             }
485
486             setEmptyPlace(QPointF(hidden.at(0).toInt(), hidden.at(1).toInt()));
487
488             pieces_.at(hiddenIndex_)->setVisible(false);
489
490             setMovingPieces();
491         }
492         else {
493             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
494             file.close();
495             file.remove();
496             return false;
497         }
498     }
499     else {
500         qDebug() << "Invalid restore file";
501         file.close();
502         file.remove();
503         return false;
504     }
505
506     QFileInfo fileInfo(file);
507
508     QDateTime created = fileInfo.created();
509     QString infoTxt = QString("Restored game state from %1")
510                       .arg(created.toString(Qt::TextDate));
511
512 #ifdef Q_WS_MAEMO_5
513     QMaemo5InformationBox::information(this, infoTxt);
514 #endif
515
516     file.close();
517     file.remove();
518
519     return true;
520 }
521
522 void GameView::saveGame()
523 {
524     if(pieces_.isEmpty() || pieces_.count() < EASY_PIECE_COUNT) {
525         return;
526     }
527
528     QDir dir;
529     if(!dir.exists(QString("%1/%2")
530                     .arg(QDir::homePath())
531                     .arg(HOME_DIRECTORY))) {
532         dir.mkpath(QString("%1/%2")
533                    .arg(QDir::homePath())
534                    .arg(HOME_DIRECTORY));
535     }
536
537     QFile file(QString("%1/%2/%3")
538                .arg(QDir::homePath())
539                .arg(HOME_DIRECTORY)
540                .arg(RESTORE_FILE));
541
542     if(!file.open(QIODevice::WriteOnly)) {
543         qDebug() << "Failed to open restore file for writing";
544         return;
545     }
546
547     QTextStream out(&file);
548
549     out << Settings::instance()->pieceCount();
550     out << QString(";;");
551     if(Settings::instance()->imagePath().isEmpty()) {
552         out << QString("default");
553     }
554     else {
555         out << Settings::instance()->imagePath();
556     }
557     out << QString(";;");
558     out << PuzzleItem::moveCount();
559     out << QString(";;");
560
561     // piece positions
562     int number = 0;
563     int hiddenNo = 0;
564
565     while(number != pieces_.count()) {
566         for(int i = 0; i < pieces_.count(); ++i) {
567             if(pieces_.at(i)->pieceNumber() == number + 1) {
568                 out << pieces_.at(i)->currentPlace().x();
569                 out << QString("#");
570                 out << pieces_.at(i)->currentPlace().y();
571                 out << QString(";;");
572                 pieces_.at(i)->pieceNumber();
573                 if(!pieces_.at(i)->isVisible()) {
574                     hiddenNo = number + 1;
575                 }
576                 number++;
577                 break;
578             }
579         }
580     }
581
582     out << QString("%1#%2#%3").arg(emptyPlace().x()).arg(emptyPlace().y()).arg(hiddenNo);
583
584     out << "\n";
585
586     file.close();
587
588     qApp->quit();
589 }
590
591 int GameView::correctPlaces() const
592 {
593     int c = 0;
594
595     for(int i = 0; i < pieces_.count(); ++i) {
596         if(pieces_.at(i)->currentPlace() == pieces_.at(i)->correctPlace()) {
597             c++;
598         }
599     }
600
601     return c;
602 }
603
604 QList<int> GameView::movingPlaces() const
605 {
606     QList<int> m;
607
608     for(int i = 0; i < pieces_.count(); ++i) {
609         if(pieces_.at(i)->movable()) {
610             m.append(i);
611         }
612     }
613
614     return m;
615 }