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>
23 #include <QPushButton>
24 #include <QApplication>
29 const Piece* findPiece(PieceType type, int rotation)
31 static QHash<QPair<PieceType, int>, const Piece*> pieceCache;
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]);
38 QPair<PieceType, int> key(type, rotation);
39 if (pieceCache.contains(key))
40 return pieceCache[key];
45 QString pieceToIconId(const Piece* piece, bool flow1 = false, bool flow2 = false)
47 QString fileName = QString(IMGDIR) + "/" + QString::number(piece->type) + "_" + QString::number(piece->rotation);
49 fileName += (QString("_flow_") + (flow1? "1" : "0") + (flow2? "1" : "0"));
52 return fileName + ".png";
55 int flowCount(const Piece* piece)
57 // How many times the liquid can flow through this pre-placed
60 for (int i = 0; i < 4; ++i) {
61 if (piece->flows[i] != DirNone) {
68 Direction flowsTo(const Piece* piece, Direction flowFrom)
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];
81 GameField::GameField(QTableWidget* ui)
82 : fieldUi(ui), field(0), rows(0), cols(0)
84 connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SIGNAL(cellClicked(int, int)));
85 fieldUi->setContentsMargins(0, 0, 0, 0);
88 void GameField::initGame(int rows_, int cols_, int count, PrePlacedPiece* prePlaced)
96 field = new PlacedPiece[rows*cols];
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;
106 fieldUi->setRowCount(rows);
107 fieldUi->setColumnCount(cols);
109 for (int i = 0; i < rows; ++i)
110 fieldUi->setRowHeight(i, 72);
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(""));
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);
126 int GameField::toIndex(int row, int col)
128 return row * cols + col;
131 bool GameField::setPiece(int row, int col, const Piece* piece, bool fixed)
133 if (row < 0 || row >= rows || col < 0 || col >= cols) {
134 qWarning() << "Invalid piece index";
138 int index = toIndex(row, col);
139 if (field[index].piece->type == PieceNone) {
140 field[index].piece = piece;
141 field[index].fixed = fixed;
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));
149 label->setStyleSheet("background-color: #263d49");
157 const Piece* GameField::pieceAt(int row, int col)
159 if (row < 0 || row >= rows || col < 0 || col >= cols) {
160 qWarning() << "Invalid piece index";
164 int index = toIndex(row, col);
165 return field[index].piece;
168 bool GameField::isPrePlaced(int row, int col)
170 if (row < 0 || row >= rows || col < 0 || col >= cols) {
171 qWarning() << "Invalid piece index";
175 int index = toIndex(row, col);
176 return field[index].fixed;
179 void GameField::indicateFlow(int row, int col, Direction dir)
181 // Indicate the flow: fill the piece in question with the
182 // liquid. (The piece can also be an empty one, or an illegal
185 if (row < 0 || col < 0 || row >= rows || col >= cols) {
188 if (dir == DirFailed || dir == DirPassed) {
189 // No need to indicate these pseudo-directions
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;
197 else if (dir != DirNone && (field[index].piece->flows[2] == dir || field[index].piece->flows[3] == dir)) {
198 field[index].flow[1] = true;
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;
208 qWarning() << "Indicate flow: illegal direction" << row << col << dir;
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);
216 label->setPixmap(QPixmap(iconId));
219 AvailablePieces::AvailablePieces(QTableWidget* ui)
222 connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
226 for (int i = 0; i < 2; ++i)
227 pieceUi->setColumnWidth(i, 120);
229 for (int i = 0; i < 5; ++i)
230 pieceUi->setRowHeight(i, 70);
232 for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
233 if (ppieces[i].userCanAdd == false) continue;
235 QString fileName = pieceToIconId(&(ppieces[i]));
237 QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
239 pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
243 int AvailablePieces::pieceToId(const Piece* piece)
245 return piece->type * 4 + piece->rotation/90;
248 const Piece* AvailablePieces::idToPiece(int id)
250 int rotation = (id % 4)*90;
251 PieceType type = (PieceType)(id / 4);
252 return findPiece(type, rotation);
255 void AvailablePieces::initGame(int count, AvailablePiece* pieces)
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));
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));
267 pieceUi->clearSelection();
270 void AvailablePieces::onItemClicked(QTableWidgetItem* item)
272 int id = item->type() - QTableWidgetItem::UserType;
274 const Piece* piece = idToPiece(id);
275 if (piece->type != PieceNone && pieceCounts[piece] > 0) {
276 emit validPieceSelected(piece);
279 emit invalidPieceSelected();
282 void AvailablePieces::onPieceUsed(const Piece* piece)
284 pieceCounts[piece]--;
285 pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
287 // TODO: perhaps clear the selection
288 if (pieceCounts[piece] == 0)
289 emit invalidPieceSelected();
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)
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*)));
305 connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
307 connect(doneButton, SIGNAL(clicked()), this, SLOT(onDoneClicked()));
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("");
315 flowTimer.setInterval(500);
316 flowTimer.setSingleShot(false);
317 connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
320 void GameController::startLevel(QString fileName)
322 // TODO: read the data while the user is reading the
325 // Read data about pre-placed pieces and available pieces from a
327 QFile file(fileName);
329 qFatal("Error reading game file: doesn't exist");
331 file.open(QIODevice::ReadOnly);
332 QTextStream gameData(&file);
336 if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
337 qFatal("Error reading game file: rows and cols");
340 int prePlacedCount = 0;
341 gameData >> prePlacedCount;
342 if (prePlacedCount < 2 || prePlacedCount > 100)
343 qFatal("Error reading game file: piece count");
345 PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
346 for (int i = 0; i < prePlacedCount; ++i) {
349 if (type < 0 || type >= PiecesEnd)
350 qFatal("Error reading game file: type of pre-placed piece");
353 gameData >> rotation;
354 if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270)
355 qFatal("Error reading game file: rotation of pre-placed piece");
357 prePlaced[i].piece = findPiece((PieceType)type, rotation);
358 if (!prePlaced[i].piece)
359 qFatal("Error reading game file: invalid pre-placed piece");
361 // Record that the liquid must flow through this pre-placed
363 neededFlow += flowCount(prePlaced[i].piece);
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");
371 if (prePlaced[i].piece->type == PieceStart) {
372 startRow = prePlaced[i].row;
373 startCol = prePlaced[i].col;
374 startDir = prePlaced[i].piece->flows[0];
377 fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
380 int availableCount = 0;
381 gameData >> availableCount;
382 if (availableCount < 2 || availableCount >= noPieces)
383 qFatal("Error reading game file: no of pieeces");
385 AvailablePiece* availablePieces = new AvailablePiece[availableCount];
386 for (int i = 0; i < availableCount; ++i) {
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");
396 pieceUi->initGame(availableCount, availablePieces);
397 delete[] availablePieces;
399 gameData >> timeLeft;
401 qFatal("Error reading game file: time left");
402 timeLabel->setText(QString::number(timeLeft));
404 // Clear piece selection
405 onInvalidPieceSelected();
407 doneButton->setText("Done");
413 void GameController::onTimeout()
416 timeLabel->setText(QString::number(timeLeft));
423 void GameController::onCellClicked(int row, int column)
425 if (!levelRunning) return;
426 if (currentPiece->type == PieceNone) return;
427 if (fieldUi->setPiece(row, column, currentPiece))
428 emit pieceUsed(currentPiece);
431 void GameController::onValidPieceSelected(const Piece* piece)
433 currentPiece = piece;
436 void GameController::onInvalidPieceSelected()
438 currentPiece = ppieces;
441 void GameController::onDoneClicked()
443 // If the level was running, start flowing the liquid
446 // Else, skip the flowing animation
449 flowTimer.setInterval(0);
453 void GameController::levelEnds()
455 if (!levelRunning) return;
457 doneButton->setText("Next");
458 levelRunning = false;
462 // Initiate computing the flow
468 flowTimer.setInterval(500);
472 void GameController::computeFlow()
475 // Where the flow currently is
476 // and which direction the flow goes after that piece
477 fieldUi->indicateFlow(flowRow, flowCol, flowDir);
479 if (flowDir == DirFailed) {
485 if (flowDir == DirPassed) {
487 emit levelPassed(flowScore);
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
495 if (animate) flowTimer.setInterval(1000);
500 if (flowDir == DirDone) {
501 // Again, give the user some time...
502 if (flowPreplaced < neededFlow) {
504 // TODO: indicate which pipes were missing
509 if (animate) flowTimer.setInterval(1000);
513 // Compute where it flows next
514 if (flowDir == DirRight) {
518 else if (flowDir == DirLeft) {
522 else if (flowDir == DirUp) {
526 else if (flowDir == DirDown) {
531 if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
534 if (animate) flowTimer.setInterval(1000);
538 // Now we know the next piece and where the flow comes *from*
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))
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)
562 connect(levelStartButton, SIGNAL(clicked()), this, SLOT(onLevelCollectionChosen()));
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()));
568 readLevelCollections(collections);
569 chooseLevelCollection();
572 void LevelSwitcher::chooseLevelCollection()
576 foreach (const QString& collection, levelCollections.keys()) {
577 QListWidgetItem *newItem = new QListWidgetItem();
579 // Check how many levels the user has already passed
581 if (savedGames.contains(collection)) {
582 passed = savedGames[collection];
585 if (levelCollections.contains(collection)) {
586 total = levelCollections[collection].size();
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);
602 void LevelSwitcher::onLevelCollectionChosen()
605 curColl = levelList->currentItem()->text().split(",").first();
607 if (levelCollections.contains(curColl)) {
608 levels = levelCollections[curColl];
611 qFatal("Error choosing a level collection: unrecognized");
614 // Go to the level the user has not yet passed
615 if (savedGames.contains(curColl)) {
616 level = savedGames[curColl];
617 if (level >= levels.size()) {
624 startTitle->setText("Starting a new game.");
626 startTitle->setText(QString("Continuing a game from level ") + QString::number(level+1) + QString("."));
628 scoreLabel->setText("0");
632 void LevelSwitcher::onStartClicked()
634 levelLabel->setText(QString::number(level+1));
635 gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
640 void LevelSwitcher::initiateLevel()
642 if (level >= levels.size()) {
643 qWarning() << "Level index too large";
647 QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
649 qFatal("Error reading game file: doesn't exist");
650 file.open(QIODevice::ReadOnly);
651 QTextStream gameData(&file);
653 QString introText = gameData.readLine();
654 introText.replace("IMGDIR", IMGDIR);
656 // The start button might be connected to "chooseLevelCollection"
657 startButton->disconnect();
658 connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
659 startLabel->setText(introText);
664 void LevelSwitcher::onLevelPassed(int score)
667 scoreLabel->setText(QString::number(score));
669 if (level < levels.size() - 1) {
671 startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
672 // Record that the level has been passed, so that the user can
674 savedGames.insert(curColl, level);
680 startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
681 startLabel->setText("Start a new game?");
682 startButton->disconnect();
683 connect(startButton, SIGNAL(clicked()), this, SLOT(chooseLevelCollection()));
684 // Record that all levels have been passed
685 savedGames.insert(curColl, levels.size());
694 void LevelSwitcher::onLevelFailed()
696 startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
700 void LevelSwitcher::readSavedGames()
702 QFile file(QDir::homePath() + "/.evilplumber");
703 if (!file.exists()) {
704 qWarning() << "Save file doesn't exist";
707 file.open(QIODevice::ReadOnly);
708 QTextStream saveData(&file);
709 QString collection = 0;
711 while (!saveData.atEnd()) {
712 saveData >> collection;
715 if (collection != "")
716 savedGames.insert(collection, level);
721 void LevelSwitcher::readLevelCollections(QStringList collections)
723 foreach (const QString& coll, collections) {
724 QFile file(QString(LEVDIR) + "/" + coll + ".dat");
727 qFatal("Error reading level collection: doesn't exist");
728 file.open(QIODevice::ReadOnly);
729 QTextStream levelData(&file);
730 QStringList readLevels;
731 while (!levelData.atEnd())
732 readLevels << levelData.readLine();
734 levelCollections.insert(coll, readLevels);
739 void LevelSwitcher::writeSavedGames()
741 QFile file(QDir::homePath() + "/.evilplumber");
742 file.open(QIODevice::Truncate | QIODevice::WriteOnly);
743 QTextStream saveData(&file);
744 foreach (const QString& collection, savedGames.keys()) {
745 saveData << collection << " " << savedGames[collection] << endl;
752 // more levels to the basic collection
754 // ability to install level sets as different packages
757 // graphical hints on what to do next
758 // graphical help, showing the ui elements: demo