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