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