1 /* Evil Plumber is a small puzzle game.
2 Copyright (C) 2010 Marja Hassinen
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.
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.
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/>.
20 #include <QTableWidget>
21 #include <QListWidget>
24 #include <QPushButton>
25 #include <QApplication>
29 QString pieceToIconId(const Piece* piece, bool flow1 = false, bool flow2 = false)
31 QString fileName = QString(IMGDIR) + "/" + QString::number(piece->type) + "_" + QString::number(piece->rotation);
33 fileName += (QString("_flow_") + (flow1? "1" : "0") + (flow2? "1" : "0"));
37 qDebug() << "need: " << fileName;
38 return fileName + ".png";
41 int flowCount(const Piece* piece)
43 // How many times the liquid can flow through this pre-placed
46 for (int i = 0; i < 4; ++i) {
47 if (piece->flows[i] != DirNone) {
54 Direction flowsTo(const Piece* piece, Direction flowFrom)
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];
72 GameField::GameField(QTableWidget* ui)
73 : fieldUi(ui), field(0), rows(0), cols(0)
75 connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SIGNAL(cellClicked(int, int)));
76 fieldUi->setContentsMargins(0, 0, 0, 0);
79 void GameField::initGame(int rows_, int cols_, int count, PrePlacedPiece* prePlaced)
87 field = new PlacedPiece[rows*cols];
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;
97 fieldUi->setRowCount(rows);
98 fieldUi->setColumnCount(cols);
100 for (int i = 0; i < rows; ++i)
101 fieldUi->setRowHeight(i, 72);
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(""));
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);
117 int GameField::toIndex(int row, int col)
119 return row * cols + col;
122 bool GameField::setPiece(int row, int col, const Piece* piece, bool fixed)
124 qDebug() << "set piece" << row << col;
126 if (row < 0 || row >= rows || col < 0 || col >= cols) {
127 qWarning() << "Invalid piece index";
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;
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));
147 const Piece* GameField::pieceAt(int row, int col)
149 if (row < 0 || row >= rows || col < 0 || col >= cols) {
150 qWarning() << "Invalid piece index";
154 int index = toIndex(row, col);
155 return field[index].piece;
158 bool GameField::isPrePlaced(int row, int col)
160 if (row < 0 || row >= rows || col < 0 || col >= cols) {
161 qWarning() << "Invalid piece index";
165 int index = toIndex(row, col);
166 return field[index].fixed;
169 void GameField::indicateFlow(int row, int col, Direction dir)
171 // Indicate the flow: fill the piece in question with the
172 // liquid. (The piece can also be an empty one, or an illegal
174 qDebug() << "ind flow" << row << col << dir;
175 if (row < 0 || col < 0 || row >= rows || col >= cols) {
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;
182 else if (dir != DirNone && (field[index].piece->flows[2] == dir || field[index].piece->flows[3] == dir)) {
183 field[index].flow[1] = true;
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;
194 qWarning() << "Indicate flow: illegal direction" << row << col << dir;
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);
203 label->setPixmap(QPixmap(iconId));
206 QHash<QPair<PieceType, int>, const Piece*> AvailablePieces::pieceCache;
208 AvailablePieces::AvailablePieces(QTableWidget* ui)
211 connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
217 qDebug() << pieceUi->rowCount() << pieceUi->columnCount();
219 for (int i = 0; i < 2; ++i)
220 pieceUi->setColumnWidth(i, 120);
222 for (int i = 0; i < 4; ++i)
223 pieceUi->setRowHeight(i, 70);
225 for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
226 if (ppieces[i].userCanAdd == false) continue;
228 //qDebug() << ppieces[i].type << ppieces[i].rotation;
229 QString fileName = pieceToIconId(&(ppieces[i]));
231 QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
233 pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
237 int AvailablePieces::pieceToId(const Piece* piece)
239 return piece->type * 4 + piece->rotation/90;
242 const Piece* AvailablePieces::idToPiece(int id)
244 int rotation = (id % 4)*90;
245 PieceType type = (PieceType)(id / 4);
246 QPair<PieceType, int> key(type, rotation);
247 if (!pieceCache.contains(key))
249 return pieceCache[key];
252 void AvailablePieces::initPieceCache()
254 for (int i = 0; ppieces[i].type != PiecesEnd; ++i)
255 pieceCache.insert(QPair<PieceType, int>(ppieces[i].type, ppieces[i].rotation), &ppieces[i]);
258 void AvailablePieces::initGame(int count, AvailablePiece* pieces)
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));
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));
270 pieceUi->clearSelection();
273 void AvailablePieces::onItemClicked(QTableWidgetItem* item)
275 qDebug() << "piece clicked";
276 int id = item->type() - QTableWidgetItem::UserType;
278 const Piece* piece = idToPiece(id);
279 if (piece->type != PieceNone && pieceCounts[piece] > 0) {
280 emit validPieceSelected(piece);
283 emit invalidPieceSelected();
286 void AvailablePieces::onPieceUsed(const Piece* piece)
288 pieceCounts[piece]--;
289 pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
291 // TODO: perhaps clear the selection
292 if (pieceCounts[piece] == 0)
293 emit invalidPieceSelected();
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)
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*)));
309 connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
311 connect(doneButton, SIGNAL(clicked()), this, SLOT(levelEnds()));
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("");
319 flowTimer.setInterval(500);
320 flowTimer.setSingleShot(false);
321 connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
324 void GameController::startLevel(QString fileName)
326 // TODO: read the data while the user is reading the
329 // Read data about pre-placed pieces and available pieces from a
331 QFile file(fileName);
333 qFatal("Error reading game file: doesn't exist");
335 file.open(QIODevice::ReadOnly);
336 QTextStream gameData(&file);
340 qDebug() << rows << cols;
341 if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
342 qFatal("Error reading game file: rows and cols");
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");
351 PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
352 for (int i = 0; i < prePlacedCount; ++i) {
355 if (ix < 0 || ix >= noPieces)
356 qFatal("Error reading game file: no of pieces");
357 prePlaced[i].piece = &ppieces[ix];
359 // Record that the liquid must flow through this pre-placed
361 neededFlow += flowCount(prePlaced[i].piece);
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");
369 if (prePlaced[i].piece->type == PieceStart) {
370 startRow = prePlaced[i].row;
371 startCol = prePlaced[i].col;
372 startDir = prePlaced[i].piece->flows[0];
375 fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
378 int availableCount = 0;
379 gameData >> availableCount;
380 if (availableCount < 2 || availableCount >= noPieces)
381 qFatal("Error reading game file: no of pieeces");
383 AvailablePiece* availablePieces = new AvailablePiece[availableCount];
384 for (int i = 0; i < availableCount; ++i) {
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");
394 pieceUi->initGame(availableCount, availablePieces);
395 delete[] availablePieces;
397 gameData >> timeLeft;
399 qFatal("Error reading game file: time left");
400 timeLabel->setText(QString::number(timeLeft));
402 // Clear piece selection
403 onInvalidPieceSelected();
405 doneButton->setEnabled(true);
410 void GameController::onTimeout()
413 timeLabel->setText(QString::number(timeLeft));
420 void GameController::onCellClicked(int row, int column)
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);
429 void GameController::onValidPieceSelected(const Piece* piece)
431 qDebug() << "selected: " << piece->type << piece->rotation;
432 currentPiece = piece;
435 void GameController::onInvalidPieceSelected()
437 currentPiece = ppieces;
440 void GameController::levelEnds()
442 if (!levelRunning) return;
444 doneButton->setEnabled(false);
445 levelRunning = false;
448 // Initiate computing the flow
454 flowTimer.setInterval(500);
458 void GameController::computeFlow()
461 // Where the flow currently is
462 // and which direction the flow goes after that piece
463 fieldUi->indicateFlow(flowRow, flowCol, flowDir);
465 if (flowDir == DirFailed) {
471 if (flowDir == DirNone) {
472 // This square contained no pipe or an incompatible pipe. Get
473 // some more time, so that the user sees the failure before we
476 flowTimer.setInterval(1000);
481 if (flowDir == DirDone) {
482 if (flowPreplaced < neededFlow) {
484 // TODO: indicate which pipes were missing
485 flowTimer.setInterval(1000);
489 emit levelPassed(flowScore);
494 // Compute where it flows next
495 if (flowDir == DirRight) {
499 else if (flowDir == DirLeft) {
503 else if (flowDir == DirUp) {
507 else if (flowDir == DirDown) {
512 if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
515 flowTimer.setInterval(1000);
519 // Now we know the next piece and where the flow comes *from*
520 qDebug() << "flow to" << flowRow << flowCol;
522 // Check which piece is there
523 const Piece* piece = fieldUi->pieceAt(flowRow, flowCol);
524 qDebug() << "there is" << piece->type << piece->rotation;
525 flowDir = flowsTo(piece, flowDir);
526 // If the piece was pre-placed, record that the liquid has
527 // flown through it once
528 if (fieldUi->isPrePlaced(flowRow, flowCol))
532 LevelSwitcher::LevelSwitcher(GameController* gameController,
533 QWidget* levelWidget, QListWidget* levelList,
534 QPushButton* levelStartButton,
535 QWidget* startWidget, QLabel* startTitle,
536 QLabel* startLabel, QPushButton* startButton,
537 QLabel* levelLabel, QLabel* scoreLabel,
538 QStringList levelCollections)
539 : gameController(gameController),
540 levelWidget(levelWidget), levelList(levelList), levelStartButton(levelStartButton),
541 startWidget(startWidget), startTitle(startTitle), startLabel(startLabel), startButton(startButton),
542 levelLabel(levelLabel), scoreLabel(scoreLabel),
543 levelCollections(levelCollections), level(0), totalScore(0)
545 connect(levelStartButton, SIGNAL(clicked()), this, SLOT(onLevelCollectionChosen()));
547 connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
548 connect(gameController, SIGNAL(levelPassed(int)), this, SLOT(onLevelPassed(int)));
549 connect(gameController, SIGNAL(levelFailed()), this, SLOT(onLevelFailed()));
550 startTitle->setText("Starting a new game.");
551 scoreLabel->setText("0");
552 chooseLevelCollection();
555 void LevelSwitcher::chooseLevelCollection()
558 foreach (const QString& collection, levelCollections) {
559 QListWidgetItem *newItem = new QListWidgetItem();
560 newItem->setText(collection);
561 levelList->addItem(newItem); // transfers ownership
566 void LevelSwitcher::onLevelCollectionChosen()
569 QString collection = levelList->currentItem()->text();
570 QFile file(QString(LEVDIR) + "/" + collection + ".dat");
572 qFatal("Error reading game file: doesn't exist");
573 file.open(QIODevice::ReadOnly);
574 QTextStream levelData(&file);
577 while (!levelData.atEnd())
578 levels << levelData.readLine();
585 void LevelSwitcher::onStartClicked()
588 levelLabel->setText(QString::number(level+1));
589 gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
592 void LevelSwitcher::initiateLevel()
594 if (level >= levels.size()) {
595 qWarning() << "Level index too large";
599 QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
601 qFatal("Error reading game file: doesn't exist");
602 file.open(QIODevice::ReadOnly);
603 QTextStream gameData(&file);
605 QString introText = gameData.readLine();
606 introText.replace("IMGDIR", IMGDIR);
607 startLabel->setText(introText);
611 void LevelSwitcher::onLevelPassed(int score)
614 scoreLabel->setText(QString::number(score));
616 if (level < levels.size() - 1) {
618 startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
622 startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
623 startLabel->setText("Start a new game?");
624 // TODO: go to the level set selection screen
630 void LevelSwitcher::onLevelFailed()
632 startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
639 // level collections: introduction + basic
641 // make fixed pipes look different than non-fixed ones
646 // graphical hints on what to do next
647 // graphical help, showing the ui elements: demo