First draft of the game + graphics, build system, debian packaging.
[evilplumber] / src / game.cpp
diff --git a/src/game.cpp b/src/game.cpp
new file mode 100644 (file)
index 0000000..df29387
--- /dev/null
@@ -0,0 +1,613 @@
+/* Evil Plumber is a small puzzle game.
+   Copyright (C) 2010 Marja Hassinen
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "game.h"
+
+#include <QTableWidget>
+#include <QDebug>
+#include <QLabel>
+#include <QFile>
+#include <QPushButton>
+#include <QFrame>
+#include <QApplication>
+
+
+QString pieceToIconId(const Piece* piece, bool flow1 = false, bool flow2 = false)
+{
+    QString fileName = QString(IMGDIR) + "/" + QString::number(piece->type) + "_" + QString::number(piece->rotation);
+    if (flow1 || flow2) {
+        fileName += (QString("_flow_") + (flow1? "1" : "0") + (flow2? "1" : "0"));
+    }
+
+
+    qDebug() << "need: " << fileName;
+    return fileName + ".png";
+}
+
+int flowCount(const Piece* piece)
+{
+    // How many times the liquid can flow through this pre-placed
+    // pipe.
+    int flowCount = 0;
+    for (int i = 0; i < 4; ++i) {
+        if (piece->flows[i] != DirNone) {
+            ++flowCount;
+        }
+    }
+    return flowCount / 2;
+}
+
+Direction flowsTo(const Piece* piece, Direction flowFrom)
+{
+    //qDebug() << piece->flows[0];
+    //qDebug() << piece->flows[1];
+    //qDebug() << piece->flows[2];
+    //qDebug() << piece->flows[3];
+    //qDebug() << "check" << flowFrom;
+    if (piece->flows[0] == flowFrom)
+        return piece->flows[1];
+    if (piece->flows[1] == flowFrom)
+        return piece->flows[0];
+    if (piece->flows[2] == flowFrom)
+        return piece->flows[3];
+    if (piece->flows[3] == flowFrom)
+        return piece->flows[2];
+    return DirNone;
+}
+
+GameField::GameField(QTableWidget* ui)
+: fieldUi(ui), field(0), rows(0), cols(0)
+{
+    connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SIGNAL(cellClicked(int, int)));
+    fieldUi->setContentsMargins(0, 0, 0, 0);
+}
+
+void GameField::initGame(int rows_, int cols_, int count, PrePlacedPiece* prePlaced)
+{
+    fieldUi->clear();
+    // FIXME: Does the table widget call the destructors of its items...
+
+    rows = rows_;
+    cols = cols_;
+
+    delete[] field;
+    field = new PlacedPiece[rows*cols];
+
+    for (int i = 0; i < rows*cols; ++i) {
+        field[i].piece = ppieces;
+        field[i].fixed = false;
+        field[i].flow[0] = false;
+        field[i].flow[1] = false;
+    }
+
+    // Setup ui  
+    fieldUi->setRowCount(rows);
+    fieldUi->setColumnCount(cols);
+
+    for (int i = 0; i < rows; ++i)
+        fieldUi->setRowHeight(i, 72);
+
+    for (int c = 0; c < cols; ++c) {
+        fieldUi->setColumnWidth(c, 72);
+        for (int r = 0; r < rows; ++r) {
+            QModelIndex index = fieldUi->model()->index(r, c);
+            fieldUi->setIndexWidget(index, new QLabel(""));
+        }
+    }
+
+    // Set pre-placed pieces
+    for (int i = 0; i < count; ++i) {
+        setPiece(prePlaced[i].row, prePlaced[i].col, prePlaced[i].piece, true);
+    }
+}
+
+int GameField::toIndex(int row, int col)
+{
+    return row * cols + col;
+}
+
+bool GameField::setPiece(int row, int col, const Piece* piece, bool fixed)
+{
+    qDebug() << "set piece" << row << col;
+
+    if (row < 0 || row >= rows || col < 0 || col >= cols) {
+        qWarning() << "Invalid piece index";
+        return false;
+    }
+
+    int index = toIndex(row, col);
+    if (field[index].piece->type == PieceNone) {
+        qDebug() << "really setting";
+        field[index].piece = piece;
+        field[index].fixed = fixed;
+
+        QString iconId = pieceToIconId(piece);
+        QModelIndex index = fieldUi->model()->index(row, col);
+        QLabel* label = (QLabel*)fieldUi->indexWidget(index);
+        label->setPixmap(QPixmap(iconId));
+
+        return true;
+    }
+    return false;
+}
+
+const Piece* GameField::pieceAt(int row, int col)
+{
+    if (row < 0 || row >= rows || col < 0 || col >= cols) {
+        qWarning() << "Invalid piece index";
+        return ppieces;
+    }
+
+    int index = toIndex(row, col);
+    return field[index].piece;
+}
+
+bool GameField::isPrePlaced(int row, int col)
+{
+    if (row < 0 || row >= rows || col < 0 || col >= cols) {
+        qWarning() << "Invalid piece index";
+        return false;
+    }
+
+    int index = toIndex(row, col);
+    return field[index].fixed;
+}
+
+void GameField::indicateFlow(int row, int col, Direction dir)
+{
+    // Indicate the flow: fill the piece in question with the
+    // liquid. (The piece can also be an empty one, or an illegal
+    // one.)
+    qDebug() << "ind flow" << row << col << dir;
+    if (row < 0 || col < 0 || row >= rows || col >= cols) {
+        return;
+    }
+    int index = toIndex(row, col);
+    if (dir != DirNone && (field[index].piece->flows[0] == dir || field[index].piece->flows[1] == dir)) {
+        field[index].flow[0] = true;
+    }
+    else if (dir != DirNone && (field[index].piece->flows[2] == dir || field[index].piece->flows[3] == dir)) {
+        field[index].flow[1] = true;
+    }
+    else if (dir == DirNone) {
+        // Flowing to a pipe from a wrong direction -> A hack to get
+        // the correct icon (same as an empty square flooded)
+        field[index].piece = ppieces;
+        field[index].flow[0] = true;
+        field[index].flow[1] = false;
+        
+    }
+    else {
+        qWarning() << "Indicate flow: illegal direction" << row << col << dir;
+        return;
+    }
+
+    QString iconId = pieceToIconId(field[index].piece, field[index].flow[0], field[index].flow[1]);
+    qDebug() << "icon id" << iconId;
+    QModelIndex mIndex = fieldUi->model()->index(row, col);
+    QLabel* label = (QLabel*)fieldUi->indexWidget(mIndex);
+
+    label->setPixmap(QPixmap(iconId));
+    // The pixmap won't show nicely if we're just sleeping...
+    QApplication::processEvents();
+}
+
+QHash<QPair<PieceType, int>, const Piece*> AvailablePieces::pieceCache;
+
+AvailablePieces::AvailablePieces(QTableWidget* ui)
+  : pieceUi(ui)
+{
+    connect(pieceUi, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)));
+
+    initPieceCache();
+
+    // Setup ui
+
+    qDebug() << pieceUi->rowCount() << pieceUi->columnCount();
+
+    for (int i = 0; i < 2; ++i)
+        pieceUi->setColumnWidth(i, 120);
+
+    for (int i = 0; i < 4; ++i)
+        pieceUi->setRowHeight(i, 70);
+
+    for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
+        if (ppieces[i].userCanAdd == false) continue;
+
+        //qDebug() << ppieces[i].type << ppieces[i].rotation;
+        QString fileName = pieceToIconId(&(ppieces[i]));
+
+        QTableWidgetItem* item = new QTableWidgetItem(QIcon(fileName), "0", QTableWidgetItem::UserType + pieceToId(&(ppieces[i])));
+
+        pieceUi->setItem(ppieces[i].uiRow, ppieces[i].uiColumn, item);
+    }
+}
+
+int AvailablePieces::pieceToId(const Piece* piece)
+{
+    return piece->type * 4 + piece->rotation/90;
+}
+
+const Piece* AvailablePieces::idToPiece(int id)
+{
+    int rotation = (id % 4)*90;
+    PieceType type = (PieceType)(id / 4);
+    QPair<PieceType, int> key(type, rotation);
+    if (!pieceCache.contains(key))
+        return ppieces;
+    return pieceCache[key];
+}
+
+void AvailablePieces::initPieceCache()
+{
+    for (int i = 0; ppieces[i].type != PiecesEnd; ++i)
+        pieceCache.insert(QPair<PieceType, int>(ppieces[i].type, ppieces[i].rotation), &ppieces[i]);
+}
+
+void AvailablePieces::initGame(int count, AvailablePiece* pieces)
+{
+    for (int i = 0; ppieces[i].type != PiecesEnd; ++i) {
+        if (ppieces[i].userCanAdd == false) continue;
+        pieceCounts.insert(&ppieces[i], 0);
+        pieceUi->item(ppieces[i].uiRow, ppieces[i].uiColumn)->setText(QString::number(0));
+    }
+
+    for (int i = 0; i < count; ++i) {
+        pieceCounts.insert(pieces[i].piece, pieces[i].count);
+        pieceUi->item(pieces[i].piece->uiRow, pieces[i].piece->uiColumn)->setText(QString::number(pieces[i].count));
+    }
+    pieceUi->clearSelection();
+}
+
+void AvailablePieces::onItemClicked(QTableWidgetItem* item)
+{
+    qDebug() << "piece clicked";
+    int id =  item->type() - QTableWidgetItem::UserType;
+
+    const Piece* piece = idToPiece(id);
+    if (piece->type != PieceNone && pieceCounts[piece] > 0) {
+         emit validPieceSelected(piece);
+    }
+    else
+        emit invalidPieceSelected();
+}
+
+void AvailablePieces::onPieceUsed(const Piece* piece)
+{
+    pieceCounts[piece]--;
+    pieceUi->item(piece->uiRow, piece->uiColumn)->setText(QString::number(pieceCounts[piece]));
+
+    // TODO: perhaps clear the selection
+    if (pieceCounts[piece] == 0)
+        emit invalidPieceSelected();
+}
+
+GameController::GameController(AvailablePieces* pieceUi, GameField* fieldUi, 
+                               QLabel* timeLabel, QPushButton* doneButton)
+    : pieceUi(pieceUi), fieldUi(fieldUi), 
+      timeLabel(timeLabel), doneButton(doneButton),
+      currentPiece(ppieces), rows(0), cols(0), timeLeft(0), levelRunning(false), neededFlow(0),
+      startRow(0), startCol(0), startDir(DirNone), flowRow(0), flowCol(0), flowDir(DirNone), flowPreplaced(0), flowScore(0)
+{
+    connect(fieldUi, SIGNAL(cellClicked(int, int)), this, SLOT(onCellClicked(int, int)));
+    connect(pieceUi, SIGNAL(invalidPieceSelected()), 
+            this, SLOT(onInvalidPieceSelected()));
+    connect(pieceUi, SIGNAL(validPieceSelected(const Piece*)), 
+            this, SLOT(onValidPieceSelected(const Piece*)));
+
+    connect(this, SIGNAL(pieceUsed(const Piece*)), pieceUi, SLOT(onPieceUsed(const Piece*)));
+
+    connect(doneButton, SIGNAL(clicked()), this, SLOT(levelEnds()));
+
+    // Setup the timer, but don't start it yet
+    timer.setInterval(1000);
+    timer.setSingleShot(false);
+    connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
+    timeLabel->setText("");
+
+    flowTimer.setInterval(500);
+    flowTimer.setSingleShot(false);
+    connect(&flowTimer, SIGNAL(timeout()), this, SLOT(computeFlow()));
+}
+
+void GameController::startLevel(QString fileName)
+{
+    // TODO: read the data while the user is reading the
+    // instructions...
+
+    // Read data about pre-placed pieces and available pieces from a
+    // text file.
+    QFile file(fileName);
+    if (!file.exists())
+        qFatal("Error reading game file: doesn't exist");
+
+    file.open(QIODevice::ReadOnly);
+    QTextStream gameData(&file);
+
+    gameData >> rows;
+    gameData >> cols;
+    qDebug() << rows << cols;
+    if (rows < 2 || rows > 10 || cols < 2 || cols > 10)
+        qFatal("Error reading game file: rows and cols");
+
+    neededFlow = 0;
+    int prePlacedCount = 0;
+    gameData >> prePlacedCount;
+    qDebug() << rows << cols;
+    if (prePlacedCount < 2 || prePlacedCount > 100)
+        qFatal("Error reading game file: piece count000");
+
+    PrePlacedPiece* prePlaced = new PrePlacedPiece[prePlacedCount];
+    for (int i = 0; i < prePlacedCount; ++i) {
+        int ix = 0;
+        gameData >> ix;
+        if (ix < 0 || ix >= noPieces)
+            qFatal("Error reading game file: no of pieces");
+        prePlaced[i].piece = &ppieces[ix];
+
+        // Record that the liquid must flow through this pre-placed
+        // piece (if it can)
+        neededFlow += flowCount(prePlaced[i].piece);
+
+        gameData >> prePlaced[i].row;
+        gameData >> prePlaced[i].col;
+        if (prePlaced[i].row < 0 || prePlaced[i].row >= rows || 
+            prePlaced[i].col < 0 || prePlaced[i].col >= cols)
+            qFatal("Error reading game file: piece position");
+
+        if (prePlaced[i].piece->type == PieceStart) {
+            startRow = prePlaced[i].row;
+            startCol = prePlaced[i].col;
+            startDir = prePlaced[i].piece->flows[0];
+        }
+    }
+    fieldUi->initGame(rows, cols, prePlacedCount, prePlaced);
+    delete[] prePlaced;
+
+    int availableCount = 0;
+    gameData >> availableCount;
+    if (availableCount < 2 || availableCount >= noPieces)
+        qFatal("Error reading game file: no of pieeces");
+
+    AvailablePiece* availablePieces = new AvailablePiece[availableCount];
+    for (int i = 0; i < availableCount; ++i) {
+        int ix = 0;
+        gameData >> ix;
+        if (ix < 0 || ix >= noPieces)
+            qFatal("Error reading game file: piece index");
+        availablePieces[i].piece = &ppieces[ix];
+        gameData >> availablePieces[i].count;
+        if (availablePieces[i].count < 0 || availablePieces[i].count > 100)
+            qFatal("Error reading game file: piece count");
+    }
+    pieceUi->initGame(availableCount, availablePieces);
+    delete[] availablePieces;
+
+    gameData >> timeLeft;
+    if (timeLeft < 0) 
+        qFatal("Error reading game file: time left");
+    timeLabel->setText(QString::number(timeLeft));
+
+    // Clear piece selection
+    onInvalidPieceSelected();
+
+    doneButton->setEnabled(true);
+    timer.start();
+    levelRunning = true;
+}
+
+void GameController::onTimeout()
+{
+    --timeLeft;
+    timeLabel->setText(QString::number(timeLeft));
+    if (timeLeft <= 0) {
+        timer.stop();
+        levelEnds();
+    }
+}
+
+void GameController::onCellClicked(int row, int column)
+{
+  qDebug() << "clicked: " << row << column;
+  if (!levelRunning) return;
+  if (currentPiece->type == PieceNone) return;
+  if (fieldUi->setPiece(row, column, currentPiece))
+      emit pieceUsed(currentPiece);
+}
+
+void GameController::onValidPieceSelected(const Piece* piece)
+{
+    qDebug() << "selected: " << piece->type << piece->rotation;
+    currentPiece = piece;
+}
+
+void GameController::onInvalidPieceSelected()
+{
+    currentPiece = ppieces;
+}
+
+void GameController::levelEnds()
+{
+    if (!levelRunning) return;
+
+    doneButton->setEnabled(false);
+    levelRunning = false;
+    timer.stop();
+
+    // Initiate computing the flow
+    flowRow = startRow;
+    flowCol = startCol;
+    flowDir = startDir;
+    flowPreplaced = 0;
+    flowScore = 0;
+    flowTimer.setInterval(500);
+    flowTimer.start();
+}
+
+void GameController::computeFlow()
+{
+    // We know:
+    // Where the flow currently is
+    // and which direction the flow goes after that piece
+    fieldUi->indicateFlow(flowRow, flowCol, flowDir);
+
+    if (flowDir == DirFailed) {
+        flowTimer.stop();
+        emit levelFailed();
+        return;
+    }
+
+    if (flowDir == DirNone) {
+        // This square contained no pipe or an incompatible pipe. Get
+        // some more time, so that the user sees the failure before we
+        // emit levelFailed.
+        flowDir = DirFailed;
+        flowTimer.setInterval(1000);
+        return;
+    }
+    flowScore += 10;
+
+    if (flowDir == DirDone) {
+        if (flowPreplaced < neededFlow) {
+            flowDir = DirFailed;
+            // TODO: indicate which pipes were missing
+            flowTimer.setInterval(1000);
+        }
+        else {
+            flowTimer.stop();
+            emit levelPassed(flowScore);
+        }
+        return;
+    }
+
+    // Compute where it flows next
+    if (flowDir == DirRight) {
+        ++flowCol;
+        flowDir = DirLeft;
+    }
+    else if (flowDir == DirLeft) {
+        --flowCol;
+        flowDir = DirRight;
+    }
+    else if (flowDir == DirUp) {
+        --flowRow;
+        flowDir = DirDown;
+    }
+    else if (flowDir == DirDown) {
+        ++flowRow;
+        flowDir = DirUp;
+    }
+
+    if (flowRow < 0 || flowCol < 0 || flowRow >= rows || flowCol >= cols) {
+        // Out of bounds
+        flowDir = DirFailed;
+        flowTimer.setInterval(1000);
+        return;
+    }
+
+    // Now we know the next piece and where the flow comes *from*
+    qDebug() << "flow to" << flowRow << flowCol;
+
+    // Check which piece is there
+    const Piece* piece = fieldUi->pieceAt(flowRow, flowCol);
+    qDebug() << "there is" << piece->type << piece->rotation;
+    flowDir = flowsTo(piece, flowDir);
+    // If the piece was pre-placed, record that the liquid has
+    // flown through it once
+    if (fieldUi->isPrePlaced(flowRow, flowCol))
+        flowPreplaced += 1;
+}
+
+LevelSwitcher::LevelSwitcher(GameController* gameController, QLabel* levelLabel, 
+                             QFrame* startFrame, QLabel* startTitle, QLabel* startLabel, QPushButton* startButton,
+                             QLabel* scoreLabel,
+
+                             QStringList levels)
+    : gameController(gameController), levelLabel(levelLabel), 
+      startFrame(startFrame), startTitle(startTitle), startLabel(startLabel), startButton(startButton),
+      scoreLabel(scoreLabel),
+      levels(levels), level(0), totalScore(0)
+{
+    connect(startButton, SIGNAL(clicked()), this, SLOT(onStartClicked()));
+    connect(gameController, SIGNAL(levelPassed(int)), this, SLOT(onLevelPassed(int)));
+    connect(gameController, SIGNAL(levelFailed()), this, SLOT(onLevelFailed()));
+    startTitle->setText("Starting a new game.");
+    scoreLabel->setText("0");
+    initiateLevel();
+}
+
+void LevelSwitcher::onStartClicked()
+{
+    startFrame->hide();
+    levelLabel->setText(QString::number(level+1));
+    gameController->startLevel(QString(LEVDIR) + "/" + levels[level] + ".dat");
+}
+
+void LevelSwitcher::initiateLevel()
+{
+    QFile file(QString(LEVDIR) + "/" + levels[level] + ".leg");
+    if (!file.exists())
+        qFatal("Error reading game file: doesn't exist");
+    file.open(QIODevice::ReadOnly);
+    QTextStream gameData(&file);
+
+    QString introText = gameData.readLine();
+    introText.replace("IMGDIR", IMGDIR);
+    startLabel->setText(introText);
+    startFrame->show();
+}
+
+void LevelSwitcher::onLevelPassed(int score)
+{
+    totalScore += score;
+    scoreLabel->setText(QString::number(score));
+
+    if (level < levels.size() - 1) {
+        ++ level;
+        startTitle->setText(QString("Level ") + QString::number(level) + QString(" passed, proceeding to level ") + QString::number(level+1));
+        initiateLevel();
+    }
+    else {
+        startTitle->setText(QString("All levels passed. Score: ") + QString::number(score));
+        startLabel->setText("Start a new game?");
+        // TODO: go to the level set selection screen
+        level = 0;
+        startFrame->show();
+    }
+}
+
+void LevelSwitcher::onLevelFailed()
+{
+    startTitle->setText(QString("Level ") + QString::number(level+1) + QString(" failed, try again!"));
+    initiateLevel();
+}
+
+// Todo next:
+// desktop stuff
+// icon for app manager
+// install all graphics
+// better graphics
+// save & load
+// level collections: introduction + basic
+// more levels
+// make fixed pipes look different than non-fixed ones
+// --------------
+// re-placing pieces
+// graphical hints on what to do next
+// graphical help, showing the ui elements: demo
+// "done" animation
+// level editor