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