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"));
53 qDebug() << "need: " << fileName;
54 return fileName + ".png";
57 int flowCount(const Piece* piece)
59 // How many times the liquid can flow through this pre-placed
62 for (int i = 0; i < 4; ++i) {
63 if (piece->flows[i] != DirNone) {
70 Direction flowsTo(const Piece* piece, Direction flowFrom)
72 //qDebug() << piece->flows[0];
73 //qDebug() << piece->flows[1];
74 //qDebug() << piece->flows[2];
75 //qDebug() << piece->flows[3];
76 //qDebug() << "check" << flowFrom;
77 if (piece->flows[0] == flowFrom)
78 return piece->flows[1];
79 if (piece->flows[1] == flowFrom)
80 return piece->flows[0];
81 if (piece->flows[2] == flowFrom)
82 return piece->flows[3];
83 if (piece->flows[3] == flowFrom)
84 return piece->flows[2];
88 GameField::GameField(QTableWidget* ui)
89 : fieldUi(ui), field(0), rows(0), cols(0)
91 connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SIGNAL(cellClicked(int, int)));
92 fieldUi->setContentsMargins(0, 0, 0, 0);
95 void GameField::initGame(int rows_, int cols_, int count, PrePlacedPiece* prePlaced)
103 field = new PlacedPiece[rows*cols];
105 for (int i = 0; i < rows*cols; ++i) {
106 field[i].piece = ppieces;
107 field[i].fixed = false;
108 field[i].flow[0] = false;
109 field[i].flow[1] = false;
113 fieldUi->setRowCount(rows);
114 fieldUi->setColumnCount(cols);
116 for (int i = 0; i < rows; ++i)
117 fieldUi->setRowHeight(i, 72);
119 for (int c = 0; c < cols; ++c) {
120 fieldUi->setColumnWidth(c, 72);
121 for (int r = 0; r < rows; ++r) {
122 QModelIndex index = fieldUi->model()->index(r, c);
123 fieldUi->setIndexWidget(index, new QLabel(""));
127 // Set pre-placed pieces
128 for (int i = 0; i < count; ++i) {
129 setPiece(prePlaced[i].row, prePlaced[i].col, prePlaced[i].piece, true);
133 int GameField::toIndex(int row, int col)
135 return row * cols + col;
138 bool GameField::setPiece(int row, int col, const Piece* piece, bool fixed)
140 qDebug() << "set piece" << row << col;
142 if (row < 0 || row >= rows || col < 0 || col >= cols) {
143 qWarning() << "Invalid piece index";
147 int index = toIndex(row, col);
148 if (field[index].piece->type == PieceNone) {
149 qDebug() << "really setting";
150 field[index].piece = piece;
151 field[index].fixed = fixed;
153 QString iconId = pieceToIconId(piece);
154 QModelIndex index = fieldUi->model()->index(row, col);
155 QLabel* label = (QLabel*)fieldUi->indexWidget(index);
156 label->setPixmap(QPixmap(iconId));
163 const Piece* GameField::pieceAt(int row, int col)
165 if (row < 0 || row >= rows || col < 0 || col >= cols) {
166 qWarning() << "Invalid piece index";
170 int index = toIndex(row, col);
171 return field[index].piece;
174 bool GameField::isPrePlaced(int row, int col)
176 if (row < 0 || row >= rows || col < 0 || col >= cols) {
177 qWarning() << "Invalid piece index";
181 int index = toIndex(row, col);
182 return field[index].fixed;
185 void GameField::indicateFlow(int row, int col, Direction dir)
187 // Indicate the flow: fill the piece in question with the
188 // liquid. (The piece can also be an empty one, or an illegal
190 qDebug() << "ind flow" << row << col << dir;
191 if (row < 0 || col < 0 || row >= rows || col >= cols) {
194 if (dir == DirDone || dir == DirFailed || dir == DirPassed) {
195 // No need to indicate these pseudo-directions
199 int index = toIndex(row, col);
200 if (dir != DirNone && (field[index].piece->flows[0] == dir || field[index].piece->flows[1] == dir)) {
201 field[index].flow[0] = true;
203 else if (dir != DirNone && (field[index].piece->flows[2] == dir || field[index].piece->flows[3] == dir)) {
204 field[index].flow[1] = true;
206 else if (dir == DirNone) {
207 // Flowing to a pipe from a wrong direction -> A hack to get
208 // the correct icon (same as an empty square flooded)
209 field[index].piece = ppieces;
210 field[index].flow[0] = true;
211 field[index].flow[1] = false;
214 qWarning() << "Indicate flow: illegal direction" << row << col << dir;
218 QString iconId = pieceToIconId(field[index].piece, field[index].flow[0], field[index].flow[1]);
219 qDebug() << "icon id" << iconId;
220 QModelIndex mIndex = fieldUi->model()->index(row, col);
221 QLabel* label = (QLabel*)fieldUi->indexWidget(mIndex);
223 label->setPixmap(QPixmap(iconId));
226 AvailablePieces::AvailablePieces(QTableWidget* ui)
229 connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
233 qDebug() << pieceUi->rowCount() << pieceUi->columnCount();
235 for (int i = 0; i < 2; ++i)
236 pieceUi->setColumnWidth(i, 120);
238 for (int i = 0; i < 4; ++i)
239 pieceUi->setRowHeight(i, 70);
241 for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
242 if (ppieces[i].userCanAdd == false) continue;
244 //qDebug() << ppieces[i].type << ppieces[i].rotation;
245 QString fileName = pieceToIconId(&(ppieces[i]));
247 QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
249 pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
253 int AvailablePieces::pieceToId(const Piece* piece)
255 return piece->type * 4 + piece->rotation/90;
258 const Piece* AvailablePieces::idToPiece(int id)
260 int rotation = (id % 4)*90;
261 PieceType type = (PieceType)(id / 4);
262 return findPiece(type, rotation);
265 void AvailablePieces::initGame(int count, AvailablePiece* pieces)
267 for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
268 if (ppieces[i].userCanAdd == false) continue;
269 pieceCounts.insert(&ppieces[i], 0);
270 pieceUi->item(ppieces[i].uiRow, ppieces[i].uiColumn)->setText(QString::number(0));
273 for (int i = 0; i < count; ++i) {
274 pieceCounts.insert(pieces[i].piece, pieces[i].count);
275 pieceUi->item(pieces[i].piece->uiRow, pieces[i].piece->uiColumn)->setText(QString::number(pieces[i].count));
277 pieceUi->clearSelection();
280 void AvailablePieces::onItemClicked(QTableWidgetItem* item)
282 qDebug() << "piece clicked";
283 int id = item->type() - QTableWidgetItem::UserType;
285 const Piece* piece = idToPiece(id);
286 if (piece->type != PieceNone && pieceCounts[piece] > 0) {
287 emit validPieceSelected(piece);
290 emit invalidPieceSelected();
293 void AvailablePieces::onPieceUsed(const Piece* piece)
295 pieceCounts[piece]--;
296 pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
298 // TODO: perhaps clear the selection
299 if (pieceCounts[piece] == 0)
300 emit invalidPieceSelected();
303 GameController::GameController(AvailablePieces* pieceUi, GameField* fieldUi,
304 QLabel* timeLabel, QPushButton* doneButton)
305 : pieceUi(pieceUi), fieldUi(fieldUi),
306 timeLabel(timeLabel), doneButton(doneButton),
307 currentPiece(ppieces), rows(0), cols(0), timeLeft(0), levelRunning(false), neededFlow(0),
308 startRow(0), startCol(0), startDir(DirNone), flowRow(0), flowCol(0), flowDir(DirNone), flowPreplaced(0), flowScore(0)
310 connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SLOT(onCellClicked(int, int)));
311 connect(pieceUi, SIGNAL(invalidPieceSelected()),
312 this, SLOT(onInvalidPieceSelected()));
313 connect(pieceUi, SIGNAL(validPieceSelected(const Piece*)),
314 this, SLOT(onValidPieceSelected(const Piece*)));
316 connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
318 connect(doneButton, SIGNAL(clicked()), this, SLOT(levelEnds()));
320 // Setup the timer, but don't start it yet
321 timer.setInterval(1000);
322 timer.setSingleShot(false);
323 connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
324 timeLabel->setText("");
326 flowTimer.setInterval(500);
327 flowTimer.setSingleShot(false);
328 connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
331 void GameController::startLevel(QString fileName)
333 // TODO: read the data while the user is reading the
336 // Read data about pre-placed pieces and available pieces from a
338 QFile file(fileName);
340 qFatal("Error reading game file: doesn't exist");
342 file.open(QIODevice::ReadOnly);
343 QTextStream gameData(&file);
347 qDebug() << rows << cols;
348 if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
349 qFatal("Error reading game file: rows and cols");
352 int prePlacedCount = 0;
353 gameData >> prePlacedCount;
354 if (prePlacedCount < 2 || prePlacedCount > 100)
355 qFatal("Error reading game file: piece count");
357 PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
358 for (int i = 0; i < prePlacedCount; ++i) {
361 if (type < 0 || type >= PiecesEnd)
362 qFatal("Error reading game file: type of pre-placed piece");
365 gameData >> rotation;
366 if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270)
367 qFatal("Error reading game file: rotation of pre-placed piece");
369 prePlaced[i].piece = findPiece((PieceType)type, rotation);
370 if (!prePlaced[i].piece)
371 qFatal("Error reading game file: invalid pre-placed piece");
373 // Record that the liquid must flow through this pre-placed
375 neededFlow += flowCount(prePlaced[i].piece);
377 gameData >> prePlaced[i].row;
378 gameData >> prePlaced[i].col;
379 if (prePlaced[i].row < 0 || prePlaced[i].row >= rows ||
380 prePlaced[i].col < 0 || prePlaced[i].col >= cols)
381 qFatal("Error reading game file: piece position");
383 if (prePlaced[i].piece->type == PieceStart) {
384 startRow = prePlaced[i].row;
385 startCol = prePlaced[i].col;
386 startDir = prePlaced[i].piece->flows[0];
389 fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
392 int availableCount = 0;
393 gameData >> availableCount;
394 if (availableCount < 2 || availableCount >= noPieces)
395 qFatal("Error reading game file: no of pieeces");
397 AvailablePiece* availablePieces = new AvailablePiece[availableCount];
398 for (int i = 0; i < availableCount; ++i) {
401 if (ix < 0 || ix >= noPieces)
402 qFatal("Error reading game file: piece index");
403 availablePieces[i].piece = &ppieces[ix];
404 gameData >> availablePieces[i].count;
405 if (availablePieces[i].count < 0 || availablePieces[i].count > 100)
406 qFatal("Error reading game file: piece count");
408 pieceUi->initGame(availableCount, availablePieces);
409 delete[] availablePieces;
411 gameData >> timeLeft;
413 qFatal("Error reading game file: time left");
414 timeLabel->setText(QString::number(timeLeft));
416 // Clear piece selection
417 onInvalidPieceSelected();
419 doneButton->setEnabled(true);
425 void GameController::onTimeout()
428 timeLabel->setText(QString::number(timeLeft));
435 void GameController::onCellClicked(int row, int column)
437 qDebug() << "clicked: " << row << column;
438 if (!levelRunning) return;
439 if (currentPiece->type == PieceNone) return;
440 if (fieldUi->setPiece(row, column, currentPiece))
441 emit pieceUsed(currentPiece);
444 void GameController::onValidPieceSelected(const Piece* piece)
446 qDebug() << "selected: " << piece->type << piece->rotation;
447 currentPiece = piece;
450 void GameController::onInvalidPieceSelected()
452 currentPiece = ppieces;
455 void GameController::levelEnds()
457 if (!levelRunning) return;
459 doneButton->setEnabled(false);
460 levelRunning = false;
463 // Initiate computing the flow
469 flowTimer.setInterval(500);
473 void GameController::computeFlow()
476 // Where the flow currently is
477 // and which direction the flow goes after that piece
478 fieldUi->indicateFlow(flowRow, flowCol, flowDir);
480 if (flowDir == DirFailed) {
486 if (flowDir == DirPassed) {
488 emit levelPassed(flowScore);
491 if (flowDir == DirNone) {
492 // This square contained no pipe or an incompatible pipe. Get
493 // some more time, so that the user sees the failure before we
496 flowTimer.setInterval(1000);
501 if (flowDir == DirDone) {
502 // Again, give the user some time...
503 if (flowPreplaced < neededFlow) {
505 // TODO: indicate which pipes were missing
510 flowTimer.setInterval(1000);
514 // Compute where it flows next
515 if (flowDir == DirRight) {
519 else if (flowDir == DirLeft) {
523 else if (flowDir == DirUp) {
527 else if (flowDir == DirDown) {
532 if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
535 flowTimer.setInterval(1000);
539 // Now we know the next piece and where the flow comes *from*
540 qDebug() << "flow to" << flowRow << flowCol;
542 // Check which piece is there
543 const Piece* piece = fieldUi->pieceAt(flowRow, flowCol);
544 qDebug() << "there is" << piece->type << piece->rotation;
545 flowDir = flowsTo(piece, flowDir);
546 // If the piece was pre-placed, record that the liquid has
547 // flown through it once
548 if (fieldUi->isPrePlaced(flowRow, flowCol))
552 LevelSwitcher::LevelSwitcher(GameController* gameController,
553 QWidget* levelWidget, QListWidget* levelList,
554 QPushButton* levelStartButton,
555 QWidget* startWidget, QLabel* startTitle,
556 QLabel* startLabel, QPushButton* startButton,
557 QLabel* levelLabel, QLabel* scoreLabel,
558 QStringList levelCollections)
559 : gameController(gameController),
560 levelWidget(levelWidget), levelList(levelList), levelStartButton(levelStartButton),
561 startWidget(startWidget), startTitle(startTitle), startLabel(startLabel), startButton(startButton),
562 levelLabel(levelLabel), scoreLabel(scoreLabel),
563 levelCollections(levelCollections), curColl(0), level(0), totalScore(0)
565 connect(levelStartButton, SIGNAL(clicked()), this, SLOT(onLevelCollectionChosen()));
567 connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
568 connect(gameController, SIGNAL(levelPassed(int)), this, SLOT(onLevelPassed(int)));
569 connect(gameController, SIGNAL(levelFailed()), this, SLOT(onLevelFailed()));
571 chooseLevelCollection();
574 void LevelSwitcher::chooseLevelCollection()
578 foreach (const QString& collection, levelCollections) {
579 QListWidgetItem *newItem = new QListWidgetItem();
581 // Check how many levels the user has already passed
583 if (savedGames.contains(collection)) {
584 passed = savedGames[collection];
587 newItem->setText(collection + " " + QString::number(passed) + " / " /*+ y*/);
588 levelList->addItem(newItem); // transfers ownership
589 if (first) { // TODO: and passed < total
590 levelList->setCurrentItem(newItem);
597 void LevelSwitcher::onLevelCollectionChosen()
600 curColl = levelList->currentItem()->text().split(" ").first();
601 QFile file(QString(LEVDIR) + "/" + curColl + ".dat");
602 qDebug() << "Trying to read" << file.fileName();
604 qFatal("Error reading game file: doesn't exist");
605 file.open(QIODevice::ReadOnly);
606 QTextStream levelData(&file);
609 while (!levelData.atEnd())
610 levels << levelData.readLine();
613 // Go to the level the user has not yet passed
614 if (savedGames.contains(curColl)) {
615 level = savedGames[curColl];
616 if (level >= levels.size() - 1) {
622 startTitle->setText("Starting a new game.");
623 scoreLabel->setText("0");
628 void LevelSwitcher::onStartClicked()
631 levelLabel->setText(QString::number(level+1));
632 gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
635 void LevelSwitcher::initiateLevel()
637 if (level >= levels.size()) {
638 qWarning() << "Level index too large";
642 QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
644 qFatal("Error reading game file: doesn't exist");
645 file.open(QIODevice::ReadOnly);
646 QTextStream gameData(&file);
648 QString introText = gameData.readLine();
649 introText.replace("IMGDIR", IMGDIR);
651 // The start button might be connected to "chooseLevelCollection"
652 startButton->disconnect();
653 connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
654 startLabel->setText(introText);
658 void LevelSwitcher::onLevelPassed(int score)
661 scoreLabel->setText(QString::number(score));
663 if (level < levels.size() - 1) {
665 startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
666 // Record that the level has been passed, so that the user can
668 savedGames.insert(curColl, level);
674 startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
675 startLabel->setText("Start a new game?");
676 startButton->disconnect();
677 connect(startButton, SIGNAL(clicked()), this, SLOT(chooseLevelCollection()));
678 // Record that all levels have been passed
679 savedGames.insert(curColl, levels.size() - 1);
687 void LevelSwitcher::onLevelFailed()
689 startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
693 void LevelSwitcher::readSavedGames()
695 QFile file(QDir::homePath() + "/.evilplumber");
696 if (!file.exists()) {
697 qWarning() << "Save file doesn't exist";
700 file.open(QIODevice::ReadOnly);
701 QTextStream saveData(&file);
702 QString collection = 0;
704 while (!saveData.atEnd()) {
705 saveData >> collection;
707 qDebug() << "Got saved game: " << collection << level;
708 savedGames.insert(collection, level);
713 void LevelSwitcher::writeSavedGames()
715 QFile file(QDir::homePath() + "/.evilplumber");
716 file.open(QIODevice::Truncate | QIODevice::WriteOnly);
717 QTextStream saveData(&file);
718 foreach (const QString& collection, savedGames.keys()) {
719 saveData << collection << " " << savedGames[collection] << endl;
727 // level collections: introduction + basic
729 // make fixed pipes look different than non-fixed ones
733 // graphical hints on what to do next
734 // graphical help, showing the ui elements: demo