File indexing completed on 2022-09-20 13:02:19

0001 /*
0002     SPDX-FileCopyrightText: 2008-2010 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "game.h"
0008 #include "board.h"
0009 #include "diamond.h"
0010 #include "settings.h"
0011 
0012 #include <cmath>
0013 #include <QPainter>
0014 #include <QRandomGenerator>
0015 #include <QTimerEvent>
0016 #include <KGamePopupItem>
0017 #include <KGameRenderer>
0018 #include <KgTheme>
0019 #include <KgThemeProvider>
0020 #include <KNotification>
0021 
0022 const int UpdateInterval = 40;
0023 
0024 Game::Game(KDiamond::GameState *state, KGameRenderer *renderer)
0025     : m_timerId(-1)
0026     , m_board(new KDiamond::Board(renderer))
0027     , m_gameState(state)
0028     , m_messenger(new KGamePopupItem)
0029 {
0030     connect(m_board, &KDiamond::Board::animationsFinished, this, &Game::animationFinished);
0031     connect(m_board, &KDiamond::Board::clicked, this, &Game::clickDiamond);
0032     connect(m_board, &KDiamond::Board::dragged, this, &Game::dragDiamond);
0033     //init scene (with some default scene size that makes board coordinates equal scene coordinates)
0034     const int minSize = m_board->gridSize();
0035     setSceneRect(0.0, 0.0, minSize, minSize);
0036     connect(this, &Game::sceneRectChanged, this, &Game::updateGraphics);
0037     connect(renderer->themeProvider(), &KgThemeProvider::currentThemeChanged, this, &Game::updateGraphics);
0038     addItem(m_board);
0039     //init messenger
0040     m_messenger->setMessageOpacity(0.8);
0041     m_messenger->setHideOnMouseClick(false);
0042     addItem(m_messenger);
0043     //init time management
0044     m_timerId = startTimer(UpdateInterval);
0045     //schedule late initialisation
0046     m_jobQueue << KDiamond::UpdateAvailableMovesJob;
0047 }
0048 
0049 //Checks amount of possible moves remaining
0050 void Game::getMoves()
0051 {
0052 #define C(X, Y) (m_board->hasDiamond(QPoint(X, Y)) ? m_board->diamond(QPoint(X, Y))->color() : KDiamond::Selection)
0053     m_availableMoves.clear();
0054     KDiamond::Color curColor;
0055     const int gridSize = m_board->gridSize();
0056     for (int x = 0; x < gridSize; ++x) {
0057         for (int y = 0; y < gridSize; ++y) {
0058             curColor = C(x, y);
0059             if (curColor == C(x + 1, y)) {
0060                 if (curColor == C(x - 2, y)) {
0061                     m_availableMoves.append(QPoint(x - 2, y));
0062                 }
0063                 if (curColor == C(x - 1, y - 1)) {
0064                     m_availableMoves.append(QPoint(x - 1, y - 1));
0065                 }
0066                 if (curColor == C(x - 1, y + 1)) {
0067                     m_availableMoves.append(QPoint(x - 1, y + 1));
0068                 }
0069                 if (curColor == C(x + 3, y)) {
0070                     m_availableMoves.append(QPoint(x + 3, y));
0071                 }
0072                 if (curColor == C(x + 2, y - 1)) {
0073                     m_availableMoves.append(QPoint(x + 2, y - 1));
0074                 }
0075                 if (curColor == C(x + 2, y + 1)) {
0076                     m_availableMoves.append(QPoint(x + 2, y + 1));
0077                 }
0078             }
0079             if (curColor == C(x + 2, y)) {
0080                 if (curColor == C(x + 1, y - 1)) {
0081                     m_availableMoves.append(QPoint(x + 1, y - 1));
0082                 }
0083                 if (curColor == C(x + 1, y + 1)) {
0084                     m_availableMoves.append(QPoint(x + 1, y + 1));
0085                 }
0086             }
0087             if (curColor == C(x, y + 1)) {
0088                 if (curColor == C(x, y - 2)) {
0089                     m_availableMoves.append(QPoint(x, y - 2));
0090                 }
0091                 if (curColor == C(x - 1, y - 1)) {
0092                     m_availableMoves.append(QPoint(x - 1, y - 1));
0093                 }
0094                 if (curColor == C(x + 1, y - 1)) {
0095                     m_availableMoves.append(QPoint(x + 1, y - 1));
0096                 }
0097                 if (curColor == C(x, y + 3)) {
0098                     m_availableMoves.append(QPoint(x, y + 3));
0099                 }
0100                 if (curColor == C(x - 1, y + 2)) {
0101                     m_availableMoves.append(QPoint(x - 1, y + 2));
0102                 }
0103                 if (curColor == C(x + 1, y + 2)) {
0104                     m_availableMoves.append(QPoint(x + 1, y + 2));
0105                 }
0106             }
0107             if (curColor == C(x, y + 2)) {
0108                 if (curColor == C(x - 1, y + 1)) {
0109                     m_availableMoves.append(QPoint(x - 1, y + 1));
0110                 }
0111                 if (curColor == C(x + 1, y + 1)) {
0112                     m_availableMoves.append(QPoint(x + 1, y + 1));
0113                 }
0114             }
0115         }
0116     }
0117 #undef C
0118     Q_EMIT numberMoves(m_availableMoves.size());
0119     if (m_availableMoves.isEmpty()) {
0120         m_board->clearSelection();
0121         m_gameState->setState(KDiamond::Finished);
0122     }
0123 }
0124 
0125 void Game::updateGraphics()
0126 {
0127     //calculate new metrics
0128     const QSize sceneSize = sceneRect().size().toSize();
0129     const int gridSize = m_board->gridSize();
0130     const int diamondSize = (int) floor(qMin(
0131                                             sceneSize.width() / (gridSize + 1.0), //the "+1" and "+0.5" make sure that some space is left on the window for the board border
0132                                             sceneSize.height() / (gridSize + 0.5)
0133                                         ));
0134     const int boardSize = gridSize * diamondSize;
0135     const int leftOffset = (sceneSize.width() - boardSize) / 2.0;
0136     QTransform t;
0137     t.translate(leftOffset, 0).scale(diamondSize, diamondSize);
0138     m_board->setTransform(t);
0139     //render background
0140     QPixmap pix = m_board->renderer()->spritePixmap(QStringLiteral("kdiamond-background"), sceneSize);
0141     const KgTheme *theme = m_board->renderer()->theme();
0142     const bool hasBorder = theme->customData(QStringLiteral("HasBorder")).toInt() > 0;
0143     if (hasBorder) {
0144         const qreal borderPercentage = theme->customData(QStringLiteral("BorderPercentage")).toFloat();
0145         const int padding = borderPercentage * boardSize;
0146         const int boardBorderSize = 2 * padding + boardSize;
0147         const QPixmap boardPix = m_board->renderer()->spritePixmap(QStringLiteral("kdiamond-border"), QSize(boardBorderSize, boardBorderSize));
0148         QPainter painter(&pix);
0149         painter.drawPixmap(QPoint(leftOffset - padding, -padding), boardPix);
0150     }
0151     m_backgroundPixmap = pix;
0152     update();
0153 }
0154 
0155 void Game::drawBackground(QPainter *painter, const QRectF &/*rect*/)
0156 {
0157     painter->drawPixmap(0, 0, m_backgroundPixmap);
0158 }
0159 
0160 void Game::clickDiamond(const QPoint &point)
0161 {
0162     if (m_gameState->state() != KDiamond::Playing) {
0163         return;
0164     }
0165     //do not allow more than two selections
0166     const bool isSelected = m_board->hasSelection(point);
0167     if (!isSelected && m_board->selections().count() == 2) {
0168         return;
0169     }
0170     //select only adjacent diamonds (i.e. if a distant diamond is selected, deselect the first one)
0171     const auto selections = m_board->selections();
0172     for (const QPoint &point2 : selections) {
0173         const int diff = qAbs(point2.x() - point.x()) + qAbs(point2.y() - point.y());
0174         if (diff > 1) {
0175             m_board->setSelection(point2, false);
0176         }
0177     }
0178     //toggle selection state
0179     m_board->setSelection(point, !isSelected);
0180     if (m_board->selections().count() == 2) {
0181         m_jobQueue << KDiamond::SwapDiamondsJob;
0182     }
0183 }
0184 
0185 void Game::dragDiamond(const QPoint &point, const QPoint &direction)
0186 {
0187     //direction must not be null, and must point along one axis
0188     if ((direction.x() == 0) ^ (direction.y() == 0)) {
0189         //find target indices
0190         const QPoint point2 = point + direction;
0191         if (!m_board->hasDiamond(point2)) {
0192             return;
0193         }
0194         //simulate the clicks involved in this operation
0195         m_board->clearSelection();
0196         m_board->setSelection(point, true);
0197         m_board->setSelection(point2, true);
0198         m_jobQueue << KDiamond::SwapDiamondsJob;
0199     }
0200 }
0201 
0202 void Game::timerEvent(QTimerEvent *event)
0203 {
0204     //propagate event to superclass if necessary
0205     if (event->timerId() != m_timerId) {
0206         QGraphicsScene::timerEvent(event);
0207         return;
0208     }
0209     //do not handle any jobs while animations are running
0210     if (m_gameState->state() == KDiamond::Paused || m_board->hasRunningAnimations()) {
0211         killTimer(m_timerId);
0212         m_timerId = -1;
0213         return;
0214     }
0215     //anything to do in this update?
0216     if (m_jobQueue.isEmpty()) {
0217         return;
0218     }
0219     //execute next job in queue
0220     const KDiamond::Job job = m_jobQueue.takeFirst();
0221     switch (job) {
0222     case KDiamond::SwapDiamondsJob: {
0223         if (m_board->selections().count() != 2) {
0224             break;    //this can be the case if, during a cascade, two diamonds are selected (inserts SwapDiamondsJob) and then deselected
0225         }
0226         //ensure that the selected diamonds are neighbors (this is not necessarily the case as diamonds can move to fill gaps)
0227         const QList<QPoint> points = m_board->selections();
0228         m_board->clearSelection();
0229         const int dx = qAbs(points[0].x() - points[1].x());
0230         const int dy = qAbs(points[0].y() - points[1].y());
0231         if (dx + dy != 1) {
0232             break;
0233         }
0234         //start a new cascade
0235         m_gameState->resetCascadeCounter();
0236         //copy selection info into another storage (to allow the user to select the next two diamonds while the cascade runs)
0237         m_swappingDiamonds = points;
0238         m_jobQueue << KDiamond::RemoveRowsJob; //We already insert this here to avoid another conditional statement.
0239     } //fall through
0240     case KDiamond::RevokeSwapDiamondsJob:
0241         //invoke movement
0242         KNotification::event(QStringLiteral("move"));
0243         m_board->swapDiamonds(m_swappingDiamonds[0], m_swappingDiamonds[1]);
0244         break;
0245     case KDiamond::RemoveRowsJob: {
0246         //find diamond rows and delete these diamonds
0247         const QList<QPoint> diamondsToRemove = findCompletedRows();
0248         if (diamondsToRemove.isEmpty()) {
0249             //no diamond rows were formed by the last move -> revoke movement (unless we are in a cascade)
0250             if (!m_swappingDiamonds.isEmpty()) {
0251                 m_jobQueue.prepend(KDiamond::RevokeSwapDiamondsJob);
0252             } else {
0253                 m_jobQueue << KDiamond::UpdateAvailableMovesJob;
0254             }
0255         } else {
0256             //all moves may now be out-dated - flush the moves list
0257             if (!m_availableMoves.isEmpty()) {
0258                 m_availableMoves.clear();
0259                 Q_EMIT numberMoves(-1);
0260             }
0261             //it is now safe to delete the position of the swapping diamonds
0262             m_swappingDiamonds.clear();
0263             //report to Game
0264             m_gameState->addPoints(diamondsToRemove.count());
0265             //invoke remove animation, then fill gaps immediately after the animation
0266             KNotification::event(QStringLiteral("remove"));
0267             for (const QPoint &diamondPos : std::as_const(diamondsToRemove)) {
0268                 m_board->removeDiamond(diamondPos);
0269             }
0270             m_jobQueue.prepend(KDiamond::FillGapsJob);
0271         }
0272         break;
0273     }
0274     case KDiamond::FillGapsJob:
0275         //fill gaps
0276         m_board->fillGaps();
0277         m_jobQueue.prepend(KDiamond::RemoveRowsJob); //allow cascades (i.e. clear rows that have been formed by falling diamonds)
0278         break;
0279     case KDiamond::UpdateAvailableMovesJob:
0280         if (m_gameState->state() != KDiamond::Finished) {
0281             getMoves();
0282         }
0283         break;
0284     case KDiamond::EndGameJob:
0285         Q_EMIT pendingAnimationsFinished();
0286         killTimer(m_timerId);
0287         m_timerId = -1;
0288         break;
0289     }
0290 }
0291 
0292 QList<QPoint> Game::findCompletedRows()
0293 {
0294     //The tactic of this function is brute-force. For now, I do not have a better idea: A friend of mine advised me to look in the environment of moved diamonds, but this is not easy since the re-filling after a deletion can lead to rows that are far away from the original movement. Therefore, we simply search through all diamonds looking for combinations in the horizonal and vertical direction.
0295     KDiamond::Color currentColor;
0296     QList<QPoint> diamonds;
0297     int x, y, xh, yh; //counters
0298     const int gridSize = m_board->gridSize();
0299 #define C(X, Y) m_board->diamond(QPoint(X, Y))->color()
0300     //searching in horizontal direction
0301     for (y = 0; y < gridSize; ++y) {
0302         for (x = 0; x < gridSize - 2; ++x) { //counter stops at gridSize - 2 to ensure availability of indices x + 1, x + 2
0303             currentColor = C(x, y);
0304             if (currentColor != C(x + 1, y)) {
0305                 continue;
0306             }
0307             if (currentColor != C(x + 2, y)) {
0308                 continue;
0309             }
0310             //If the execution is here, we have found a row of three diamonds starting at (x,y).
0311             diamonds << QPoint(x, y);
0312             diamonds << QPoint(x + 1, y);
0313             diamonds << QPoint(x + 2, y);
0314             //Does the row have even more elements?
0315             if (x + 3 >= gridSize) {
0316                 //impossible to locate more diamonds - do not go through the following loop
0317                 x += 2;
0318                 continue;
0319             }
0320             for (xh = x + 3; xh <= gridSize - 1; ++xh) {
0321                 if (currentColor == C(xh, y)) {
0322                     diamonds << QPoint(xh, y);
0323                 } else {
0324                     break;    //row has stopped before this diamond - no need to continue searching
0325                 }
0326             }
0327             x = xh - 1; //do not search at this position in the row anymore (add -1 because x is incremented before the next loop)
0328         }
0329     }
0330     //searching in vertical direction (essentially the same algorithm, just with swapped indices -> no comments here, read the comments above)
0331     for (x = 0; x < gridSize; ++x) {
0332         for (y = 0; y < gridSize - 2; ++y) {
0333             currentColor = C(x, y);
0334             if (currentColor != C(x, y + 1)) {
0335                 continue;
0336             }
0337             if (currentColor != C(x, y + 2)) {
0338                 continue;
0339             }
0340             diamonds << QPoint(x, y);
0341             diamonds << QPoint(x, y + 1);
0342             diamonds << QPoint(x, y + 2);
0343             if (y + 3 >= gridSize) {
0344                 y += 2;
0345                 continue;
0346             }
0347             for (yh = y + 3; yh <= gridSize - 1; ++yh) {
0348                 if (currentColor == C(x, yh)) {
0349                     diamonds << QPoint(x, yh);
0350                 } else {
0351                     break;
0352                 }
0353             }
0354             y = yh - 1;
0355         }
0356     }
0357 #undef C
0358     return diamonds;
0359 }
0360 
0361 void Game::showHint()
0362 {
0363     if (m_availableMoves.isEmpty() || !m_board->selections().isEmpty()) {
0364         return;
0365     }
0366     const QPoint location = m_availableMoves.value( QRandomGenerator::global()->bounded(m_availableMoves.size()));
0367     m_board->setSelection(location, true);
0368     m_gameState->removePoints(3);
0369 }
0370 
0371 void Game::animationFinished()
0372 {
0373     if (m_timerId == -1) {
0374         m_timerId = startTimer(UpdateInterval);
0375     }
0376 }
0377 
0378 void Game::stateChange(KDiamond::State state)
0379 {
0380     m_board->setPaused(state == KDiamond::Paused);
0381     switch ((int) state) {
0382     case KDiamond::Finished:
0383         m_board->clearSelection();
0384         m_jobQueue << KDiamond::EndGameJob;
0385         break;
0386     case KDiamond::Playing:
0387         if (m_timerId == -1) {
0388             m_timerId = startTimer(UpdateInterval);
0389         }
0390         break;
0391     }
0392 }
0393 
0394 void Game::message(const QString &message)
0395 {
0396     if (message.isEmpty()) {
0397         m_messenger->forceHide();
0398     } else {
0399         m_messenger->showMessage(message, KGamePopupItem::TopLeft);
0400     }
0401 }
0402