File indexing completed on 2024-04-28 07:51:31

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 <KGameGraphicsViewRenderer>
0018 #include <KGameTheme>
0019 #include <KGameThemeProvider>
0020 #include <KNotification>
0021 
0022 const int UpdateInterval = 40;
0023 
0024 Game::Game(KDiamond::GameState *state, KGameGraphicsViewRenderer *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(), &KGameThemeProvider::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 KGameTheme *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         [[fallthrough]];
0240     }
0241     case KDiamond::RevokeSwapDiamondsJob:
0242         //invoke movement
0243         KNotification::event(QStringLiteral("move"));
0244         m_board->swapDiamonds(m_swappingDiamonds[0], m_swappingDiamonds[1]);
0245         break;
0246     case KDiamond::RemoveRowsJob: {
0247         //find diamond rows and delete these diamonds
0248         const QList<QPoint> diamondsToRemove = findCompletedRows();
0249         if (diamondsToRemove.isEmpty()) {
0250             //no diamond rows were formed by the last move -> revoke movement (unless we are in a cascade)
0251             if (!m_swappingDiamonds.isEmpty()) {
0252                 m_jobQueue.prepend(KDiamond::RevokeSwapDiamondsJob);
0253             } else {
0254                 m_jobQueue << KDiamond::UpdateAvailableMovesJob;
0255             }
0256         } else {
0257             //all moves may now be out-dated - flush the moves list
0258             if (!m_availableMoves.isEmpty()) {
0259                 m_availableMoves.clear();
0260                 Q_EMIT numberMoves(-1);
0261             }
0262             //it is now safe to delete the position of the swapping diamonds
0263             m_swappingDiamonds.clear();
0264             //report to Game
0265             m_gameState->addPoints(diamondsToRemove.count());
0266             //invoke remove animation, then fill gaps immediately after the animation
0267             KNotification::event(QStringLiteral("remove"));
0268             for (const QPoint &diamondPos : std::as_const(diamondsToRemove)) {
0269                 m_board->removeDiamond(diamondPos);
0270             }
0271             m_jobQueue.prepend(KDiamond::FillGapsJob);
0272         }
0273         break;
0274     }
0275     case KDiamond::FillGapsJob:
0276         //fill gaps
0277         m_board->fillGaps();
0278         m_jobQueue.prepend(KDiamond::RemoveRowsJob); //allow cascades (i.e. clear rows that have been formed by falling diamonds)
0279         break;
0280     case KDiamond::UpdateAvailableMovesJob:
0281         if (m_gameState->state() != KDiamond::Finished) {
0282             getMoves();
0283         }
0284         break;
0285     case KDiamond::EndGameJob:
0286         Q_EMIT pendingAnimationsFinished();
0287         killTimer(m_timerId);
0288         m_timerId = -1;
0289         break;
0290     }
0291 }
0292 
0293 QList<QPoint> Game::findCompletedRows()
0294 {
0295     //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.
0296     KDiamond::Color currentColor;
0297     QList<QPoint> diamonds;
0298     int x, y, xh, yh; //counters
0299     const int gridSize = m_board->gridSize();
0300 #define C(X, Y) m_board->diamond(QPoint(X, Y))->color()
0301     //searching in horizontal direction
0302     for (y = 0; y < gridSize; ++y) {
0303         for (x = 0; x < gridSize - 2; ++x) { //counter stops at gridSize - 2 to ensure availability of indices x + 1, x + 2
0304             currentColor = C(x, y);
0305             if (currentColor != C(x + 1, y)) {
0306                 continue;
0307             }
0308             if (currentColor != C(x + 2, y)) {
0309                 continue;
0310             }
0311             //If the execution is here, we have found a row of three diamonds starting at (x,y).
0312             diamonds << QPoint(x, y);
0313             diamonds << QPoint(x + 1, y);
0314             diamonds << QPoint(x + 2, y);
0315             //Does the row have even more elements?
0316             if (x + 3 >= gridSize) {
0317                 //impossible to locate more diamonds - do not go through the following loop
0318                 x += 2;
0319                 continue;
0320             }
0321             for (xh = x + 3; xh <= gridSize - 1; ++xh) {
0322                 if (currentColor == C(xh, y)) {
0323                     diamonds << QPoint(xh, y);
0324                 } else {
0325                     break;    //row has stopped before this diamond - no need to continue searching
0326                 }
0327             }
0328             x = xh - 1; //do not search at this position in the row anymore (add -1 because x is incremented before the next loop)
0329         }
0330     }
0331     //searching in vertical direction (essentially the same algorithm, just with swapped indices -> no comments here, read the comments above)
0332     for (x = 0; x < gridSize; ++x) {
0333         for (y = 0; y < gridSize - 2; ++y) {
0334             currentColor = C(x, y);
0335             if (currentColor != C(x, y + 1)) {
0336                 continue;
0337             }
0338             if (currentColor != C(x, y + 2)) {
0339                 continue;
0340             }
0341             diamonds << QPoint(x, y);
0342             diamonds << QPoint(x, y + 1);
0343             diamonds << QPoint(x, y + 2);
0344             if (y + 3 >= gridSize) {
0345                 y += 2;
0346                 continue;
0347             }
0348             for (yh = y + 3; yh <= gridSize - 1; ++yh) {
0349                 if (currentColor == C(x, yh)) {
0350                     diamonds << QPoint(x, yh);
0351                 } else {
0352                     break;
0353                 }
0354             }
0355             y = yh - 1;
0356         }
0357     }
0358 #undef C
0359     return diamonds;
0360 }
0361 
0362 void Game::showHint()
0363 {
0364     if (m_availableMoves.isEmpty() || !m_board->selections().isEmpty()) {
0365         return;
0366     }
0367     const QPoint location = m_availableMoves.value( QRandomGenerator::global()->bounded(m_availableMoves.size()));
0368     m_board->setSelection(location, true);
0369     m_gameState->removePoints(3);
0370 }
0371 
0372 void Game::animationFinished()
0373 {
0374     if (m_timerId == -1) {
0375         m_timerId = startTimer(UpdateInterval);
0376     }
0377 }
0378 
0379 void Game::stateChange(KDiamond::State state)
0380 {
0381     m_board->setPaused(state == KDiamond::Paused);
0382     switch ((int) state) {
0383     case KDiamond::Finished:
0384         m_board->clearSelection();
0385         m_jobQueue << KDiamond::EndGameJob;
0386         break;
0387     case KDiamond::Playing:
0388         if (m_timerId == -1) {
0389             m_timerId = startTimer(UpdateInterval);
0390         }
0391         break;
0392     }
0393 }
0394 
0395 void Game::message(const QString &message)
0396 {
0397     if (message.isEmpty()) {
0398         m_messenger->forceHide();
0399     } else {
0400         m_messenger->showMessage(message, KGamePopupItem::TopLeft);
0401     }
0402 }
0403 
0404 #include "moc_game.cpp"