Simplifying level reading & more intro 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 <QFile>
24 #include <QPushButton>
25 #include <QApplication>
26 #include <QDebug>
27
28 const Piece* findPiece(PieceType type, int rotation)
29 {
30     static QHash<QPair<PieceType, int>, const Piece*> pieceCache;
31
32     // Fill the cache on the first run
33     if (pieceCache.size() == 0) {
34         for (int i = 0; ppieces[i].type != PiecesEnd; ++i)
35             pieceCache.insert(QPair<PieceType, int>(ppieces[i].type, ppieces[i].rotation), &ppieces[i]);
36     }
37     QPair<PieceType, int> key(type, rotation);
38     if (pieceCache.contains(key))
39         return pieceCache[key];
40     return 0;
41        
42 }
43
44 QString pieceToIconId(const Piece* piece, bool flow1 = false, bool flow2 = false)
45 {
46     QString fileName = QString(IMGDIR) + "/" + QString::number(piece->type) + "_" + QString::number(piece->rotation);
47     if (flow1 || flow2) {
48         fileName += (QString("_flow_") + (flow1? "1" : "0") + (flow2? "1" : "0"));
49     }
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         return true;
158     }
159     return false;
160 }
161
162 const Piece* GameField::pieceAt(int row, int col)
163 {
164     if (row < 0 || row >= rows || col < 0 || col >= cols) {
165         qWarning() << "Invalid piece index";
166         return ppieces;
167     }
168
169     int index = toIndex(row, col);
170     return field[index].piece;
171 }
172
173 bool GameField::isPrePlaced(int row, int col)
174 {
175     if (row < 0 || row >= rows || col < 0 || col >= cols) {
176         qWarning() << "Invalid piece index";
177         return false;
178     }
179
180     int index = toIndex(row, col);
181     return field[index].fixed;
182 }
183
184 void GameField::indicateFlow(int row, int col, Direction dir)
185 {
186     // Indicate the flow: fill the piece in question with the
187     // liquid. (The piece can also be an empty one, or an illegal
188     // one.)
189     qDebug() << "ind flow" << row << col << dir;
190     if (row < 0 || col < 0 || row >= rows || col >= cols) {
191         return;
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     }
208     else {
209         qWarning() << "Indicate flow: illegal direction" << row << col << dir;
210         return;
211     }
212
213     QString iconId = pieceToIconId(field[index].piece, field[index].flow[0], field[index].flow[1]);
214     qDebug() << "icon id" << iconId;
215     QModelIndex mIndex = fieldUi->model()->index(row, col);
216     QLabel* label = (QLabel*)fieldUi->indexWidget(mIndex);
217
218     label->setPixmap(QPixmap(iconId));
219 }
220
221 AvailablePieces::AvailablePieces(QTableWidget* ui)
222   : pieceUi(ui)
223 {
224     connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
225
226     // Setup ui
227
228     qDebug() << pieceUi->rowCount() << pieceUi->columnCount();
229
230     for (int i = 0; i < 2; ++i)
231         pieceUi->setColumnWidth(i, 120);
232
233     for (int i = 0; i < 4; ++i)
234         pieceUi->setRowHeight(i, 70);
235
236     for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
237         if (ppieces[i].userCanAdd == false) continue;
238
239         //qDebug() << ppieces[i].type << ppieces[i].rotation;
240         QString fileName = pieceToIconId(&(ppieces[i]));
241
242         QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
243
244         pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
245     }
246 }
247
248 int AvailablePieces::pieceToId(const Piece* piece)
249 {
250     return piece->type * 4 + piece->rotation/90;
251 }
252
253 const Piece* AvailablePieces::idToPiece(int id)
254 {
255     int rotation = (id % 4)*90;
256     PieceType type = (PieceType)(id / 4);
257     return findPiece(type, rotation);
258 }
259
260 void AvailablePieces::initGame(int count, AvailablePiece* pieces)
261 {
262     for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
263         if (ppieces[i].userCanAdd == false) continue;
264         pieceCounts.insert(&ppieces[i], 0);
265         pieceUi->item(ppieces[i].uiRow, ppieces[i].uiColumn)->setText(QString::number(0));
266     }
267
268     for (int i = 0; i < count; ++i) {
269         pieceCounts.insert(pieces[i].piece, pieces[i].count);
270         pieceUi->item(pieces[i].piece->uiRow, pieces[i].piece->uiColumn)->setText(QString::number(pieces[i].count));
271     }
272     pieceUi->clearSelection();
273 }
274
275 void AvailablePieces::onItemClicked(QTableWidgetItem* item)
276 {
277     qDebug() << "piece clicked";
278     int id =  item->type() - QTableWidgetItem::UserType;
279
280     const Piece* piece = idToPiece(id);
281     if (piece->type != PieceNone && pieceCounts[piece] > 0) {
282          emit validPieceSelected(piece);
283     }
284     else
285         emit invalidPieceSelected();
286 }
287
288 void AvailablePieces::onPieceUsed(const Piece* piece)
289 {
290     pieceCounts[piece]--;
291     pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
292
293     // TODO: perhaps clear the selection
294     if (pieceCounts[piece] == 0)
295         emit invalidPieceSelected();
296 }
297
298 GameController::GameController(AvailablePieces* pieceUi, GameField* fieldUi, 
299                                QLabel* timeLabel, QPushButton* doneButton)
300     : pieceUi(pieceUi), fieldUi(fieldUi), 
301       timeLabel(timeLabel), doneButton(doneButton),
302       currentPiece(ppieces), rows(0), cols(0), timeLeft(0), levelRunning(false), neededFlow(0),
303       startRow(0), startCol(0), startDir(DirNone), flowRow(0), flowCol(0), flowDir(DirNone), flowPreplaced(0), flowScore(0)
304 {
305     connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SLOT(onCellClicked(int, int)));
306     connect(pieceUi, SIGNAL(invalidPieceSelected()), 
307             this, SLOT(onInvalidPieceSelected()));
308     connect(pieceUi, SIGNAL(validPieceSelected(const Piece*)), 
309             this, SLOT(onValidPieceSelected(const Piece*)));
310
311     connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
312
313     connect(doneButton, SIGNAL(clicked()), this, SLOT(levelEnds()));
314
315     // Setup the timer, but don't start it yet
316     timer.setInterval(1000);
317     timer.setSingleShot(false);
318     connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
319     timeLabel->setText("");
320
321     flowTimer.setInterval(500);
322     flowTimer.setSingleShot(false);
323     connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
324 }
325
326 void GameController::startLevel(QString fileName)
327 {
328     // TODO: read the data while the user is reading the
329     // instructions...
330
331     // Read data about pre-placed pieces and available pieces from a
332     // text file.
333     QFile file(fileName);
334     if (!file.exists())
335         qFatal("Error reading game file: doesn't exist");
336
337     file.open(QIODevice::ReadOnly);
338     QTextStream gameData(&file);
339
340     gameData >> rows;
341     gameData >> cols;
342     qDebug() << rows << cols;
343     if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
344         qFatal("Error reading game file: rows and cols");
345
346     neededFlow = 0;
347     int prePlacedCount = 0;
348     gameData >> prePlacedCount;
349     if (prePlacedCount < 2 || prePlacedCount > 100)
350         qFatal("Error reading game file: piece count");
351
352     PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
353     for (int i = 0; i < prePlacedCount; ++i) {
354         int type = 0;
355         gameData >> type;
356         if (type < 0 || type >= PiecesEnd)
357             qFatal("Error reading game file: type of pre-placed piece");
358
359         int rotation = 0;
360         gameData >> rotation;
361         if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270)
362             qFatal("Error reading game file: rotation of pre-placed piece");
363
364         prePlaced[i].piece = findPiece((PieceType)type, rotation);
365         if (!prePlaced[i].piece)
366             qFatal("Error reading game file: invalid pre-placed piece");
367
368         // Record that the liquid must flow through this pre-placed
369         // piece (if it can)
370         neededFlow += flowCount(prePlaced[i].piece);
371
372         gameData >> prePlaced[i].row;
373         gameData >> prePlaced[i].col;
374         if (prePlaced[i].row < 0 || prePlaced[i].row >= rows || 
375             prePlaced[i].col < 0 || prePlaced[i].col >= cols)
376             qFatal("Error reading game file: piece position");
377
378         if (prePlaced[i].piece->type == PieceStart) {
379             startRow = prePlaced[i].row;
380             startCol = prePlaced[i].col;
381             startDir = prePlaced[i].piece->flows[0];
382         }
383     }
384     fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
385     delete[] prePlaced;
386
387     int availableCount = 0;
388     gameData >> availableCount;
389     if (availableCount < 2 || availableCount >= noPieces)
390         qFatal("Error reading game file: no of pieeces");
391
392     AvailablePiece* availablePieces = new AvailablePiece[availableCount];
393     for (int i = 0; i < availableCount; ++i) {
394         int ix = 0;
395         gameData >> ix;
396         if (ix < 0 || ix >= noPieces)
397             qFatal("Error reading game file: piece index");
398         availablePieces[i].piece = &ppieces[ix];
399         gameData >> availablePieces[i].count;
400         if (availablePieces[i].count < 0 || availablePieces[i].count > 100)
401             qFatal("Error reading game file: piece count");
402     }
403     pieceUi->initGame(availableCount, availablePieces);
404     delete[] availablePieces;
405
406     gameData >> timeLeft;
407     if (timeLeft < 0) 
408         qFatal("Error reading game file: time left");
409     timeLabel->setText(QString::number(timeLeft));
410
411     // Clear piece selection
412     onInvalidPieceSelected();
413
414     doneButton->setEnabled(true);
415     timer.start();
416     levelRunning = true;
417 }
418
419 void GameController::onTimeout()
420 {
421     --timeLeft;
422     timeLabel->setText(QString::number(timeLeft));
423     if (timeLeft <= 0) {
424         timer.stop();
425         levelEnds();
426     }
427 }
428
429 void GameController::onCellClicked(int row, int column)
430 {
431   qDebug() << "clicked: " << row << column;
432   if (!levelRunning) return;
433   if (currentPiece->type == PieceNone) return;
434   if (fieldUi->setPiece(row, column, currentPiece))
435       emit pieceUsed(currentPiece);
436 }
437
438 void GameController::onValidPieceSelected(const Piece* piece)
439 {
440     qDebug() << "selected: " << piece->type << piece->rotation;
441     currentPiece = piece;
442 }
443
444 void GameController::onInvalidPieceSelected()
445 {
446     currentPiece = ppieces;
447 }
448
449 void GameController::levelEnds()
450 {
451     if (!levelRunning) return;
452
453     doneButton->setEnabled(false);
454     levelRunning = false;
455     timer.stop();
456
457     // Initiate computing the flow
458     flowRow = startRow;
459     flowCol = startCol;
460     flowDir = startDir;
461     flowPreplaced = 0;
462     flowScore = 0;
463     flowTimer.setInterval(500);
464     flowTimer.start();
465 }
466
467 void GameController::computeFlow()
468 {
469     // We know:
470     // Where the flow currently is
471     // and which direction the flow goes after that piece
472     fieldUi->indicateFlow(flowRow, flowCol, flowDir);
473
474     if (flowDir == DirFailed) {
475         flowTimer.stop();
476         emit levelFailed();
477         return;
478     }
479
480     if (flowDir == DirPassed) {
481         flowTimer.stop();
482         emit levelPassed(flowScore);
483     }
484
485     if (flowDir == DirNone) {
486         // This square contained no pipe or an incompatible pipe. Get
487         // some more time, so that the user sees the failure before we
488         // emit levelFailed.
489         flowDir = DirFailed;
490         flowTimer.setInterval(1000);
491         return;
492     }
493     flowScore += 10;
494
495     if (flowDir == DirDone) {
496         // Again, give the user some time...
497         if (flowPreplaced < neededFlow) {
498             flowDir = DirFailed;
499             // TODO: indicate which pipes were missing
500         }
501         else
502             flowDir = DirPassed;
503
504         flowTimer.setInterval(1000);
505         return;
506     }
507
508     // Compute where it flows next
509     if (flowDir == DirRight) {
510         ++flowCol;
511         flowDir = DirLeft;
512     }
513     else if (flowDir == DirLeft) {
514         --flowCol;
515         flowDir = DirRight;
516     }
517     else if (flowDir == DirUp) {
518         --flowRow;
519         flowDir = DirDown;
520     }
521     else if (flowDir == DirDown) {
522         ++flowRow;
523         flowDir = DirUp;
524     }
525
526     if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
527         // Out of bounds
528         flowDir = DirFailed;
529         flowTimer.setInterval(1000);
530         return;
531     }
532
533     // Now we know the next piece and where the flow comes *from*
534     qDebug() << "flow to" << flowRow << flowCol;
535
536     // Check which piece is there
537     const Piece* piece = fieldUi->pieceAt(flowRow, flowCol);
538     qDebug() << "there is" << piece->type << piece->rotation;
539     flowDir = flowsTo(piece, flowDir);
540     // If the piece was pre-placed, record that the liquid has
541     // flown through it once
542     if (fieldUi->isPrePlaced(flowRow, flowCol))
543         flowPreplaced += 1;
544 }
545
546 LevelSwitcher::LevelSwitcher(GameController* gameController,
547                              QWidget* levelWidget, QListWidget* levelList, 
548                              QPushButton* levelStartButton,
549                              QWidget* startWidget, QLabel* startTitle, 
550                              QLabel* startLabel, QPushButton* startButton,
551                              QLabel* levelLabel, QLabel* scoreLabel,
552                              QStringList levelCollections)
553     : gameController(gameController),
554       levelWidget(levelWidget), levelList(levelList), levelStartButton(levelStartButton),
555       startWidget(startWidget), startTitle(startTitle), startLabel(startLabel), startButton(startButton),
556       levelLabel(levelLabel), scoreLabel(scoreLabel),
557       levelCollections(levelCollections), level(0), totalScore(0)
558 {
559     connect(levelStartButton, SIGNAL(clicked()), this, SLOT(onLevelCollectionChosen()));
560
561     connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
562     connect(gameController, SIGNAL(levelPassed(int)), this, SLOT(onLevelPassed(int)));
563     connect(gameController, SIGNAL(levelFailed()), this, SLOT(onLevelFailed()));
564     startTitle->setText("Starting a new game.");
565     scoreLabel->setText("0");
566     chooseLevelCollection();
567 }
568
569 void LevelSwitcher::chooseLevelCollection()
570 {
571     levelList->clear();
572     bool first = true;
573     foreach (const QString& collection, levelCollections) {
574         QListWidgetItem *newItem = new QListWidgetItem();
575         newItem->setText(collection);
576         levelList->addItem(newItem); // transfers ownership
577         if (first) {
578             levelList->setCurrentItem(newItem);
579             first = false;
580         }
581     }
582     levelWidget->show();
583 }
584
585 void LevelSwitcher::onLevelCollectionChosen()
586 {
587     levelWidget->hide();
588     QString collection = levelList->currentItem()->text();
589     QFile file(QString(LEVDIR) + "/" + collection + ".dat");
590     if (!file.exists())
591         qFatal("Error reading game file: doesn't exist");
592     file.open(QIODevice::ReadOnly);
593     QTextStream levelData(&file);
594     levels.clear();
595     
596     while (!levelData.atEnd())
597         levels << levelData.readLine();
598
599     level = 0;
600     totalScore = 0;
601     initiateLevel();
602 }
603
604 void LevelSwitcher::onStartClicked()
605 {
606     startWidget->hide();
607     levelLabel->setText(QString::number(level+1));
608     gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
609 }
610
611 void LevelSwitcher::initiateLevel()
612 {
613     if (level >= levels.size()) {
614         qWarning() << "Level index too large";
615         return;
616     }
617
618     QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
619     if (!file.exists())
620         qFatal("Error reading game file: doesn't exist");
621     file.open(QIODevice::ReadOnly);
622     QTextStream gameData(&file);
623
624     QString introText = gameData.readLine();
625     introText.replace("IMGDIR", IMGDIR);
626
627     // The start button might be connected to "chooseLevelCollection"
628     startButton->disconnect();
629     connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
630     startLabel->setText(introText);
631     startWidget->show();
632 }
633
634 void LevelSwitcher::onLevelPassed(int score)
635 {
636     totalScore += score;
637     scoreLabel->setText(QString::number(score));
638
639     if (level < levels.size() - 1) {
640         ++ level;
641         startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
642         initiateLevel();
643     }
644     else {
645         startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
646         startLabel->setText("Start a new game?");
647         startButton->disconnect();
648         connect(startButton, SIGNAL(clicked()), this, SLOT(chooseLevelCollection()));
649         level = 0;
650         startWidget->show();
651     }
652 }
653
654 void LevelSwitcher::onLevelFailed()
655 {
656     startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
657     initiateLevel();
658 }
659
660 // Todo next:
661 // better graphics
662 // save & load
663 // level collections: introduction + basic
664 // more levels
665 // make fixed pipes look different than non-fixed ones
666 // color theme
667 // --------------
668 // re-placing pieces
669 // graphical hints on what to do next
670 // graphical help, showing the ui elements: demo
671 // "done" animation
672 // level editor