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