14667049d8c09f1d475839746157638281bc6141
[evilplumber] / src / game.cpp
1 /* Evil Plumber is a small puzzle game.
2    Copyright (C) 2010 Marja Hassinen
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "game.h"
19
20 #include <QTableWidget>
21 #include <QListWidget>
22 #include <QLabel>
23 #include <QPushButton>
24 #include <QApplication>
25 #include <QFile>
26 #include <QDir>
27 #include <QDebug>
28
29 const Piece* findPiece(PieceType type, int rotation)
30 {
31     static QHash<QPair<PieceType, int>, const Piece*> pieceCache;
32
33     // Fill the cache on the first run
34     if (pieceCache.size() == 0) {
35         for (int i = 0; ppieces[i].type != PiecesEnd; ++i)
36             pieceCache.insert(QPair<PieceType, int>(ppieces[i].type, ppieces[i].rotation), &ppieces[i]);
37     }
38     QPair<PieceType, int> key(type, rotation);
39     if (pieceCache.contains(key))
40         return pieceCache[key];
41     return 0;
42        
43 }
44
45 QString pieceToIconId(const Piece* piece, bool flow1 = false, bool flow2 = false)
46 {
47     QString fileName = QString(IMGDIR) + "/" + QString::number(piece->type) + "_" + QString::number(piece->rotation);
48     if (flow1 || flow2) {
49         fileName += (QString("_flow_") + (flow1? "1" : "0") + (flow2? "1" : "0"));
50     }
51
52     //qDebug() << "need: " << fileName;
53     return fileName + ".png";
54 }
55
56 int flowCount(const Piece* piece)
57 {
58     // How many times the liquid can flow through this pre-placed
59     // pipe.
60     int flowCount = 0;
61     for (int i = 0; i < 4; ++i) {
62         if (piece->flows[i] != DirNone) {
63             ++flowCount;
64         }
65     }
66     return flowCount / 2;
67 }
68
69 Direction flowsTo(const Piece* piece, Direction flowFrom)
70 {
71     //qDebug() << piece->flows[0];
72     //qDebug() << piece->flows[1];
73     //qDebug() << piece->flows[2];
74     //qDebug() << piece->flows[3];
75     //qDebug() << "check" << flowFrom;
76     if (piece->flows[0] == flowFrom)
77         return piece->flows[1];
78     if (piece->flows[1] == flowFrom)
79         return piece->flows[0];
80     if (piece->flows[2] == flowFrom)
81         return piece->flows[3];
82     if (piece->flows[3] == flowFrom)
83         return piece->flows[2];
84     return DirNone;
85 }
86
87 GameField::GameField(QTableWidget* ui)
88 : fieldUi(ui), field(0), rows(0), cols(0)
89 {
90     connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SIGNAL(cellClicked(int, int)));
91     fieldUi->setContentsMargins(0, 0, 0, 0);
92 }
93
94 void GameField::initGame(int rows_, int cols_, int count, PrePlacedPiece* prePlaced)
95 {
96     fieldUi->clear();
97
98     rows = rows_;
99     cols = cols_;
100
101     delete[] field;
102     field = new PlacedPiece[rows*cols];
103
104     for (int i = 0; i < rows*cols; ++i) {
105         field[i].piece = ppieces;
106         field[i].fixed = false;
107         field[i].flow[0] = false;
108         field[i].flow[1] = false;
109     }
110
111     // Setup ui  
112     fieldUi->setRowCount(rows);
113     fieldUi->setColumnCount(cols);
114
115     for (int i = 0; i < rows; ++i)
116         fieldUi->setRowHeight(i, 72);
117
118     for (int c = 0; c < cols; ++c) {
119         fieldUi->setColumnWidth(c, 72);
120         for (int r = 0; r < rows; ++r) {
121             QModelIndex index = fieldUi->model()->index(r, c);
122             fieldUi->setIndexWidget(index, new QLabel(""));
123         }
124     }
125
126     // Set pre-placed pieces
127     for (int i = 0; i < count; ++i) {
128         setPiece(prePlaced[i].row, prePlaced[i].col, prePlaced[i].piece, true);
129     }
130 }
131
132 int GameField::toIndex(int row, int col)
133 {
134     return row * cols + col;
135 }
136
137 bool GameField::setPiece(int row, int col, const Piece* piece, bool fixed)
138 {
139     qDebug() << "set piece" << row << col;
140
141     if (row < 0 || row >= rows || col < 0 || col >= cols) {
142         qWarning() << "Invalid piece index";
143         return false;
144     }
145
146     int index = toIndex(row, col);
147     if (field[index].piece->type == PieceNone) {
148         qDebug() << "really setting";
149         field[index].piece = piece;
150         field[index].fixed = fixed;
151
152         QString iconId = pieceToIconId(piece);
153         QModelIndex index = fieldUi->model()->index(row, col);
154         QLabel* label = (QLabel*)fieldUi->indexWidget(index);
155         label->setPixmap(QPixmap(iconId));
156
157         if (fixed) {
158             label->setStyleSheet("background-color: #263d49");
159         }
160
161         return true;
162     }
163     return false;
164 }
165
166 const Piece* GameField::pieceAt(int row, int col)
167 {
168     if (row < 0 || row >= rows || col < 0 || col >= cols) {
169         qWarning() << "Invalid piece index";
170         return ppieces;
171     }
172
173     int index = toIndex(row, col);
174     return field[index].piece;
175 }
176
177 bool GameField::isPrePlaced(int row, int col)
178 {
179     if (row < 0 || row >= rows || col < 0 || col >= cols) {
180         qWarning() << "Invalid piece index";
181         return false;
182     }
183
184     int index = toIndex(row, col);
185     return field[index].fixed;
186 }
187
188 void GameField::indicateFlow(int row, int col, Direction dir)
189 {
190     // Indicate the flow: fill the piece in question with the
191     // liquid. (The piece can also be an empty one, or an illegal
192     // one.)
193     qDebug() << "ind flow" << row << col << dir;
194     if (row < 0 || col < 0 || row >= rows || col >= cols) {
195         return;
196     }
197     if (dir == DirFailed || dir == DirPassed) {
198         // No need to indicate these pseudo-directions
199         return;
200     }
201
202     int index = toIndex(row, col);
203     if (dir != DirNone && (field[index].piece->flows[0] == dir || field[index].piece->flows[1] == dir)) {
204         field[index].flow[0] = true;
205     }
206     else if (dir != DirNone && (field[index].piece->flows[2] == dir || field[index].piece->flows[3] == dir)) {
207         field[index].flow[1] = true;
208     }
209     else if (dir == DirNone) {
210         // Flowing to a pipe from a wrong direction -> A hack to get
211         // the correct icon (same as an empty square flooded)
212         field[index].piece = ppieces;
213         field[index].flow[0] = true;
214         field[index].flow[1] = false;
215     }
216     else {
217         qWarning() << "Indicate flow: illegal direction" << row << col << dir;
218         return;
219     }
220
221     QString iconId = pieceToIconId(field[index].piece, field[index].flow[0], field[index].flow[1]);
222     qDebug() << "icon id" << iconId;
223     QModelIndex mIndex = fieldUi->model()->index(row, col);
224     QLabel* label = (QLabel*)fieldUi->indexWidget(mIndex);
225
226     label->setPixmap(QPixmap(iconId));
227 }
228
229 AvailablePieces::AvailablePieces(QTableWidget* ui)
230   : pieceUi(ui)
231 {
232     connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
233
234     // Setup ui
235
236     qDebug() << pieceUi->rowCount() << pieceUi->columnCount();
237
238     for (int i = 0; i < 2; ++i)
239         pieceUi->setColumnWidth(i, 120);
240
241     for (int i = 0; i < 5; ++i)
242         pieceUi->setRowHeight(i, 70);
243
244     for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
245         if (ppieces[i].userCanAdd == false) continue;
246
247         //qDebug() << ppieces[i].type << ppieces[i].rotation;
248         QString fileName = pieceToIconId(&(ppieces[i]));
249
250         QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
251
252         pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
253     }
254 }
255
256 int AvailablePieces::pieceToId(const Piece* piece)
257 {
258     return piece->type * 4 + piece->rotation/90;
259 }
260
261 const Piece* AvailablePieces::idToPiece(int id)
262 {
263     int rotation = (id % 4)*90;
264     PieceType type = (PieceType)(id / 4);
265     return findPiece(type, rotation);
266 }
267
268 void AvailablePieces::initGame(int count, AvailablePiece* pieces)
269 {
270     for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
271         if (ppieces[i].userCanAdd == false) continue;
272         pieceCounts.insert(&ppieces[i], 0);
273         pieceUi->item(ppieces[i].uiRow, ppieces[i].uiColumn)->setText(QString::number(0));
274     }
275
276     for (int i = 0; i < count; ++i) {
277         pieceCounts.insert(pieces[i].piece, pieces[i].count);
278         pieceUi->item(pieces[i].piece->uiRow, pieces[i].piece->uiColumn)->setText(QString::number(pieces[i].count));
279     }
280     pieceUi->clearSelection();
281 }
282
283 void AvailablePieces::onItemClicked(QTableWidgetItem* item)
284 {
285     qDebug() << "piece clicked";
286     int id =  item->type() - QTableWidgetItem::UserType;
287
288     const Piece* piece = idToPiece(id);
289     if (piece->type != PieceNone && pieceCounts[piece] > 0) {
290          emit validPieceSelected(piece);
291     }
292     else
293         emit invalidPieceSelected();
294 }
295
296 void AvailablePieces::onPieceUsed(const Piece* piece)
297 {
298     pieceCounts[piece]--;
299     pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
300
301     // TODO: perhaps clear the selection
302     if (pieceCounts[piece] == 0)
303         emit invalidPieceSelected();
304 }
305
306 GameController::GameController(AvailablePieces* pieceUi, GameField* fieldUi, 
307                                QLabel* timeLabel, QPushButton* doneButton)
308     : pieceUi(pieceUi), fieldUi(fieldUi), 
309       timeLabel(timeLabel), doneButton(doneButton),
310       currentPiece(ppieces), rows(0), cols(0), timeLeft(0), levelRunning(false), neededFlow(0),
311       startRow(0), startCol(0), startDir(DirNone), flowRow(0), flowCol(0), flowDir(DirNone), flowPreplaced(0), flowScore(0)
312 {
313     connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SLOT(onCellClicked(int, int)));
314     connect(pieceUi, SIGNAL(invalidPieceSelected()), 
315             this, SLOT(onInvalidPieceSelected()));
316     connect(pieceUi, SIGNAL(validPieceSelected(const Piece*)), 
317             this, SLOT(onValidPieceSelected(const Piece*)));
318
319     connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
320
321     connect(doneButton, SIGNAL(clicked()), this, SLOT(levelEnds()));
322
323     // Setup the timer, but don't start it yet
324     timer.setInterval(1000);
325     timer.setSingleShot(false);
326     connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
327     timeLabel->setText("");
328
329     flowTimer.setInterval(500);
330     flowTimer.setSingleShot(false);
331     connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
332 }
333
334 void GameController::startLevel(QString fileName)
335 {
336     // TODO: read the data while the user is reading the
337     // instructions...
338
339     // Read data about pre-placed pieces and available pieces from a
340     // text file.
341     QFile file(fileName);
342     if (!file.exists())
343         qFatal("Error reading game file: doesn't exist");
344
345     file.open(QIODevice::ReadOnly);
346     QTextStream gameData(&file);
347
348     gameData >> rows;
349     gameData >> cols;
350     qDebug() << rows << cols;
351     if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
352         qFatal("Error reading game file: rows and cols");
353
354     neededFlow = 0;
355     int prePlacedCount = 0;
356     gameData >> prePlacedCount;
357     if (prePlacedCount < 2 || prePlacedCount > 100)
358         qFatal("Error reading game file: piece count");
359
360     PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
361     for (int i = 0; i < prePlacedCount; ++i) {
362         int type = 0;
363         gameData >> type;
364         if (type < 0 || type >= PiecesEnd)
365             qFatal("Error reading game file: type of pre-placed piece");
366
367         int rotation = 0;
368         gameData >> rotation;
369         if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270)
370             qFatal("Error reading game file: rotation of pre-placed piece");
371
372         prePlaced[i].piece = findPiece((PieceType)type, rotation);
373         if (!prePlaced[i].piece)
374             qFatal("Error reading game file: invalid pre-placed piece");
375
376         // Record that the liquid must flow through this pre-placed
377         // piece (if it can)
378         neededFlow += flowCount(prePlaced[i].piece);
379
380         gameData >> prePlaced[i].row;
381         gameData >> prePlaced[i].col;
382         if (prePlaced[i].row < 0 || prePlaced[i].row >= rows || 
383             prePlaced[i].col < 0 || prePlaced[i].col >= cols)
384             qFatal("Error reading game file: piece position");
385
386         if (prePlaced[i].piece->type == PieceStart) {
387             startRow = prePlaced[i].row;
388             startCol = prePlaced[i].col;
389             startDir = prePlaced[i].piece->flows[0];
390         }
391     }
392     fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
393     delete[] prePlaced;
394
395     int availableCount = 0;
396     gameData >> availableCount;
397     if (availableCount < 2 || availableCount >= noPieces)
398         qFatal("Error reading game file: no of pieeces");
399
400     AvailablePiece* availablePieces = new AvailablePiece[availableCount];
401     for (int i = 0; i < availableCount; ++i) {
402         int ix = 0;
403         gameData >> ix;
404         if (ix < 0 || ix >= noPieces)
405             qFatal("Error reading game file: piece index");
406         availablePieces[i].piece = &ppieces[ix];
407         gameData >> availablePieces[i].count;
408         if (availablePieces[i].count < 0 || availablePieces[i].count > 100)
409             qFatal("Error reading game file: piece count");
410     }
411     pieceUi->initGame(availableCount, availablePieces);
412     delete[] availablePieces;
413
414     gameData >> timeLeft;
415     if (timeLeft < 0) 
416         qFatal("Error reading game file: time left");
417     timeLabel->setText(QString::number(timeLeft));
418
419     // Clear piece selection
420     onInvalidPieceSelected();
421
422     doneButton->setEnabled(true);
423     timer.start();
424     levelRunning = true;
425     file.close();
426 }
427
428 void GameController::onTimeout()
429 {
430     --timeLeft;
431     timeLabel->setText(QString::number(timeLeft));
432     if (timeLeft <= 0) {
433         timer.stop();
434         levelEnds();
435     }
436 }
437
438 void GameController::onCellClicked(int row, int column)
439 {
440   qDebug() << "clicked: " << row << column;
441   if (!levelRunning) return;
442   if (currentPiece->type == PieceNone) return;
443   if (fieldUi->setPiece(row, column, currentPiece))
444       emit pieceUsed(currentPiece);
445 }
446
447 void GameController::onValidPieceSelected(const Piece* piece)
448 {
449     qDebug() << "selected: " << piece->type << piece->rotation;
450     currentPiece = piece;
451 }
452
453 void GameController::onInvalidPieceSelected()
454 {
455     currentPiece = ppieces;
456 }
457
458 void GameController::levelEnds()
459 {
460     if (!levelRunning) return;
461
462     doneButton->setEnabled(false);
463     levelRunning = false;
464     timer.stop();
465
466     // Initiate computing the flow
467     flowRow = startRow;
468     flowCol = startCol;
469     flowDir = startDir;
470     flowPreplaced = 0;
471     flowScore = 0;
472     flowTimer.setInterval(500);
473     flowTimer.start();
474 }
475
476 void GameController::computeFlow()
477 {
478     // We know:
479     // Where the flow currently is
480     // and which direction the flow goes after that piece
481     fieldUi->indicateFlow(flowRow, flowCol, flowDir);
482
483     if (flowDir == DirFailed) {
484         flowTimer.stop();
485         emit levelFailed();
486         return;
487     }
488
489     if (flowDir == DirPassed) {
490         flowTimer.stop();
491         emit levelPassed(flowScore);
492     }
493
494     if (flowDir == DirNone) {
495         // This square contained no pipe or an incompatible pipe. Get
496         // some more time, so that the user sees the failure before we
497         // emit levelFailed.
498         flowDir = DirFailed;
499         flowTimer.setInterval(1000);
500         return;
501     }
502     flowScore += 10;
503
504     if (flowDir == DirDone) {
505         // Again, give the user some time...
506         if (flowPreplaced < neededFlow) {
507             flowDir = DirFailed;
508             // TODO: indicate which pipes were missing
509         }
510         else
511             flowDir = DirPassed;
512
513         flowTimer.setInterval(1000);
514         return;
515     }
516
517     // Compute where it flows next
518     if (flowDir == DirRight) {
519         ++flowCol;
520         flowDir = DirLeft;
521     }
522     else if (flowDir == DirLeft) {
523         --flowCol;
524         flowDir = DirRight;
525     }
526     else if (flowDir == DirUp) {
527         --flowRow;
528         flowDir = DirDown;
529     }
530     else if (flowDir == DirDown) {
531         ++flowRow;
532         flowDir = DirUp;
533     }
534
535     if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
536         // Out of bounds
537         flowDir = DirFailed;
538         flowTimer.setInterval(1000);
539         return;
540     }
541
542     // Now we know the next piece and where the flow comes *from*
543     qDebug() << "flow to" << flowRow << flowCol;
544
545     // Check which piece is there
546     const Piece* piece = fieldUi->pieceAt(flowRow, flowCol);
547     qDebug() << "there is" << piece->type << piece->rotation;
548     flowDir = flowsTo(piece, flowDir);
549     // If the piece was pre-placed, record that the liquid has
550     // flown through it once
551     if (fieldUi->isPrePlaced(flowRow, flowCol))
552         flowPreplaced += 1;
553 }
554
555 LevelSwitcher::LevelSwitcher(GameController* gameController,
556                              QWidget* levelWidget, QListWidget* levelList, 
557                              QPushButton* levelStartButton,
558                              QWidget* startWidget, QLabel* startTitle, 
559                              QLabel* startLabel, QPushButton* startButton,
560                              QWidget* gameWidget, QLabel* levelLabel, QLabel* scoreLabel,
561                              QStringList collections)
562     : gameController(gameController),
563       levelWidget(levelWidget), levelList(levelList), levelStartButton(levelStartButton),
564       startWidget(startWidget), startTitle(startTitle), startLabel(startLabel), startButton(startButton),
565       gameWidget(gameWidget), levelLabel(levelLabel), scoreLabel(scoreLabel),
566       curColl(""), level(0), totalScore(0)
567 {
568     connect(levelStartButton, SIGNAL(clicked()), this, SLOT(onLevelCollectionChosen()));
569
570     connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
571     connect(gameController, SIGNAL(levelPassed(int)), this, SLOT(onLevelPassed(int)));
572     connect(gameController, SIGNAL(levelFailed()), this, SLOT(onLevelFailed()));
573     readSavedGames();
574     readLevelCollections(collections);
575     chooseLevelCollection();
576 }
577
578 void LevelSwitcher::chooseLevelCollection()
579 {
580     levelList->clear();
581     bool first = true;
582     foreach (const QString& collection, levelCollections.keys()) {
583         QListWidgetItem *newItem = new QListWidgetItem();
584
585         // Check how many levels the user has already passed
586         int passed = 0;
587         if (savedGames.contains(collection)) {
588             passed = savedGames[collection];
589         }
590         int total = 0;
591         if (levelCollections.contains(collection)) {
592             total = levelCollections[collection].size();
593         }
594
595         newItem->setText(collection + "   \tPassed: " + 
596                          QString::number(passed) + " / " + QString::number(total));
597         levelList->addItem(newItem); // transfers ownership
598         if (first && passed < total) {
599             levelList->setCurrentItem(newItem);
600             first = false;
601         }
602     }
603     gameWidget->hide();
604     startWidget->hide();
605     levelWidget->show();
606 }
607
608 void LevelSwitcher::onLevelCollectionChosen()
609 {
610     levelWidget->hide();
611     curColl = levelList->currentItem()->text().split(" ").first();
612
613     if (levelCollections.contains(curColl)) {
614         levels = levelCollections[curColl];
615     }
616     else
617         qFatal("Error choosing a level collection: unrecognized");
618
619     level = 0;
620     // Go to the level the user has not yet passed
621     if (savedGames.contains(curColl)) {
622         qDebug() << "going to saved level" << savedGames[curColl];
623         level = savedGames[curColl];
624         if (level >= levels.size()) {
625             level = 0;
626         }
627     }
628     
629     totalScore = 0;
630     startTitle->setText("Starting a new game.");
631     scoreLabel->setText("0");
632     initiateLevel();
633 }
634
635 void LevelSwitcher::onStartClicked()
636 {
637     levelLabel->setText(QString::number(level+1));
638     gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
639     startWidget->hide();
640     gameWidget->show();
641 }
642
643 void LevelSwitcher::initiateLevel()
644 {
645     if (level >= levels.size()) {
646         qWarning() << "Level index too large";
647         return;
648     }
649
650     QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
651     if (!file.exists())
652         qFatal("Error reading game file: doesn't exist");
653     file.open(QIODevice::ReadOnly);
654     QTextStream gameData(&file);
655
656     QString introText = gameData.readLine();
657     introText.replace("IMGDIR", IMGDIR);
658
659     // The start button might be connected to "chooseLevelCollection"
660     startButton->disconnect();
661     connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
662     startLabel->setText(introText);
663     gameWidget->hide();
664     startWidget->show();
665 }
666
667 void LevelSwitcher::onLevelPassed(int score)
668 {
669     totalScore += score;
670     scoreLabel->setText(QString::number(score));
671
672     if (level < levels.size() - 1) {
673         ++ level;
674         startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
675         // Record that the level has been passed, so that the user can
676         // start again
677         savedGames.insert(curColl, level);
678         writeSavedGames();
679
680         initiateLevel();
681     }
682     else {
683         startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
684         startLabel->setText("Start a new game?");
685         startButton->disconnect();
686         connect(startButton, SIGNAL(clicked()), this, SLOT(chooseLevelCollection()));
687         // Record that all levels have been passed
688         savedGames.insert(curColl, levels.size());
689         writeSavedGames();
690
691         level = 0;
692         startWidget->show();
693     }
694 }
695
696 void LevelSwitcher::onLevelFailed()
697 {
698     startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
699     initiateLevel();
700 }
701
702 void LevelSwitcher::readSavedGames()
703 {
704     QFile file(QDir::homePath() + "/.evilplumber");
705     if (!file.exists()) {
706         qWarning() << "Save file doesn't exist";
707         return;
708     }
709     file.open(QIODevice::ReadOnly);
710     QTextStream saveData(&file);
711     QString collection = 0;
712     int level = 0;
713     while (!saveData.atEnd()) {
714         saveData >> collection;
715         saveData >> level;
716         qDebug() << "Got saved game: " << collection << level;
717         if (collection != "")
718             savedGames.insert(collection, level);
719     }
720     file.close();
721 }
722
723 void LevelSwitcher::readLevelCollections(QStringList collections)
724 {
725     foreach (const QString& coll, collections) {
726         QFile file(QString(LEVDIR) + "/" + coll + ".dat");
727         qDebug() << "Trying to read" << file.fileName();
728         if (!file.exists())
729             qFatal("Error reading level collection: doesn't exist");
730         file.open(QIODevice::ReadOnly);
731         QTextStream levelData(&file);
732         QStringList readLevels;
733         while (!levelData.atEnd())
734             readLevels << levelData.readLine();
735
736         levelCollections.insert(coll, readLevels);
737         file.close();
738     }
739 }
740
741 void LevelSwitcher::writeSavedGames()
742 {
743     QFile file(QDir::homePath() + "/.evilplumber");
744     file.open(QIODevice::Truncate | QIODevice::WriteOnly);
745     QTextStream saveData(&file);
746     foreach (const QString& collection, savedGames.keys()) {
747         qDebug() << "writing" << collection << savedGames[collection];
748         saveData << collection << " " << savedGames[collection] << endl;
749     }
750     file.close();
751 }
752
753 // TODO:
754 // --- 0.1 ---
755 // more levels to the basic collection
756 // make fixed pipes look different than non-fixed ones
757 // get rid of debug prints
758 // --- 0.2 ---
759 // ability to install level sets as different packages
760 // better graphics
761 // color theme
762 // re-placing pieces
763 // graphical hints on what to do next
764 // graphical help, showing the ui elements: demo
765 // "done" animation
766 // level editor