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