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