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