File indexing completed on 2024-04-14 04:01:09
0001 /* 0002 KShisen - A japanese game similar to Mahjongg 0003 SPDX-FileCopyrightText: 1997 Mario Weilguni <mweilguni@sime.com> 0004 SPDX-FileCopyrightText: 2002-2004 Dave Corrie <kde@davecorrie.com> 0005 SPDX-FileCopyrightText: 2007 Mauricio Piacentini <mauricio@tabuleiro.com> 0006 SPDX-FileCopyrightText: 2009-2016 Frederik Schwarzer <schwarzer@kde.org> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 // own 0012 #include "board.h" 0013 0014 // STL 0015 #include <algorithm> 0016 #include <array> 0017 0018 // Qt 0019 #include <QMouseEvent> 0020 #include <QPainter> 0021 #include <QStandardPaths> 0022 #include <QTimer> 0023 0024 // KF 0025 #include <KLocalizedString> 0026 #include <KMessageBox> 0027 0028 // KShisen 0029 #include "prefs.h" 0030 0031 namespace KShisen 0032 { 0033 #define EMPTY 0 0034 #define SEASONS_START 28 0035 #define FLOWERS_START 39 0036 0037 static std::array constexpr s_delay {1000, 750, 500, 250, 125}; 0038 static std::array constexpr s_sizeX {14, 16, 18, 24, 26, 30}; 0039 static std::array constexpr s_sizeY {6, 9, 8, 12, 14, 16}; 0040 0041 Board::Board(QWidget * parent) 0042 : QWidget(parent) 0043 , m_random(QRandomGenerator::global()->generate()) 0044 , m_soundPick(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-touch.ogg"))) 0045 , m_soundFall(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-fall-tile.ogg"))) 0046 { 0047 m_tileRemove1.setX(-1); 0048 0049 resetTimer(); 0050 0051 QPalette palette; 0052 palette.setBrush(backgroundRole(), m_background.getBackground()); 0053 setPalette(palette); 0054 0055 loadSettings(); 0056 } 0057 0058 void Board::loadSettings() 0059 { 0060 const QString tileSetBeforeLoad = Prefs::tileSet(); 0061 if (!loadTileset(tileSetBeforeLoad)) { 0062 const QString tileSetAfterLoad = Prefs::tileSet(); 0063 if (tileSetBeforeLoad.isEmpty()) { 0064 // This is when the user starts the app for the first time 0065 if (tileSetBeforeLoad != tileSetAfterLoad) { 0066 // we're good the default was loaded 0067 } else { 0068 KMessageBox::error(this 0069 , i18nc("%1 is a path to a tile set file", "An error occurred when loading the default tile set.\nPlease install the KMahjongg library.") 0070 , i18n("Error Loading Tiles") 0071 ); 0072 } 0073 } else { 0074 // This is when there was a tile set and has disappear, maybe the user or someone else removed it 0075 if (tileSetBeforeLoad != tileSetAfterLoad) { 0076 // warn the user tile set could not be loaded and the default one was loaded 0077 KMessageBox::information(this 0078 , i18nc("%1 is a path to a tile set file", "An error occurred when loading the tile set %1. The default tile set has been loaded.", tileSetBeforeLoad) 0079 , i18n("Error Loading Tiles") 0080 ); 0081 } else { 0082 // neither the user tile set nor the default could be loaded 0083 KMessageBox::error(this 0084 , i18nc("%1 is a path to a tile set file", "An error occurred when loading the tile set %1. The default tile set could also not be loaded.\nPlease install the KMahjongg library.", tileSetBeforeLoad) 0085 , i18n("Error Loading Tiles") 0086 ); 0087 } 0088 } 0089 } 0090 0091 // Load background 0092 const QString backgroundBeforeLoad = Prefs::background(); 0093 if (!loadBackground(backgroundBeforeLoad)) { 0094 const QString backgroundAfterLoad = Prefs::background(); 0095 if (backgroundBeforeLoad.isEmpty()) { 0096 // This is when the user starts the app for the first time 0097 if (backgroundBeforeLoad != backgroundAfterLoad) { 0098 // we're good the default was loaded 0099 } else { 0100 KMessageBox::error(this 0101 , i18nc("%1 is a path to a background image file", "An error occurred when loading the default background.\nPlease install the KMahjongg library.") 0102 , i18n("Error Loading Tiles") 0103 ); 0104 } 0105 } else { 0106 // This is when there was a background and has disappear, maybe the user or someone else removed it 0107 if (backgroundBeforeLoad != backgroundAfterLoad) { 0108 // warn the user background could not be loaded and the default one was loaded 0109 KMessageBox::information(this 0110 , i18nc("%1 is a path to a background image file", "An error occurred when loading the background %1. The default background has been loaded.", backgroundBeforeLoad) 0111 , i18n("Error Loading Tiles") 0112 ); 0113 } else { 0114 // neither the user background nor the default could be loaded 0115 KMessageBox::error(this 0116 , i18nc("%1 is a path to a background image file", "An error occurred when loading the background %1. The default background could also not be loaded.\nPlease install the KMahjongg library.", backgroundBeforeLoad) 0117 , i18n("Error Loading Tiles") 0118 ); 0119 } 0120 } 0121 } 0122 0123 // There are tile sets, that have only one tile for e.g. the flowers group. 0124 // If these tile sets are played in non-chineseStyle, this one tile face 0125 // appears too often and not every tile matches another one with the same 0126 // face because they are technically different (e.g different flowers). 0127 // The solution is to enforce chineseStyle gameplay for tile sets that are 0128 // known to be reduced. Those are Egypt and Alphabet for now. 0129 if (Prefs::tileSet().endsWith(QLatin1String("egypt.desktop")) || Prefs::tileSet().endsWith(QLatin1String("alphabet.desktop"))) { 0130 setChineseStyleFlag(true); 0131 } else { 0132 setChineseStyleFlag(Prefs::chineseStyle()); 0133 } 0134 setTilesCanSlideFlag(Prefs::tilesCanSlide()); 0135 // Need to load solvable before size because setSize calls newGame which 0136 // uses the solvable flag. Same with shuffle. 0137 setSolvableFlag(Prefs::solvable()); 0138 setShowUnsolvableMessageFlag(Prefs::showUnsolvableMessage()); 0139 m_shuffle = Prefs::level() * 4 + 1; 0140 setSize(s_sizeX.at(Prefs::size()), s_sizeY.at(Prefs::size())); 0141 setGravityFlag(Prefs::gravity()); 0142 setDelay(s_delay.at(Prefs::speed())); 0143 setSoundsEnabled(Prefs::sounds()); 0144 0145 if (m_level != Prefs::level()) { 0146 newGame(); 0147 } 0148 m_level = Prefs::level(); 0149 } 0150 0151 bool Board::loadTileset(QString const & pathToTileset) 0152 { 0153 if (m_tiles.loadTileset(pathToTileset)) { 0154 if (m_tiles.loadGraphics()) { 0155 Prefs::setTileSet(pathToTileset); 0156 Prefs::self()->save(); 0157 resizeBoard(); 0158 return true; 0159 } 0160 } 0161 //Try default 0162 if (m_tiles.loadDefault()) { 0163 if (m_tiles.loadGraphics()) { 0164 Prefs::setTileSet(m_tiles.path()); 0165 Prefs::self()->save(); 0166 resizeBoard(); 0167 } 0168 } 0169 return false; 0170 } 0171 0172 bool Board::loadBackground(QString const & pathToBackground) 0173 { 0174 if (m_background.load(pathToBackground, width(), height())) { 0175 if (m_background.loadGraphics()) { 0176 Prefs::setBackground(pathToBackground); 0177 Prefs::self()->save(); 0178 resizeBoard(); 0179 return true; 0180 } 0181 } 0182 //Try default 0183 if (m_background.loadDefault()) { 0184 if (m_background.loadGraphics()) { 0185 Prefs::setBackground(m_background.path()); 0186 Prefs::self()->save(); 0187 resizeBoard(); 0188 } 0189 } 0190 return false; 0191 } 0192 0193 int Board::xTiles() const 0194 { 0195 return m_xTiles; 0196 } 0197 0198 int Board::yTiles() const 0199 { 0200 return m_yTiles; 0201 } 0202 0203 int Board::tiles() const 0204 { 0205 return m_field.size(); 0206 } 0207 0208 void Board::setField(TilePos tilePos, int value) 0209 { 0210 if (!isValidPos(tilePos)) { 0211 qCCritical(KSHISEN_General) << "Attempted write to invalid field position:" 0212 << tilePos.x() 0213 << "," 0214 << tilePos.y(); 0215 } 0216 0217 m_field.at(tilePos.y() * xTiles() + tilePos.x()) = value; 0218 } 0219 0220 int Board::field(TilePos tilePos) const 0221 { 0222 if (!isValidPosWithOutline(tilePos)) { 0223 qCCritical(KSHISEN_General) << "Attempted read from invalid field position:" 0224 << tilePos.x() 0225 << "," 0226 << tilePos.y(); 0227 } 0228 0229 if (!isValidPos(tilePos)) { 0230 return EMPTY; 0231 } 0232 0233 return m_field.at(tilePos.y() * xTiles() + tilePos.x()); 0234 } 0235 0236 void Board::applyGravity() 0237 { 0238 if (!m_gravityFlag) { 0239 return; 0240 } 0241 for (decltype(xTiles()) column = 0; column < xTiles(); ++column) { 0242 auto rptr = yTiles() - 1; 0243 auto wptr = yTiles() - 1; 0244 while (rptr >= 0) { 0245 auto wptrPos = TilePos(column, wptr); 0246 if (field(wptrPos) != EMPTY) { 0247 --rptr; 0248 --wptr; 0249 } else { 0250 auto rptrPos = TilePos(column, rptr); 0251 if (field(rptrPos) != EMPTY) { 0252 setField(wptrPos, field(rptrPos)); 0253 setField(rptrPos, EMPTY); 0254 repaintTile(rptrPos); 0255 repaintTile(wptrPos); 0256 --wptr; 0257 --rptr; 0258 if (Prefs::sounds()) { 0259 m_soundFall.start(); 0260 } 0261 } else { 0262 --rptr; 0263 } 0264 } 0265 } 0266 } 0267 } 0268 0269 void Board::unmarkTile() 0270 { 0271 // if nothing is marked, nothing to do 0272 if (m_markX == -1 || m_markY == -1) { 0273 return; 0274 } 0275 drawPossibleMoves(false); 0276 m_possibleMoves.clear(); 0277 // We need to set m_markX and m_markY to -1 before calling 0278 // updateField() to ensure the tile is redrawn as unmarked. 0279 auto const oldTilePos = TilePos(m_markX, m_markY); 0280 m_markX = -1; 0281 m_markY = -1; 0282 repaintTile(oldTilePos); 0283 } 0284 0285 void Board::mousePressEvent(QMouseEvent * e) 0286 { 0287 // Do not process mouse events while the connection is drawn. 0288 // Clicking on one of the already connected tiles would have selected 0289 // it before removing it. This is more a workaround than a proper fix 0290 // but I have to understand the usage of m_paintConnection first in 0291 // order to consider its reuse here. (schwarzer) 0292 if (m_paintInProgress) { 0293 return; 0294 } 0295 switch (m_gameState) { 0296 case GameState::Normal: 0297 break; 0298 case GameState::Over: 0299 newGame(); 0300 return; 0301 case GameState::Paused: 0302 setPauseEnabled(false); 0303 return; 0304 case GameState::Stuck: 0305 return; 0306 } 0307 // Calculate field position 0308 auto posX = (e->pos().x() - xOffset()) / (m_tiles.qWidth() * 2); 0309 auto posY = (e->pos().y() - yOffset()) / (m_tiles.qHeight() * 2); 0310 0311 if (e->pos().x() < xOffset() || e->pos().y() < yOffset() || posX >= xTiles() || posY >= yTiles()) { 0312 posX = -1; 0313 posY = -1; 0314 } 0315 0316 // Mark tile 0317 if (e->button() == Qt::LeftButton) { 0318 clearHighlight(); 0319 0320 if (posX != -1) { 0321 marked(TilePos(posX, posY)); 0322 } else { 0323 // unmark when clicking outside the board 0324 unmarkTile(); 0325 } 0326 } 0327 0328 // Assist by highlighting all tiles of same type 0329 if (e->button() == Qt::RightButton) { 0330 auto const clickedTile = field(TilePos(posX, posY)); 0331 0332 // Clear marked tile 0333 if (m_markX != -1 && field(TilePos(m_markX, m_markY)) != clickedTile) { 0334 unmarkTile(); 0335 } else { 0336 m_markX = -1; 0337 m_markY = -1; 0338 } 0339 0340 // Perform highlighting 0341 if (clickedTile != m_highlightedTile) { 0342 auto const oldHighlighted = m_highlightedTile; 0343 m_highlightedTile = clickedTile; 0344 for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { 0345 for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { 0346 auto const fieldTile = field(TilePos(i, j)); 0347 if (fieldTile != EMPTY) { 0348 if (fieldTile == oldHighlighted) { 0349 repaintTile(TilePos(i, j)); 0350 } else if (fieldTile == clickedTile) { 0351 repaintTile(TilePos(i, j)); 0352 } else if (m_chineseStyleFlag) { 0353 if (isTileSeason(clickedTile) && isTileSeason(fieldTile)) { 0354 repaintTile(TilePos(i, j)); 0355 } else if (isTileFlower(clickedTile) && isTileFlower(fieldTile)) { 0356 repaintTile(TilePos(i, j)); 0357 } 0358 // oldHighlighted 0359 if (isTileSeason(oldHighlighted) && isTileSeason(fieldTile)) { 0360 repaintTile(TilePos(i, j)); 0361 } else if (isTileFlower(oldHighlighted) && isTileFlower(fieldTile)) { 0362 repaintTile(TilePos(i, j)); 0363 } 0364 } 0365 } 0366 } 0367 } 0368 } 0369 } 0370 } 0371 0372 int Board::xOffset() const 0373 { 0374 auto tw = m_tiles.qWidth() * 2; 0375 return (width() - (tw * xTiles())) / 2; 0376 } 0377 0378 int Board::yOffset() const 0379 { 0380 auto th = m_tiles.qHeight() * 2; 0381 return (height() - (th * yTiles())) / 2; 0382 } 0383 0384 void Board::setSize(int x, int y) 0385 { 0386 if (x == xTiles() && y == yTiles()) { 0387 return; 0388 } 0389 0390 m_field.resize(x * y); 0391 0392 m_xTiles = x; 0393 m_yTiles = y; 0394 0395 std::fill(m_field.begin(), m_field.end(), EMPTY); 0396 0397 // set the minimum size of the scalable window 0398 auto constexpr minScale = 0.2; 0399 auto const w = qRound(m_tiles.qWidth() * 2.0 * minScale) * xTiles() + m_tiles.width(); 0400 auto const h = qRound(m_tiles.qHeight() * 2.0 * minScale) * yTiles() + m_tiles.height(); 0401 0402 setMinimumSize(w, h); 0403 0404 resizeBoard(); 0405 newGame(); 0406 Q_EMIT changed(); 0407 } 0408 0409 void Board::resizeEvent(QResizeEvent * e) 0410 { 0411 qCDebug(KSHISEN_General) << "[resizeEvent]"; 0412 if (e->spontaneous()) { 0413 qCDebug(KSHISEN_General) << "[resizeEvent] spontaneous"; 0414 } 0415 resizeBoard(); 0416 Q_EMIT resized(); 0417 } 0418 0419 void Board::resizeBoard() 0420 { 0421 // calculate tile size required to fit all tiles in the window 0422 auto const newsize = m_tiles.preferredTileSize(QSize(width(), height()), xTiles(), yTiles()); 0423 m_tiles.reloadTileset(newsize); 0424 //recalculate bg, if needed 0425 m_background.sizeChanged(width(), height()); 0426 //reload our bg brush, using the cache in libkmahjongg if possible 0427 QPalette palette; 0428 palette.setBrush(backgroundRole(), m_background.getBackground()); 0429 setPalette(palette); 0430 } 0431 0432 0433 void Board::newGame() 0434 { 0435 m_gameState = GameState::Normal; 0436 setCheatModeEnabled(false); 0437 0438 m_markX = -1; 0439 m_markY = -1; 0440 m_highlightedTile = -1; // will clear previous highlight 0441 0442 resetUndo(); 0443 resetRedo(); 0444 m_connection.clear(); 0445 m_possibleMoves.clear(); 0446 0447 // distribute all tiles on board 0448 auto curTile = 1; 0449 auto tileCount = 0; 0450 0451 /* 0452 * Note by jwickers: i changed the way to distribute tiles 0453 * in chinese mahjongg there are 4 tiles of each 0454 * except flowers and seasons (4 flowers and 4 seasons, 0455 * but one unique tile of each, that is why they are 0456 * the only ones numbered) 0457 * That uses the chineseStyle flag 0458 */ 0459 for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { 0460 for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { 0461 // do not duplicate flowers or seasons 0462 if (!m_chineseStyleFlag || !(isTileSeason(curTile) || isTileFlower(curTile))) { 0463 setField(TilePos(x, y), curTile); 0464 if (++tileCount >= 4) { 0465 tileCount = 0; 0466 ++curTile; 0467 } 0468 } else { 0469 tileCount = 0; 0470 setField(TilePos(x, y), curTile++); 0471 } 0472 if (curTile > Board::nTiles) { 0473 curTile = 1; 0474 } 0475 } 0476 } 0477 0478 if (m_shuffle == 0) { 0479 update(); 0480 resetTimer(); 0481 Q_EMIT newGameStarted(); 0482 Q_EMIT changed(); 0483 return; 0484 } 0485 0486 // shuffle the field 0487 auto const tx = xTiles(); 0488 auto const ty = yTiles(); 0489 for (decltype(tx * ty * m_shuffle) i = 0; i < tx * ty * m_shuffle; ++i) { 0490 auto const tilePos1 = TilePos(m_random.bounded(tx), m_random.bounded(ty)); 0491 auto const tilePos2 = TilePos(m_random.bounded(tx), m_random.bounded(ty)); 0492 // keep and use t, because the next setField() call changes what field() will return 0493 // so there would a significant impact on shuffling with the field() call put into the 0494 // place where 't' is used 0495 auto const t = field(tilePos1); 0496 setField(tilePos1, field(tilePos2)); 0497 setField(tilePos2, t); 0498 } 0499 0500 // if m_solvableFlag is false, the game does not need to be solvable; we can drop out here 0501 if (!m_solvableFlag) { 0502 update(); 0503 resetTimer(); 0504 Q_EMIT newGameStarted(); 0505 Q_EMIT changed(); 0506 return; 0507 } 0508 0509 0510 auto oldfield = m_field; 0511 decltype(m_field) tiles(m_field.size()); 0512 decltype(m_field) pos(m_field.size()); 0513 //jwickers: in case the game cannot made solvable we do not want to run an infinite loop 0514 auto maxAttempts = 200; 0515 0516 while (!isSolvable(false) && maxAttempts > 0) { 0517 // generate a list of free tiles and positions 0518 auto numberOfTiles = 0; 0519 for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { 0520 if (m_field.at(i) != EMPTY) { 0521 pos.at(numberOfTiles) = i; 0522 tiles.at(numberOfTiles) = m_field.at(i); 0523 ++numberOfTiles; 0524 } 0525 } 0526 0527 // restore field 0528 m_field = oldfield; 0529 0530 // redistribute unsolved tiles 0531 while (numberOfTiles > 0) { 0532 // get a random tile 0533 auto const r1 = m_random.bounded(numberOfTiles); 0534 auto const r2 = m_random.bounded(numberOfTiles); 0535 auto const tile = tiles.at(r1); 0536 auto const apos = pos.at(r2); 0537 0538 // truncate list 0539 tiles.at(r1) = tiles.at(numberOfTiles - 1); 0540 pos.at(r2) = pos.at(numberOfTiles - 1); 0541 --numberOfTiles; 0542 0543 // put this tile on the new position 0544 m_field.at(apos) = tile; 0545 } 0546 0547 // remember field 0548 oldfield = m_field; 0549 --maxAttempts; 0550 } 0551 // debug, tell if make solvable failed 0552 if (maxAttempts == 0) { 0553 qCCritical(KSHISEN_General) << "NewGame make solvable failed"; 0554 } 0555 0556 0557 // restore field 0558 m_field = oldfield; 0559 0560 update(); 0561 resetTimer(); 0562 Q_EMIT changed(); 0563 } 0564 0565 bool Board::tilesMatch(int tile1, int tile2) const 0566 { 0567 // identical tiles always match 0568 if (tile1 == tile2) { 0569 return true; 0570 } 0571 // when chinese style is set, there are special rules 0572 // for flowers and seasons 0573 if (m_chineseStyleFlag) { 0574 // if both tiles are seasons 0575 if (isTileSeason(tile1) && isTileSeason(tile2)) { 0576 return true; 0577 } 0578 // if both tiles are flowers 0579 if (isTileFlower(tile1) && isTileFlower(tile2)) { 0580 return true; 0581 } 0582 } 0583 return false; 0584 } 0585 0586 bool Board::isTileHighlighted(TilePos tilePos) const 0587 { 0588 if (tilePos.x() == m_markX && tilePos.y() == m_markY) { 0589 return true; 0590 } 0591 0592 if (tilesMatch(m_highlightedTile, field(tilePos))) { 0593 return true; 0594 } 0595 0596 // m_tileRemove1.first != -1 is used because the repaint of the first if 0597 // on undrawConnection highlighted the tiles that fell because of gravity 0598 if (!m_connection.empty() && m_tileRemove1.x() != -1) { 0599 if (tilePos.x() == m_connection.front().x() && tilePos.y() == m_connection.front().y()) { 0600 return true; 0601 } 0602 0603 if (tilePos.x() == m_connection.back().x() && tilePos.y() == m_connection.back().y()) { 0604 return true; 0605 } 0606 } 0607 0608 return false; 0609 } 0610 0611 void Board::repaintTile(TilePos tilePos) 0612 { 0613 auto const r = QRect(xOffset() + tilePos.x() * m_tiles.qWidth() * 2, 0614 yOffset() + tilePos.y() * m_tiles.qHeight() * 2, 0615 m_tiles.width(), 0616 m_tiles.height()); 0617 0618 update(r); 0619 } 0620 0621 void Board::showInfoRect(QPainter & p, QString const & message) 0622 { 0623 auto const boxWidth = width() * 0.6; 0624 auto const boxHeight = height() * 0.6; 0625 auto const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight); 0626 QFont font; 0627 auto const fontsize = static_cast<int>(boxHeight / 13); 0628 font.setPointSize(fontsize); 0629 p.setFont(font); 0630 p.setBrush(QBrush(QColor(100, 100, 100, 150))); 0631 p.setRenderHint(QPainter::Antialiasing); 0632 p.drawRoundedRect(contentsRect, 10, 10); 0633 0634 p.drawText(contentsRect, Qt::AlignCenter | Qt::TextWordWrap, message); 0635 } 0636 0637 void Board::drawTiles(QPainter & p, QPaintEvent * e) 0638 { 0639 auto const w = m_tiles.width(); 0640 auto const h = m_tiles.height(); 0641 auto const fw = m_tiles.qWidth() * 2; 0642 auto const fh = m_tiles.qHeight() * 2; 0643 for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { 0644 for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { 0645 auto const tile = field(TilePos(i, j)); 0646 if (tile == EMPTY) { 0647 continue; 0648 } 0649 0650 auto const xpos = xOffset() + i * fw; 0651 auto const ypos = yOffset() + j * fh; 0652 auto const r = QRect(xpos, ypos, w, h); 0653 if (e->rect().intersects(r)) { 0654 if (isTileHighlighted(TilePos(i, j))) { 0655 p.drawPixmap(xpos, ypos, m_tiles.selectedTile(1)); 0656 } else { 0657 p.drawPixmap(xpos, ypos, m_tiles.unselectedTile(1)); 0658 } 0659 0660 //draw face 0661 p.drawPixmap(xpos, ypos, m_tiles.tileface(tile - 1)); 0662 } 0663 } 0664 } 0665 } 0666 0667 void Board::paintEvent(QPaintEvent * e) 0668 { 0669 auto const ur = e->rect(); // rectangle to update 0670 QPainter p(this); 0671 p.fillRect(ur, m_background.getBackground()); 0672 0673 switch (m_gameState) { 0674 case GameState::Normal: 0675 drawTiles(p, e); 0676 break; 0677 case GameState::Paused: 0678 showInfoRect(p, i18n("Game Paused\nClick to resume game.")); 0679 break; 0680 case GameState::Stuck: 0681 drawTiles(p, e); 0682 showInfoRect(p, i18n("Game Stuck\nNo more moves possible.")); 0683 break; 0684 case GameState::Over: 0685 showInfoRect(p, i18n("Game Over\nClick to start a new game.")); 0686 break; 0687 } 0688 0689 if (m_paintConnection) { 0690 p.setPen(QPen(QColor("red"), lineWidth())); 0691 0692 auto pt1 = m_connection.cbegin(); 0693 auto pt2 = pt1 + 1; 0694 while (pt2 != m_connection.cend()) { 0695 p.drawLine(midCoord(*pt1), midCoord(*pt2)); 0696 ++pt1; 0697 ++pt2; 0698 } 0699 QTimer::singleShot(delay(), this, &Board::undrawConnection); 0700 m_paintConnection = false; 0701 } 0702 if (m_paintPossibleMoves) { 0703 p.setPen(QPen(QColor("blue"), lineWidth())); 0704 // paint all possible moves 0705 for (auto const &move : std::as_const(m_possibleMoves)) { 0706 auto pt1 = move.path().cbegin(); 0707 auto pt2 = pt1 + 1; 0708 while (pt2 != move.path().cend()) { 0709 p.drawLine(midCoord(*pt1), midCoord(*pt2)); 0710 ++pt1; 0711 ++pt2; 0712 } 0713 } 0714 m_paintConnection = false; 0715 } 0716 p.end(); 0717 } 0718 0719 void Board::reverseSlide(TilePos tilePos, Slide const & slide) 0720 { 0721 // slide[XY]2 is the current location of the last tile to slide 0722 // slide[XY]1 is its destination 0723 // calculate the offset for the tiles to slide 0724 auto const dx = slide.front().x() - slide.back().x(); 0725 auto const dy = slide.front().y() - slide.back().y(); 0726 auto currentTile = 0; 0727 // move all tiles between slideX2, slideY2 and x, y to slide with that offset 0728 if (dx == 0) { 0729 if (tilePos.y() < slide.back().y()) { 0730 for (auto i = tilePos.y() + 1; i <= slide.back().y(); ++i) { 0731 currentTile = field(TilePos(tilePos.x(), i)); 0732 if (currentTile == EMPTY) { 0733 continue; 0734 } 0735 setField(TilePos(tilePos.x(), i), EMPTY); 0736 setField(TilePos(tilePos.x(), i + dy), currentTile); 0737 repaintTile(TilePos(tilePos.x(), i)); 0738 repaintTile(TilePos(tilePos.x(), i + dy)); 0739 } 0740 } else { 0741 for (auto i = tilePos.y() - 1; i >= slide.back().y(); --i) { 0742 currentTile = field(TilePos(tilePos.x(), i)); 0743 if (currentTile == EMPTY) { 0744 continue; 0745 } 0746 setField(TilePos(tilePos.x(), i), EMPTY); 0747 setField(TilePos(tilePos.x(), i + dy), currentTile); 0748 repaintTile(TilePos(tilePos.x(), i)); 0749 repaintTile(TilePos(tilePos.x(), i + dy)); 0750 } 0751 } 0752 } else if (dy == 0) { 0753 if (tilePos.x() < slide.back().x()) { 0754 for (auto i = tilePos.x() + 1; i <= slide.back().x(); ++i) { 0755 currentTile = field(TilePos(i, tilePos.y())); 0756 if (currentTile == EMPTY) { 0757 continue; 0758 } 0759 setField(TilePos(i, tilePos.y()), EMPTY); 0760 setField(TilePos(i + dx, tilePos.y()), currentTile); 0761 repaintTile(TilePos(i, tilePos.y())); 0762 repaintTile(TilePos(i + dx, tilePos.y())); 0763 } 0764 } else { 0765 for (auto i = tilePos.x() - 1; i >= slide.back().x(); --i) { 0766 currentTile = field(TilePos(i, tilePos.y())); 0767 if (currentTile == EMPTY) { 0768 continue; 0769 } 0770 setField(TilePos(i, tilePos.y()), EMPTY); 0771 setField(TilePos(i + dx, tilePos.y()), currentTile); 0772 repaintTile(TilePos(i, tilePos.y())); 0773 repaintTile(TilePos(i + dx, tilePos.y())); 0774 } 0775 } 0776 } 0777 } 0778 0779 void Board::performSlide(TilePos tilePos, Slide const & slide) 0780 { 0781 // check if there is something to slide 0782 if (slide.empty()) { 0783 return; 0784 } 0785 0786 // slide.first is the current location of the last tile to slide 0787 // slide.last is its destination 0788 // calculate the offset for the tiles to slide 0789 auto const dx = slide.back().x() - slide.front().x(); 0790 auto const dy = slide.back().y() - slide.front().y(); 0791 auto currentTile = 0; 0792 // move all tiles between m_markX, m_markY and the last tile to slide with that offset 0793 if (dx == 0) { 0794 if (tilePos.y() < slide.front().y()) { 0795 for (auto i = slide.front().y(); i > tilePos.y(); --i) { 0796 currentTile = field(TilePos(tilePos.x(), i)); 0797 setField(TilePos(tilePos.x(), i), EMPTY); 0798 setField(TilePos(tilePos.x(), i + dy), currentTile); 0799 repaintTile(TilePos(tilePos.x(), i)); 0800 repaintTile(TilePos(tilePos.x(), i + dy)); 0801 } 0802 } else { 0803 for (auto i = slide.front().y(); i < tilePos.y(); ++i) { 0804 currentTile = field(TilePos(tilePos.x(), i)); 0805 setField(TilePos(tilePos.x(), i), EMPTY); 0806 setField(TilePos(tilePos.x(), i + dy), currentTile); 0807 repaintTile(TilePos(tilePos.x(), i)); 0808 repaintTile(TilePos(tilePos.x(), i + dy)); 0809 } 0810 } 0811 } else if (dy == 0) { 0812 if (tilePos.x() < slide.front().x()) { 0813 for (auto i = slide.front().x(); i > tilePos.x(); --i) { 0814 currentTile = field(TilePos(i, tilePos.y())); 0815 setField(TilePos(i, tilePos.y()), EMPTY); 0816 setField(TilePos(i + dx, tilePos.y()), currentTile); 0817 repaintTile(TilePos(i, tilePos.y())); 0818 repaintTile(TilePos(i + dx, tilePos.y())); 0819 } 0820 } else { 0821 for (auto i = slide.front().x(); i < tilePos.x(); ++i) { 0822 currentTile = field(TilePos(i, tilePos.y())); 0823 setField(TilePos(i, tilePos.y()), EMPTY); 0824 setField(TilePos(i + dx, tilePos.y()), currentTile); 0825 repaintTile(TilePos(i, tilePos.y())); 0826 repaintTile(TilePos(i + dx, tilePos.y())); 0827 } 0828 } 0829 } 0830 } 0831 0832 void Board::performMove(PossibleMove & possibleMoves) 0833 { 0834 m_connection = possibleMoves.path(); 0835 #ifdef DEBUGGING 0836 // DEBUG undo, save board state 0837 auto saved1 = m_field; 0838 #endif 0839 // if the tiles can slide, we have to update the slided tiles too 0840 // and store the slide in a Move 0841 if (possibleMoves.hasSlide()) { 0842 performSlide(TilePos(m_markX, m_markY), possibleMoves.slide()); 0843 madeMove(TilePos(m_markX, m_markY), TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y()), possibleMoves.slide()); 0844 } else { 0845 madeMove(TilePos(m_markX, m_markY), TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y())); 0846 } 0847 drawPossibleMoves(false); 0848 drawConnection(); 0849 m_tileRemove1 = TilePos(m_markX, m_markY); 0850 m_tileRemove2 = TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y()); 0851 m_markX = -1; 0852 m_markY = -1; 0853 m_possibleMoves.clear(); 0854 #ifdef DEBUGGING 0855 // DEBUG undo, force gravity 0856 undrawConnection(); 0857 // DEBUG undo, save board2 state 0858 auto saved2 = m_field; 0859 decltype(m_field) saved3; // after undo 0860 decltype(m_field) saved4; // after redo 0861 // DEBUG undo, undo move 0862 auto errorFound = false; 0863 if (canUndo()) { 0864 undo(); 0865 // DEBUG undo, compare to saved board state 0866 for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { 0867 if (saved1.at(i) != m_field.at(i)) { 0868 qCDebug(KSHISEN_General) << "[DEBUG Undo 1], tile (" << i << ") was" << saved1.at(i) << "before move, it is" << m_field.at(i) << "after undo."; 0869 errorFound = true; 0870 } 0871 } 0872 // DEBUG undo, save board state 0873 saved3 = m_field; 0874 // DEBUG undo, redo 0875 if (canRedo()) { 0876 redo(); 0877 undrawConnection(); 0878 // DEBUG undo, compare to saved board2 state 0879 for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { 0880 if (saved2.at(i) != m_field.at(i)) { 0881 qCDebug(KSHISEN_General) << "[DEBUG Undo 2], tile (" << i << ") was" << saved2.at(i) << "after move, it is" << m_field.at(i) << "after redo."; 0882 errorFound = true; 0883 } 0884 } 0885 // DEBUG undo, save board state 0886 saved4 = m_field; 0887 } 0888 } 0889 // dumpBoard on error 0890 if (errorFound) { 0891 qCDebug(KSHISEN_General) << "[DEBUG] Before move"; 0892 dumpBoard(saved1); 0893 qCDebug(KSHISEN_General) << "[DEBUG] After move"; 0894 dumpBoard(saved2); 0895 qCDebug(KSHISEN_General) << "[DEBUG] Undo"; 0896 dumpBoard(saved3); 0897 qCDebug(KSHISEN_General) << "[DEBUG] Redo"; 0898 dumpBoard(saved4); 0899 } 0900 0901 #endif 0902 } 0903 0904 void Board::marked(TilePos tilePos) 0905 { 0906 if (field(tilePos) == EMPTY) { // click on empty space on the board 0907 if (m_possibleMoves.size() > 1) { // if the click is on any of the current possible moves, make that move 0908 for (auto move : std::as_const(m_possibleMoves)) { 0909 if (move.isInPath(tilePos)) { 0910 performMove(move); 0911 Q_EMIT selectATile(); 0912 return; 0913 } 0914 } 0915 } else { 0916 // unmark when not clicking on a tile 0917 unmarkTile(); 0918 return; 0919 } 0920 } 0921 // make sure that the previous connection is correctly undrawn 0922 undrawConnection(); // is this still needed? (schwarzer) 0923 0924 if (Prefs::sounds()) { 0925 m_soundPick.start(); 0926 } 0927 0928 if (tilePos.x() == m_markX && tilePos.y() == m_markY) { // the piece is already marked 0929 // unmark the piece 0930 unmarkTile(); 0931 Q_EMIT selectATile(); 0932 return; 0933 } 0934 0935 if (m_markX == -1) { // nothing is selected so far 0936 m_markX = tilePos.x(); 0937 m_markY = tilePos.y(); 0938 drawPossibleMoves(false); 0939 m_possibleMoves.clear(); 0940 repaintTile(tilePos); 0941 Q_EMIT selectAMatchingTile(); 0942 return; 0943 } 0944 if (m_possibleMoves.size() > 1) { // if the click is on any of the current possible moves, make that move 0945 0946 for (auto move : std::as_const(m_possibleMoves)) { 0947 if (move.isInPath(tilePos)) { 0948 performMove(move); 0949 Q_EMIT selectATile(); 0950 return; 0951 } 0952 } 0953 } 0954 0955 auto const tile1 = field(TilePos(m_markX, m_markY)); 0956 auto const tile2 = field(tilePos); 0957 0958 // both tiles do not match 0959 if (!tilesMatch(tile1, tile2)) { 0960 unmarkTile(); 0961 Q_EMIT tilesDoNotMatch(); 0962 return; 0963 } 0964 0965 // trace and perform the move and get the list of possible moves 0966 if (findPath(TilePos(m_markX, m_markY), tilePos, m_possibleMoves) > 0) { 0967 if (m_possibleMoves.size() > 1) { 0968 auto withSlide = 0; 0969 for (auto const &move : std::as_const(m_possibleMoves)) { 0970 move.Debug(); 0971 if (move.hasSlide()) { 0972 ++withSlide; 0973 } 0974 } 0975 // if all moves have no slide, it doesn't matter 0976 if (withSlide > 0) { 0977 drawPossibleMoves(true); 0978 Q_EMIT selectAMove(); 0979 return; 0980 } 0981 } 0982 0983 // only one move possible, perform it 0984 performMove(m_possibleMoves.front()); 0985 Q_EMIT selectATile(); 0986 // game is over? 0987 // Must delay until after tiles fall to make this test 0988 // See undrawConnection GP. 0989 } else { 0990 Q_EMIT invalidMove(); 0991 m_connection.clear(); 0992 } 0993 } 0994 0995 0996 void Board::clearHighlight() 0997 { 0998 if (m_highlightedTile == -1) { 0999 return; 1000 } 1001 auto const oldHighlighted = m_highlightedTile; 1002 m_highlightedTile = -1; 1003 1004 for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { 1005 for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { 1006 if (tilesMatch(oldHighlighted, field(TilePos(i, j)))) { 1007 repaintTile(TilePos(i, j)); 1008 } 1009 } 1010 } 1011 } 1012 1013 bool Board::canMakePath(TilePos tilePos1, TilePos tilePos2) const 1014 { 1015 if (tilePos1.x() == tilePos2.x()) { 1016 for (auto i = qMin(tilePos1.y(), tilePos2.y()) + 1; i < qMax(tilePos1.y(), tilePos2.y()); ++i) { 1017 if (field(TilePos(tilePos1.x(), i)) != EMPTY) { 1018 return false; 1019 } 1020 } 1021 return true; 1022 } 1023 1024 if (tilePos1.y() == tilePos2.y()) { 1025 for (auto i = qMin(tilePos1.x(), tilePos2.x()) + 1; i < qMax(tilePos1.x(), tilePos2.x()); ++i) { 1026 if (field(TilePos(i, tilePos1.y())) != EMPTY) { 1027 return false; 1028 } 1029 } 1030 return true; 1031 } 1032 1033 return false; 1034 } 1035 1036 bool Board::canSlideTiles(TilePos tilePos1, TilePos tilePos2, Slide & slide) const 1037 { 1038 auto distance = -1; 1039 slide.clear(); 1040 if (tilePos1.x() == tilePos2.x()) { 1041 if (tilePos1.y() > tilePos2.y()) { 1042 distance = tilePos1.y() - tilePos2.y(); 1043 // count how much free space we have for sliding 1044 auto startFree = -1; 1045 auto endFree = -1; 1046 // find first tile empty 1047 for (auto i = tilePos1.y() - 1; i >= 0; --i) { 1048 if (field(TilePos(tilePos1.x(), i)) == EMPTY) { 1049 startFree = i; 1050 break; 1051 } 1052 } 1053 // if not found, cannot slide 1054 // if the first free tile is just next to the sliding tile, no slide (should be a normal move) 1055 if (startFree == -1 || startFree == (tilePos1.y() - 1)) { 1056 return false; 1057 } 1058 // find last tile empty 1059 for (auto i = startFree - 1; i >= 0; --i) { 1060 if (field(TilePos(tilePos1.x(), i)) != EMPTY) { 1061 endFree = i; 1062 break; 1063 } 1064 } 1065 // if not found, it is the border: 0 1066 1067 // so we can slide of start_free - end_free, compare this to the distance 1068 if (distance <= (startFree - endFree)) { 1069 // first position of the last slided tile 1070 slide.push_back(TilePos(tilePos1.x(), startFree + 1)); 1071 // final position of the last slided tile 1072 slide.push_back(TilePos(tilePos1.x(), startFree + 1 - distance)); 1073 return true; 1074 } 1075 return false; 1076 1077 } else if (tilePos2.y() > tilePos1.y()) { 1078 distance = tilePos2.y() - tilePos1.y(); 1079 // count how much free space we have for sliding 1080 auto startFree = -1; 1081 auto endFree = yTiles(); 1082 // find first tile empty 1083 for (auto i = tilePos1.y() + 1; i < yTiles(); ++i) { 1084 if (field(TilePos(tilePos1.x(), i)) == EMPTY) { 1085 startFree = i; 1086 break; 1087 } 1088 } 1089 // if not found, cannot slide 1090 // if the first free tile is just next to the sliding tile, no slide (should be a normal move) 1091 if (startFree == -1 || startFree == tilePos1.y() + 1) { 1092 return false; 1093 } 1094 // find last tile empty 1095 for (auto i = startFree + 1; i < yTiles(); ++i) { 1096 if (field(TilePos(tilePos1.x(), i)) != EMPTY) { 1097 endFree = i; 1098 break; 1099 } 1100 } 1101 // if not found, it is the border: yTiles()-1 1102 1103 // so we can slide of end_free - start_free, compare this to the distance 1104 if (distance <= (endFree - startFree)) { 1105 // first position of the last slidden tile 1106 slide.push_back(TilePos(tilePos1.x(), startFree - 1)); 1107 // final position of the last slidden tile 1108 slide.push_back(TilePos(tilePos1.x(), startFree - 1 + distance)); 1109 return true; 1110 } 1111 return false; 1112 } 1113 // y1 == y2 ?! 1114 return false; 1115 } 1116 1117 if (tilePos1.y() == tilePos2.y()) { 1118 if (tilePos1.x() > tilePos2.x()) { 1119 distance = tilePos1.x() - tilePos2.x(); 1120 // count how much free space we have for sliding 1121 auto startFree = -1; 1122 auto endFree = -1; 1123 // find first tile empty 1124 for (auto i = tilePos1.x() - 1; i >= 0; --i) { 1125 if (field(TilePos(i, tilePos1.y())) == EMPTY) { 1126 startFree = i; 1127 break; 1128 } 1129 } 1130 // if not found, cannot slide 1131 // if the first free tile is just next to the sliding tile, no slide (should be a normal move) 1132 if (startFree == -1 || startFree == tilePos1.x() - 1) { 1133 return false; 1134 } 1135 // find last tile empty 1136 for (auto i = startFree - 1; i >= 0; --i) { 1137 if (field(TilePos(i, tilePos1.y())) != EMPTY) { 1138 endFree = i; 1139 break; 1140 } 1141 } 1142 // if not found, it is the border: 0 1143 1144 // so we can slide of start_free - end_free, compare this to the distance 1145 if (distance <= (startFree - endFree)) { 1146 // first position of the last slidden tile 1147 slide.push_back(TilePos(startFree + 1, tilePos1.y())); 1148 // final position of the last slidden tile 1149 slide.push_back(TilePos(startFree + 1 - distance, tilePos1.y())); 1150 return true; 1151 } 1152 return false; 1153 1154 } else if (tilePos2.x() > tilePos1.x()) { 1155 distance = tilePos2.x() - tilePos1.x(); 1156 // count how much free space we have for sliding 1157 auto startFree = -1; 1158 auto endFree = xTiles(); 1159 // find first tile empty 1160 for (auto i = tilePos1.x() + 1; i < xTiles(); ++i) { 1161 if (field(TilePos(i, tilePos1.y())) == EMPTY) { 1162 startFree = i; 1163 break; 1164 } 1165 } 1166 // if not found, cannot slide 1167 // if the first free tile is just next to the sliding tile, no slide (should be a normal move) 1168 if (startFree == -1 || startFree == tilePos1.x() + 1) { 1169 return false; 1170 } 1171 // find last tile empty 1172 for (auto i = startFree + 1; i < xTiles(); ++i) { 1173 if (field(TilePos(i, tilePos1.y())) != EMPTY) { 1174 endFree = i; 1175 break; 1176 } 1177 } 1178 // if not found, it is the border: xTiles()-1 1179 1180 // so we can slide of endFree - startFree, compare this to the distance 1181 if (distance <= (endFree - startFree)) { 1182 // first position of the last slidden tile 1183 slide.push_back(TilePos(startFree - 1, tilePos1.y())); 1184 // final position of the last slidden tile 1185 slide.push_back(TilePos(startFree - 1 + distance, tilePos1.y())); 1186 return true; 1187 } 1188 return false; 1189 } 1190 // x1 == x2 ?! 1191 return false; 1192 } 1193 return false; 1194 } 1195 1196 int Board::findPath(TilePos tilePos1, TilePos tilePos2, PossibleMoves & possibleMoves) const 1197 { 1198 possibleMoves.clear(); 1199 1200 auto simplePath = 0; 1201 1202 // first find the simple paths 1203 auto numberOfPaths = findSimplePath(tilePos1, tilePos2, possibleMoves); 1204 1205 // if the tiles can slide, 2 lines max is allowed 1206 if (m_tilesCanSlideFlag) { 1207 return numberOfPaths; 1208 } 1209 1210 // Find paths of 3 segments 1211 std::array constexpr dx {1, 0, -1, 0}; 1212 std::array constexpr dy {0, 1, 0, -1}; 1213 1214 for (auto i = 0; i < 4; ++i) { 1215 auto tempX = tilePos1.x() + dx.at(i); 1216 auto tempY = tilePos1.y() + dy.at(i); 1217 while (isValidPosWithOutline(TilePos(tempX, tempY)) && field(TilePos(tempX, tempY)) == EMPTY) { 1218 if ((simplePath = findSimplePath(TilePos(tempX, tempY), tilePos2, possibleMoves)) > 0) { 1219 possibleMoves.back().prependTile(tilePos1); 1220 numberOfPaths += simplePath; 1221 } 1222 tempX += dx.at(i); 1223 tempY += dy.at(i); 1224 } 1225 } 1226 return numberOfPaths; 1227 } 1228 1229 int Board::findSimplePath(TilePos tilePos1, TilePos tilePos2, PossibleMoves & possibleMoves) const 1230 { 1231 auto numberOfPaths = 0; 1232 Path path; 1233 // Find direct line (path of 1 segment) 1234 if (canMakePath(tilePos1, tilePos2)) { 1235 path.push_back(tilePos1); 1236 path.push_back(tilePos2); 1237 possibleMoves.push_back(PossibleMove(path)); 1238 ++numberOfPaths; 1239 } 1240 1241 // If the tiles are in the same row or column, then a 1242 // a 'simple path' cannot be found between them 1243 // That is, canMakePath should have returned true above if 1244 // that was possible 1245 if (tilePos1.x() == tilePos2.x() || tilePos1.y() == tilePos2.y()) { 1246 return numberOfPaths; 1247 } 1248 1249 // I isolate the special code when tiles can slide even if it duplicates code for now 1250 // Can we make a path sliding tiles ?, the slide move is always first, then a normal path 1251 if (m_tilesCanSlideFlag) { 1252 Slide slide; 1253 // Find path of 2 segments (route A) 1254 if (canSlideTiles(tilePos1, TilePos(tilePos2.x(), tilePos1.y()), slide) 1255 && canMakePath(TilePos(tilePos2.x(), tilePos1.y()), tilePos2)) { 1256 path.clear(); 1257 path.push_back(tilePos1); 1258 path.push_back(TilePos(tilePos2.x(), tilePos1.y())); 1259 path.push_back(tilePos2); 1260 possibleMoves.push_back(PossibleMove(path, slide)); 1261 ++numberOfPaths; 1262 } 1263 1264 // Find path of 2 segments (route B) 1265 if (canSlideTiles(tilePos1, TilePos(tilePos1.x(), tilePos2.y()), slide) 1266 && canMakePath(TilePos(tilePos1.x(), tilePos2.y()), tilePos2)) { 1267 path.clear(); 1268 path.push_back(tilePos1); 1269 path.push_back(TilePos(tilePos1.x(), tilePos2.y())); 1270 path.push_back(tilePos2); 1271 possibleMoves.push_back(PossibleMove(path, slide)); 1272 ++numberOfPaths; 1273 } 1274 } 1275 1276 // Even if tiles can slide, a path could still be done without sliding 1277 1278 // Find path of 2 segments (route A) 1279 if (field(TilePos(tilePos2.x(), tilePos1.y())) == EMPTY 1280 && canMakePath(tilePos1, TilePos(tilePos2.x(), tilePos1.y())) 1281 && canMakePath(TilePos(tilePos2.x(), tilePos1.y()), tilePos2)) { 1282 path.clear(); 1283 path.push_back(tilePos1); 1284 path.push_back(TilePos(tilePos2.x(), tilePos1.y())); 1285 path.push_back(tilePos2); 1286 possibleMoves.push_back(PossibleMove(path)); 1287 ++numberOfPaths; 1288 } 1289 1290 // Find path of 2 segments (route B) 1291 if (field(TilePos(tilePos1.x(), tilePos2.y())) == EMPTY 1292 && canMakePath(tilePos1, TilePos(tilePos1.x(), tilePos2.y())) 1293 && canMakePath(TilePos(tilePos1.x(), tilePos2.y()), tilePos2)) { 1294 path.clear(); 1295 path.push_back(tilePos1); 1296 path.push_back(TilePos(tilePos1.x(), tilePos2.y())); 1297 path.push_back(tilePos2); 1298 possibleMoves.push_back(PossibleMove(path)); 1299 ++numberOfPaths; 1300 } 1301 1302 return numberOfPaths; 1303 } 1304 1305 void Board::drawPossibleMoves(bool b) 1306 { 1307 if (m_possibleMoves.empty()) { 1308 return; 1309 } 1310 1311 m_paintPossibleMoves = b; 1312 update(); 1313 } 1314 1315 void Board::drawConnection() 1316 { 1317 m_paintInProgress = true; 1318 if (m_connection.empty()) { 1319 return; 1320 } 1321 1322 auto const tile1 = TilePos(m_connection.front().x(), m_connection.front().y()); 1323 auto const tile2 = TilePos(m_connection.back().x(), m_connection.back().y()); 1324 // lighten the fields 1325 repaintTile(tile1); 1326 repaintTile(tile2); 1327 1328 m_paintConnection = true; 1329 update(); 1330 } 1331 1332 void Board::undrawConnection() 1333 { 1334 if (m_tileRemove1.x() != -1) { 1335 setField(m_tileRemove1, EMPTY); 1336 setField(m_tileRemove2, EMPTY); 1337 applyGravity(); 1338 m_tileRemove1.setX(-1); 1339 update(); 1340 Q_EMIT tileCountChanged(); 1341 } 1342 1343 // is already undrawn? 1344 if (m_connection.empty()) { 1345 return; 1346 } 1347 1348 // Redraw all affected fields 1349 auto const oldConnection = m_connection; 1350 m_connection.clear(); 1351 m_paintConnection = false; 1352 1353 auto pt1 = oldConnection.cbegin(); 1354 auto pt2 = pt1 + 1; // TODO: std::next? 1355 while (pt2 != oldConnection.cend()) { 1356 if (pt1->y() == pt2->y()) { 1357 for (auto i = qMin(pt1->x(), pt2->x()); i <= qMax(pt1->x(), pt2->x()); ++i) { 1358 repaintTile(TilePos(i, pt1->y())); 1359 } 1360 } else { 1361 for (auto i = qMin(pt1->y(), pt2->y()); i <= qMax(pt1->y(), pt2->y()); ++i) { 1362 repaintTile(TilePos(pt1->x(), i)); 1363 } 1364 } 1365 ++pt1; 1366 ++pt2; 1367 } 1368 1369 PossibleMoves dummyPossibleMoves; 1370 // game is over? 1371 if ((!pathFoundBetweenMatchingTiles(dummyPossibleMoves) 1372 && m_showUnsolvableMessageFlag) 1373 || (tilesLeft() == 0)) { 1374 m_gameClock.pause(); 1375 Q_EMIT endOfGame(); 1376 } 1377 m_paintInProgress = false; 1378 } 1379 1380 QPoint Board::midCoord(TilePos tilePos) const 1381 { 1382 QPoint p; 1383 auto const w = m_tiles.qWidth() * 2; 1384 auto const h = m_tiles.qHeight() * 2; 1385 1386 if (tilePos.x() == -1) { 1387 p.setX(xOffset() - (w / 4)); 1388 } else if (tilePos.x() == xTiles()) { 1389 p.setX(xOffset() + (w * xTiles()) + (w / 4)); 1390 } else { 1391 p.setX(xOffset() + (w * tilePos.x()) + (w / 2)); 1392 } 1393 1394 if (tilePos.y() == -1) { 1395 p.setY(yOffset() - (w / 4)); 1396 } else if (tilePos.y() == yTiles()) { 1397 p.setY(yOffset() + (h * yTiles()) + (w / 4)); 1398 } else { 1399 p.setY(yOffset() + (h * tilePos.y()) + (h / 2)); 1400 } 1401 1402 return p; 1403 } 1404 1405 void Board::setDelay(int newValue) 1406 { 1407 if (m_delay == newValue) { 1408 return; 1409 } 1410 m_delay = newValue; 1411 } 1412 1413 int Board::delay() const 1414 { 1415 return m_delay; 1416 } 1417 1418 void Board::madeMove(TilePos tilePos1, TilePos tilePos2, Slide slide) 1419 { 1420 std::unique_ptr<Move> move; 1421 if (slide.empty()) { 1422 move = std::make_unique<Move>(tilePos1, tilePos2, field(tilePos1), field(tilePos2)); 1423 } else { 1424 move = std::make_unique<Move>(tilePos1, tilePos2, field(tilePos1), field(tilePos2), slide); 1425 } 1426 m_undo.push_back(std::move(move)); 1427 if (!m_redo.empty()) { 1428 m_redo.clear(); 1429 } 1430 Q_EMIT changed(); 1431 } 1432 1433 bool Board::canUndo() const 1434 { 1435 return !m_undo.empty(); 1436 } 1437 1438 bool Board::canRedo() const 1439 { 1440 return !m_redo.empty(); 1441 } 1442 1443 void Board::undo() 1444 { 1445 if (!canUndo()) { 1446 return; 1447 } 1448 1449 clearHighlight(); 1450 undrawConnection(); 1451 auto move = std::move(m_undo.back()); 1452 m_undo.pop_back(); 1453 if (gravityFlag()) { 1454 // When both tiles reside in the same column, the order of undo is 1455 // significant (we must undo the lower tile first). 1456 // Also in that case there cannot be a slide 1457 if (move->x1() == move->x2() && move->y1() < move->y2()) { 1458 move->swapTiles(); 1459 } 1460 1461 // if there is no slide, keep previous implementation: move both column up 1462 if (!move->hasSlide()) { 1463 qCDebug(KSHISEN_General) << "[undo] gravity from a no slide move"; 1464 1465 // move tiles from the first column up 1466 for (decltype(move->y1()) y = 0; y < move->y1(); ++y) { 1467 setField(TilePos(move->x1(), y), field(TilePos(move->x1(), y + 1))); 1468 repaintTile(TilePos(move->x1(), y)); 1469 } 1470 1471 // move tiles from the second column up 1472 for (decltype(move->y2()) y = 0; y < move->y2(); ++y) { 1473 setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); 1474 repaintTile(TilePos(move->x2(), y)); 1475 } 1476 } else { // else check all tiles from the slide that may have fallen down 1477 1478 qCDebug(KSHISEN_General) << "[undo] gravity from slide s1(" << move->slideX1() << "," << move->slideY1() << ")=>s2(" << move->slideX2() << "," << move->slideY2() << ") matching (" << move->x1() << "," << move->y1() << ")=>(" << move->x2() << "," << move->y2() << ")"; 1479 1480 // horizontal slide 1481 // because tiles that slides horizontally may fall down 1482 // in columns different than the taken tiles columns 1483 // we need to take them back up then undo the slide 1484 if (move->slideY1() == move->slideY2()) { 1485 qCDebug(KSHISEN_General) << "[undo] gravity from horizontal slide"; 1486 1487 // last slide tile went from slideX1() -> slideX2() 1488 // the number of slidden tiles is n = abs(x1 - slideX1()) 1489 auto n = move->x1() - move->slideX1(); 1490 if (n < 0) { 1491 n = -n; 1492 } 1493 // distance slidden is 1494 auto dx = move->slideX2() - move->slideX1(); 1495 if (dx < 0) { 1496 dx = -dx; 1497 } 1498 1499 qCDebug(KSHISEN_General) << "[undo] n =" << n; 1500 1501 // slidden tiles may fall down after the slide 1502 // so any tiles on top of the columns between 1503 // slideX2() -> slideX2() +/- n (excluded) should go up to slideY1() 1504 if (move->slideX2() > move->slideX1()) { // slide to the right 1505 1506 qCDebug(KSHISEN_General) << "[undo] slide right"; 1507 1508 for (auto i = move->slideX2(); i > move->slideX2() - n; --i) { 1509 // find top tile 1510 decltype(yTiles()) j = 0; 1511 for (j = 0; j < yTiles(); ++j) { 1512 if (field(TilePos(i, j)) != EMPTY) { 1513 break; 1514 } 1515 } 1516 1517 // ignore if the tile did not fall 1518 if (j <= move->slideY1()) { 1519 continue; 1520 } 1521 1522 qCDebug(KSHISEN_General) << "[undo] moving (" << i << "," << j << ") up to (" << i << "," << move->slideY1() << ")"; 1523 1524 // put it back up 1525 setField(TilePos(i, move->slideY1()), field(TilePos(i, j))); 1526 setField(TilePos(i, j), EMPTY); 1527 repaintTile(TilePos(i, j)); 1528 repaintTile(TilePos(i, move->slideY1())); 1529 } 1530 } else { // slide to the left 1531 1532 qCDebug(KSHISEN_General) << "[undo] slide left"; 1533 1534 for (auto i = move->slideX2(); i < move->slideX2() + n; ++i) { 1535 // find top tile 1536 decltype(yTiles()) j = 0; 1537 for (j = 0; j < yTiles(); ++j) { 1538 if (field(TilePos(i, j)) != EMPTY) { 1539 break; 1540 } 1541 } 1542 1543 // ignore if the tile did not fall 1544 if (j <= move->slideY1()) { 1545 continue; 1546 } 1547 1548 qCDebug(KSHISEN_General) << "[undo] moving (" << i << "," << j << ") up to (" << i << "," << move->slideY1() << ")"; 1549 1550 // put it back up 1551 setField(TilePos(i, move->slideY1()), field(TilePos(i, j))); 1552 setField(TilePos(i, j), EMPTY); 1553 repaintTile(TilePos(i, j)); 1554 repaintTile(TilePos(i, move->slideY1())); 1555 } 1556 } 1557 1558 qCDebug(KSHISEN_General) << "[undo] moving up column x2" << move->x2(); 1559 1560 // move tiles from the second column up 1561 for (decltype(move->y2()) y = 0; y <= move->y2(); ++y) { 1562 qCDebug(KSHISEN_General) << "[undo] moving up tile" << y + 1; 1563 1564 setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); 1565 repaintTile(TilePos(move->x2(), y)); 1566 } 1567 // and all columns that fell after the tiles slidden between 1568 // only if they were not replaced by a sliding tile !! 1569 // x1 -> x1+dx should go up one 1570 // if their height > slideY1() 1571 // because they have fallen after the slide 1572 if (move->slideX2() > move->slideX1()) { // slide to the right 1573 if (move->slideY1() > 0) { 1574 for (auto i = move->x1() + dx; i >= move->x1(); --i) { 1575 qCDebug(KSHISEN_General) << "[undo] moving up column" << i << "until" << move->slideY1(); 1576 1577 for (decltype(move->slideY1()) j = 0; j < move->slideY1(); ++j) { 1578 qCDebug(KSHISEN_General) << "[undo] moving up tile" << j + 1; 1579 1580 setField(TilePos(i, j), field(TilePos(i, j + 1))); 1581 repaintTile(TilePos(i, j)); 1582 } 1583 1584 qCDebug(KSHISEN_General) << "[undo] clearing last tile" << move->slideY1(); 1585 1586 setField(TilePos(i, move->slideY1()), EMPTY); 1587 repaintTile(TilePos(i, move->slideY1())); 1588 } 1589 } 1590 } else { // slide to the left 1591 if (move->slideY1() > 0) { 1592 for (auto i = move->x1() - dx; i <= move->x1(); ++i) { 1593 qCDebug(KSHISEN_General) << "[undo] moving up column" << i << "until" << move->slideY1(); 1594 1595 for (decltype(move->slideY1()) j = 0; j < move->slideY1(); ++j) { 1596 qCDebug(KSHISEN_General) << "[undo] moving up tile" << j + 1; 1597 1598 setField(TilePos(i, j), field(TilePos(i, j + 1))); 1599 repaintTile(TilePos(i, j)); 1600 } 1601 1602 qCDebug(KSHISEN_General) << "[undo] clearing last tile" << move->slideY1(); 1603 1604 setField(TilePos(i, move->slideY1()), EMPTY); 1605 repaintTile(TilePos(i, move->slideY1())); 1606 } 1607 } 1608 } 1609 1610 qCDebug(KSHISEN_General) << "[undo] reversing slide"; 1611 1612 // then undo the slide to put the tiles back to their original location 1613 reverseSlide(TilePos(move->x1(), move->y1()), move->slide()); 1614 1615 } else { 1616 qCDebug(KSHISEN_General) << "[undo] gravity from vertical slide"; 1617 1618 // vertical slide, in fact nothing special is necessary 1619 // the default implementation works because it only affects 1620 // the two columns were tiles were taken 1621 1622 // move tiles from the first column up 1623 for (decltype(move->y1()) y = 0; y < move->y1(); ++y) { 1624 setField(TilePos(move->x1(), y), field(TilePos(move->x1(), y + 1))); 1625 repaintTile(TilePos(move->x1(), y)); 1626 } 1627 1628 // move tiles from the second column up 1629 for (decltype(move->y2()) y = 0; y < move->y2(); ++y) { 1630 setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); 1631 repaintTile(TilePos(move->x2(), y)); 1632 } 1633 } 1634 } 1635 } else { // no gravity 1636 // undo slide if any 1637 if (move->hasSlide()) { 1638 // perform the slide in reverse 1639 reverseSlide(TilePos(move->x1(), move->y1()), move->slide()); 1640 } 1641 } 1642 1643 // replace taken tiles 1644 setField(TilePos(move->x1(), move->y1()), move->tile1()); 1645 setField(TilePos(move->x2(), move->y2()), move->tile2()); 1646 repaintTile(TilePos(move->x1(), move->y1())); 1647 repaintTile(TilePos(move->x2(), move->y2())); 1648 1649 m_redo.push_front(std::move(move)); 1650 Q_EMIT changed(); 1651 } 1652 1653 void Board::redo() 1654 { 1655 if (canRedo()) { 1656 clearHighlight(); 1657 undrawConnection(); 1658 auto move = std::move(m_redo.front()); 1659 m_redo.pop_front(); 1660 // redo the slide if any 1661 if (move->hasSlide()) { 1662 Slide s; 1663 s.push_back(TilePos(move->slideX1(), move->slideY1())); 1664 s.push_back(TilePos(move->slideX2(), move->slideY2())); 1665 performSlide(TilePos(move->x1(), move->y1()), s); 1666 } 1667 setField(TilePos(move->x1(), move->y1()), EMPTY); 1668 setField(TilePos(move->x2(), move->y2()), EMPTY); 1669 repaintTile(TilePos(move->x1(), move->y1())); 1670 repaintTile(TilePos(move->x2(), move->y2())); 1671 applyGravity(); 1672 m_undo.push_back(std::move(move)); 1673 Q_EMIT changed(); 1674 } 1675 } 1676 1677 void Board::showHint() 1678 { 1679 undrawConnection(); 1680 1681 if (pathFoundBetweenMatchingTiles(m_possibleMoves)) { 1682 m_connection = m_possibleMoves.front().path(); 1683 drawConnection(); 1684 } 1685 } 1686 1687 #ifdef DEBUGGING 1688 void Board::makeHintMove() 1689 { 1690 PossibleMoves possibleMoves; 1691 1692 if (pathFoundBetweenMatchingTiles(possibleMoves)) { 1693 m_markX = -1; 1694 m_markY = -1; 1695 marked(TilePos(possibleMoves.front().path().front().x(), possibleMoves.front().path().front().y())); 1696 marked(TilePos(possibleMoves.front().path().back().x(), possibleMoves.front().path().back().y())); 1697 } 1698 } 1699 1700 1701 void Board::dumpBoard() const 1702 { 1703 dumpBoard(m_field); 1704 } 1705 1706 void Board::dumpBoard(const std::vector<int> & field) const 1707 { 1708 qCDebug(KSHISEN_General) << "Board contents:"; 1709 for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { 1710 QString row; 1711 for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { 1712 auto tile = field.at(y * xTiles() + x); 1713 if (tile == EMPTY) { 1714 row += QLatin1String(" --"); 1715 } else { 1716 row += QStringLiteral("%1").arg(tile, 3); 1717 } 1718 } 1719 qCDebug(KSHISEN_General) << row; 1720 } 1721 } 1722 #endif 1723 1724 int Board::lineWidth() const 1725 { 1726 auto width = qRound(m_tiles.height() / 10.0); 1727 if (width < 3) { 1728 width = 3; 1729 } 1730 1731 return width; 1732 } 1733 1734 bool Board::pathFoundBetweenMatchingTiles(PossibleMoves & possibleMoves) const 1735 { 1736 std::array<short, Board::nTiles> done{}; 1737 1738 for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { 1739 for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { 1740 auto const tile = field(TilePos(x, y)); 1741 if (tile != EMPTY && done.at(tile - 1) != 4) { 1742 // for all these types of tile search paths 1743 for (decltype(xTiles()) xx = 0; xx < xTiles(); ++xx) { 1744 for (decltype(yTiles()) yy = 0; yy < yTiles(); ++yy) { 1745 if (xx != x || yy != y) { 1746 if (tilesMatch(field(TilePos(xx, yy)), tile)) { 1747 if (findPath(TilePos(x, y), TilePos(xx, yy), possibleMoves) > 0) { 1748 return true; 1749 } 1750 } 1751 } 1752 } 1753 } 1754 done.at(tile - 1)++; 1755 } 1756 } 1757 } 1758 return false; 1759 } 1760 1761 int Board::tilesLeft() const 1762 { 1763 return std::count_if(m_field.begin(), m_field.end(), [](auto field) { return field != EMPTY; }); 1764 } 1765 1766 int Board::currentTime() const 1767 { 1768 return m_gameClock.seconds(); 1769 } 1770 1771 bool Board::isSolvable(bool restore) 1772 { 1773 decltype(m_field) oldField; 1774 1775 if (!restore) { 1776 oldField = m_field; 1777 } 1778 1779 PossibleMoves p; 1780 while (pathFoundBetweenMatchingTiles(p)) { 1781 auto const tile1 = TilePos(p.front().path().front().x(), p.front().path().front().y()); 1782 auto const tile2 = TilePos(p.front().path().back().x(), p.front().path().back().y()); 1783 if (!tilesMatch(field(tile1), field(tile2))) { 1784 auto const errMessage = QStringLiteral("Removing unmatched tiles: (%1,%2) => %3 (%4,%5) => %6") 1785 .arg(p.front().path().front().x()) 1786 .arg(p.front().path().front().y()) 1787 .arg(field(tile1)) 1788 .arg(p.front().path().back().x()) 1789 .arg(p.front().path().back().y()) 1790 .arg(field(tile2)); 1791 qCCritical(KSHISEN_General) << errMessage; 1792 } 1793 setField(tile1, EMPTY); 1794 setField(tile2, EMPTY); 1795 } 1796 1797 auto const left = tilesLeft(); 1798 1799 if (!restore) { 1800 m_field = oldField; 1801 } 1802 1803 return left == 0; 1804 } 1805 1806 bool Board::solvableFlag() const 1807 { 1808 return m_solvableFlag; 1809 } 1810 1811 void Board::setSolvableFlag(bool enabled) 1812 { 1813 if (m_solvableFlag == enabled) { 1814 return; 1815 } 1816 m_solvableFlag = enabled; 1817 // if the solvable flag was set and the current game is not solvable, start a new game 1818 if (m_solvableFlag && !isSolvable(true)) { 1819 newGame(); 1820 } 1821 } 1822 1823 bool Board::showUnsolvableMessageFlag() const 1824 { 1825 return m_showUnsolvableMessageFlag; 1826 } 1827 1828 void Board::setShowUnsolvableMessageFlag(bool enabled) 1829 { 1830 if (m_showUnsolvableMessageFlag == enabled) { 1831 return; 1832 } 1833 m_showUnsolvableMessageFlag = enabled; 1834 } 1835 1836 bool Board::gravityFlag() const 1837 { 1838 return m_gravityFlag; 1839 } 1840 1841 void Board::setGravityFlag(bool enabled) 1842 { 1843 if (m_gravityFlag == enabled) { 1844 return; 1845 } 1846 m_gravityFlag = enabled; 1847 // start a new game if the player is in the middle of a game 1848 if (canUndo() || canRedo()) { 1849 newGame(); 1850 } 1851 } 1852 1853 void Board::setChineseStyleFlag(bool enabled) 1854 { 1855 if (m_chineseStyleFlag == enabled) { 1856 return; 1857 } 1858 m_chineseStyleFlag = enabled; 1859 // we need to force a newGame() because board generation is different 1860 newGame(); 1861 } 1862 1863 void Board::setTilesCanSlideFlag(bool enabled) 1864 { 1865 if (m_tilesCanSlideFlag == enabled) { 1866 return; 1867 } 1868 m_tilesCanSlideFlag = enabled; 1869 // start a new game if the player is in the middle of a game 1870 if (canUndo() || canRedo()) { 1871 newGame(); 1872 } 1873 } 1874 1875 void Board::setPauseEnabled(bool enabled) 1876 { 1877 if ((m_gameState == GameState::Paused && enabled) || m_gameState == GameState::Stuck) { 1878 return; 1879 } 1880 if (enabled) { 1881 m_gameState = GameState::Paused; 1882 m_gameClock.pause(); 1883 } else { 1884 m_gameState = GameState::Normal; 1885 m_gameClock.resume(); 1886 } 1887 Q_EMIT changed(); 1888 update(); 1889 } 1890 1891 QSize Board::sizeHint() const 1892 { 1893 auto dpi = logicalDpiX(); 1894 if (dpi < 75) { 1895 dpi = 75; 1896 } 1897 return QSize(9 * dpi, 7 * dpi); 1898 } 1899 1900 void Board::resetTimer() 1901 { 1902 m_gameClock.restart(); 1903 } 1904 1905 void Board::resetUndo() 1906 { 1907 if (!canUndo()) { 1908 return; 1909 } 1910 m_undo.clear(); 1911 } 1912 1913 void Board::resetRedo() 1914 { 1915 if (!canRedo()) { 1916 return; 1917 } 1918 m_redo.clear(); 1919 } 1920 1921 void Board::setGameStuckEnabled(bool enabled) 1922 { 1923 if (m_gameState == GameState::Stuck && enabled) { 1924 return; 1925 } 1926 if (enabled) { 1927 m_gameState = GameState::Stuck; 1928 m_gameClock.pause(); 1929 } else { 1930 m_gameState = GameState::Normal; 1931 m_gameClock.resume(); 1932 } 1933 Q_EMIT changed(); 1934 update(); 1935 } 1936 1937 void Board::setGameOverEnabled(bool enabled) 1938 { 1939 if (m_gameState == GameState::Over && enabled) { 1940 return; 1941 } 1942 m_gameState = GameState::Over; 1943 Q_EMIT changed(); 1944 update(); 1945 } 1946 1947 void Board::setCheatModeEnabled(bool enabled) 1948 { 1949 if (m_cheat == enabled) { 1950 return; 1951 } 1952 m_cheat = enabled; 1953 Q_EMIT cheatStatusChanged(); 1954 } 1955 1956 bool Board::isOver() const 1957 { 1958 return m_gameState == GameState::Over; 1959 } 1960 1961 bool Board::isPaused() const 1962 { 1963 return m_gameState == GameState::Paused; 1964 } 1965 1966 bool Board::isStuck() const 1967 { 1968 return m_gameState == GameState::Stuck; 1969 } 1970 1971 bool Board::hasCheated() const 1972 { 1973 return m_cheat; 1974 } 1975 1976 void Board::setSoundsEnabled(bool enabled) 1977 { 1978 Prefs::setSounds(enabled); 1979 Prefs::self()->save(); 1980 } 1981 1982 bool Board::isValidPos(TilePos tilePos) const 1983 { 1984 return tilePos.x() >= 0 1985 && tilePos.y() >= 0 1986 && tilePos.x() < xTiles() 1987 && tilePos.y() < yTiles(); 1988 } 1989 1990 bool Board::isValidPosWithOutline(TilePos tilePos) const 1991 { 1992 return tilePos.x() >= -1 1993 && tilePos.y() >= -1 1994 && tilePos.x() <= xTiles() 1995 && tilePos.y() <= yTiles(); 1996 } 1997 1998 bool Board::isTileFlower(int tile) const 1999 { 2000 return tile >= FLOWERS_START && tile <= (FLOWERS_START + 3); 2001 } 2002 2003 bool Board::isTileSeason(int tile) const 2004 { 2005 return tile >= SEASONS_START && tile <= (SEASONS_START + 3); 2006 } 2007 } // namespace KShisen 2008 2009 #include "moc_board.cpp" 2010 2011 // vim: expandtab:tabstop=4:shiftwidth=4 2012 // kate: space-indent on; indent-width 4