ee44f0a3c740cbf18e5721c662c0934f534c5826
[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
38 #include <QDebug>
39
40 GameView *GameView::instance_ = 0;
41
42 GameView::GameView(QWidget *parent) :
43         QGraphicsView(parent)
44 {
45     scene_ = new QGraphicsScene;
46     hiddenIndex_ = -1;
47     setScene(scene_);
48
49     introItem_ = new IntroItem;
50     introItem_->setText("Select new game from menu to play");
51
52     verticalStep_ = 0;
53     horizontalStep_ = 0;
54
55     qsrand(QDateTime::currentDateTime().toTime_t());
56
57     if(QFile::exists(QString("%1/%2/%3")
58                     .arg(QDir::homePath()).arg(HOME_DIRECTORY).arg(RESTORE_FILE))) {
59         if(!restoreGame()) {
60             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
61         }
62     }
63     else {
64         scene_->addItem(introItem_);
65     }
66 }
67
68 GameView *GameView::instance()
69 {
70     if(!instance_) {
71         instance_ = new GameView;
72     }
73
74     return instance_;
75 }
76
77 QList<PuzzleItem *> GameView::pieces() const
78 {
79     return pieces_;
80 }
81
82 void GameView::setPieces(const QList<PuzzleItem *> pieces, bool shuffle)
83 {
84     if(pieces.isEmpty()) {
85         qDebug() << "Empty list @ GameView::setPieces";
86         return;
87     }
88
89     QList<QGraphicsItem *> previousItems = scene_->items();
90     if(!previousItems.isEmpty()) {
91         foreach(QGraphicsItem *item, previousItems) {
92             scene_->removeItem(item);
93         }
94     }
95
96     pieces_ = pieces;
97
98     int horizontalCount = 0;
99
100     // Find out board size
101     if(pieces_.count() == EASY_PIECE_COUNT) {
102         horizontalCount = EASY_HORIZONTAL_COUNT;
103     }
104     else if(pieces_.count() == HARD_PIECE_COUNT) {
105         horizontalCount = HARD_HORIZONTAL_COUNT;
106     }
107     else {
108         qDebug() << "Invalid piece count @ GameView::setPieces";
109         qDebug() << QString("Count was %1").arg(pieces_.count());
110         return;
111     }
112
113     int verticalCount = pieces_.count() / horizontalCount;
114     horizontalStep_ = IMAGE_WIDTH / horizontalCount + 5;
115     verticalStep_ = IMAGE_HEIGHT / verticalCount + 5;
116
117     int pieceNumber = 0;
118
119     // Set pieces to their correct positions
120     for(int i = 0; i < verticalCount; ++i) {
121         for(int j = 0; j < horizontalCount; ++j) {
122             scene_->addItem(pieces_.at(pieceNumber));
123             QPointF point(j * horizontalStep_, i * verticalStep_);
124             pieces_.at(pieceNumber)->setPos(point);
125             pieces_.at(pieceNumber)->setCorrectPlace(point);
126             pieces_.at(pieceNumber)->setCurrentPlace(point);
127             pieces_.at(pieceNumber)->setDrawNumber(true);
128             pieceNumber++;
129         }
130     }
131
132     // Wait and shuffle if desired
133     if(shuffle) {
134         QTimer::singleShot(750, this, SLOT(shufflePieces()));
135     }
136 }
137
138 void GameView::shufflePieces()
139 {
140     if(pieces_.isEmpty()) {
141         qDebug() << "Empty list @ GameView::shufflePieces";
142         return;
143     }
144
145     // Give pieces ramdom locations
146     int rounds = 5; //TODO
147     for(int j = 0; j < rounds; ++j) {
148         for(int i = 0; i < pieces_.count(); ++i) {
149             QPointF tmp;
150             int changeIndex = 0;
151             while(changeIndex == i) {
152                 changeIndex = qrand() % pieces_.count();
153             }
154             tmp = pieces_.at(changeIndex)->currentPlace();
155             pieces_.at(changeIndex)->setCurrentPlace(pieces_.at(i)->currentPlace());
156             pieces_.at(i)->setCurrentPlace(tmp);
157         }
158     }
159
160     QParallelAnimationGroup *animationGroup = new QParallelAnimationGroup(this);
161     for(int i = 0; i < pieces_.count(); ++i) {
162         QPropertyAnimation *animation = new QPropertyAnimation(pieces_.at(i), "pos");
163         animation->setStartValue(pieces_.at(i)->correctPlace());
164         animation->setEndValue(pieces_.at(i)->currentPlace());
165         animation->setDuration(750);
166         animation->setEasingCurve(QEasingCurve::InOutCirc);
167         animationGroup->addAnimation(animation);
168     }
169     animationGroup->start();
170
171     // Hide random piece
172     int hiddenPiece = qrand() % pieces_.count();
173     emptyPlace_ = pieces_.at(hiddenPiece)->currentPlace();
174     pieces_.at(hiddenPiece)->hide();
175     hiddenIndex_ = hiddenPiece;
176
177     setMovingPieces();
178 }
179
180 QPointF GameView::emptyPlace()
181 {
182     return emptyPlace_;
183 }
184
185 void GameView::setEmptyPlace(const QPointF &place)
186 {
187     emptyPlace_ = place;
188 }
189
190 bool GameView::areAllPiecesOk() const
191 {
192     for(int i = 0; i < pieces_.count(); ++i) {
193         // Skip hidden piece
194         if(i == hiddenIndex_) {
195             continue;
196         }
197         // Id piece is not in it's place
198         else if(pieces_.at(i)->correctPlace() != pieces_.at(i)->currentPlace()) {
199             return false;
200         }
201     }
202     // Show hidden piece and move it to it's place
203     pieces_.at(hiddenIndex_)->show();
204     pieces_.at(hiddenIndex_)->moveMeTo(emptyPlace_);
205
206     // Set all pieces not movable and hide numbers
207     for(int i = 0; i < pieces_.count(); ++i) {
208         pieces_.at(i)->setMovable(false);
209         pieces_.at(i)->setDrawNumber(false);
210     }
211
212     // Show dialog with move count
213     QMessageBox::about(const_cast<GameView *>(this), tr("You won"), QString("Puzzle completed with %1 moves").arg(PuzzleItem::moveCount()));
214
215     return true;
216 }
217
218 void GameView::setMovingPieces()
219 {
220     if(pieces_.isEmpty()) {
221         qDebug() << "Empty list @ GameView::setMovingPieces";
222         return;
223     }
224
225     QPointF point = QPointF();
226     for(int i = 0; i < pieces_.count(); ++i) {
227         point = pieces_.at(i)->currentPlace();
228
229         // Is piece on the left side of the empty space
230         if(emptyPlace_.y() == point.y() && point.x() + horizontalStep_ == emptyPlace_.x()) {
231             pieces_.at(i)->setMovable(true);
232         }
233
234         // Is piece on the right side of the empty space
235         else if(emptyPlace_.y() == point.y() && point.x() - horizontalStep_ == emptyPlace_.x()) {
236             pieces_.at(i)->setMovable(true);
237         }
238
239         // Is piece below the empty space
240         else if(emptyPlace_.x() == point.x() && point.y() - verticalStep_ == emptyPlace_.y()) {
241             pieces_.at(i)->setMovable(true);
242         }
243
244         // Is piece on top of the empty space
245         else if(emptyPlace_.x() == point.x() && point.y() + verticalStep_ == emptyPlace_.y()) {
246             pieces_.at(i)->setMovable(true);
247         }
248
249         // The piece is somewhere else
250         else {
251             pieces_.at(i)->setMovable(false);
252         }
253     }
254 }
255
256 bool GameView::restoreGame()
257 {
258     // Read settings from file
259     QFile file(QString("%1/%2/%3")
260                .arg(QDir::homePath())
261                .arg(HOME_DIRECTORY)
262                .arg(RESTORE_FILE));
263
264     if(!file.open(QIODevice::ReadOnly)) {
265         qDebug() << "Failed to open restore file for reading";
266         return false;
267     }
268
269     QTextStream in(&file);
270
271     QStringList list;
272
273     list = in.readLine().split(";;");
274
275     qDebug() << "restore list count: " << list.count();
276
277     if(!list.isEmpty()) {
278         Settings::instance()->setPieceCount(list.at(0).toInt());
279
280         QString im = list.at(1);
281         if(im == "default" || im.isEmpty()) {
282             Settings::instance()->setImage(0);
283             Settings::instance()->setImagePath("default");
284         }
285         else {
286             Settings::instance()->setImagePath(im);
287             Settings::instance()->setImage(QPixmap(im));
288         }
289         PuzzleItem::setMoveCount(list.at(2).toInt());
290
291         setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()), false);
292
293         qDebug() << "pieces_ count after restoring image: " << pieces_.count();
294
295         if(list.count() >= pieces_.count() + 3) {
296             for(int j = 0; j < pieces_.count(); ++j) {
297                 if(!list.at(j + 3).isNull()) {
298                     QStringList points = list.at(j + 3).split("#");
299                     //if(points.count() == 2)
300                     QPointF point(points.at(0).toInt(), points.at(1).toInt());
301
302                     qDebug() << "Setting piece " << pieces_.at(j)->pieceNumber();
303                     qDebug() << "x: " << point.x() << " y: " << point.y();
304
305                     pieces_.at(j)->setCurrentPlace(point);
306                 }
307             }
308         }
309         else {
310             file.close();
311             file.remove();
312             return false;
313         }
314
315         QStringList hidden = list.last().split("#");
316
317         if(hidden.count() == 3) {
318             for(int m = 0; m < pieces_.count(); ++m) {
319                 pieces_.at(m)->setPos(pieces_.at(m)->currentPlace());
320                 if(pieces_.at(m)->pieceNumber() == hidden.at(2).toInt()) {
321                     qDebug() << "Hiding piece number " << hidden;
322                     hiddenIndex_ = m;
323                 }
324             }
325
326             setEmptyPlace(QPointF(hidden.at(0).toInt(), hidden.at(1).toInt()));
327
328             pieces_.at(hiddenIndex_)->setVisible(false);
329
330             setMovingPieces();
331         }
332         else {
333             // TODO: revert
334             setPieces(ImageImporter::instance()->newPieces(Settings::instance()->image(), Settings::instance()->pieceCount()));
335             file.close();
336             file.remove();
337             return false;
338         }
339     }
340     else {
341         qDebug() << "Invalid restore file";
342         file.close();
343         file.remove();
344         return false;
345     }
346
347     file.close();
348     file.remove();
349
350     return true;
351 }
352
353 bool GameView::saveGame()
354 {
355     if(pieces_.isEmpty() || pieces_.count() < EASY_PIECE_COUNT) {
356         return false;
357     }
358
359     QDir dir;
360     if(!dir.exists(QString("%1/%2")
361                     .arg(QDir::homePath())
362                     .arg(HOME_DIRECTORY))) {
363         dir.mkpath(QString("%1/%2")
364                    .arg(QDir::homePath())
365                    .arg(HOME_DIRECTORY));
366     }
367
368     QFile file(QString("%1/%2/%3")
369                .arg(QDir::homePath())
370                .arg(HOME_DIRECTORY)
371                .arg(RESTORE_FILE));
372
373     if(!file.open(QIODevice::WriteOnly)) {
374         qDebug() << "Failed to open restore file for writing";
375         return false;
376     }
377
378     QTextStream out(&file);
379
380     out << Settings::instance()->pieceCount();
381     out << ";;";
382     if(Settings::instance()->imagePath().isEmpty()) {
383         out << "default";
384     }
385     else {
386         out << Settings::instance()->imagePath();
387     }
388     out << ";;";
389     out << PuzzleItem::moveCount();
390     out << ";;";
391
392     // piece positions
393     int number = 0;
394     int hiddenNo = 0;
395
396     while(number != pieces_.count()) {
397         for(int i = 0; i < pieces_.count(); ++i) {
398             if(pieces_.at(i)->pieceNumber() == number + 1) {
399                 out << pieces_.at(i)->currentPlace().x();
400                 out << "#";
401                 out << pieces_.at(i)->currentPlace().y();
402                 out << ";;";
403                 pieces_.at(i)->pieceNumber();
404                 if(!pieces_.at(i)->isVisible()) {
405                     hiddenNo = number + 1;
406                 }
407                 number++;
408                 break;
409             }
410         }
411     }
412
413     out << QString("%1#%2#%3").arg(emptyPlace().x()).arg(emptyPlace().y()).arg(hiddenNo);
414
415     out << "\n";
416
417     file.close();
418
419     return true;
420 }
421
422 void GameView::closeEvent(QCloseEvent *event)
423 {
424     int answer = QMessageBox::question(this, tr("Save game status?"),
425                                        tr("Saved status will be automatically loaded when you start the application next time"),
426                                        QMessageBox::Yes, QMessageBox::No);
427
428     if(answer == QMessageBox::Yes) {
429         saveGame();
430     }
431
432     event->accept();
433 }