File indexing completed on 2024-03-24 04:04:53
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"