diff --git a/src/board.cpp b/src/board.cpp index 5e3f718..26d55ca 100644 --- a/src/board.cpp +++ b/src/board.cpp @@ -1,1980 +1,1980 @@ /*************************************************************************** * KShisen - A japanese game similar to mahjongg * * Copyright 1997 Mario Weilguni * * Copyright 2002-2004 Dave Corrie * * Copyright 2007 Mauricio Piacentini * * Copyright 2009-2016 Frederik Schwarzer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ // own #include "board.h" // STL #include #include // Qt #include #include #include #include // KDE #include // KShisen #include "debug.h" #include "prefs.h" namespace KShisen { #define EMPTY 0 #define SEASONS_START 28 #define FLOWERS_START 39 static std::array constexpr s_delay {{1000, 750, 500, 250, 125}}; static std::array constexpr s_sizeX {{14, 16, 18, 24, 26, 30}}; static std::array constexpr s_sizeY {{6, 9, 8, 12, 14, 16}}; Board::Board(QWidget * parent) : QWidget(parent) , m_gameClock() , m_tiles() , m_background() , m_random() , m_undo() , m_redo() , m_markX(0) , m_markY(0) , m_connection() , m_possibleMoves() , m_field() , m_xTiles(0) , m_yTiles(0) , m_delay(0) , m_level(0) , m_shuffle(0) , m_gameState(GameState::Normal) , m_cheat(false) , m_gravityFlag(true) , m_solvableFlag(false) , m_chineseStyleFlag(false) , m_tilesCanSlideFlag(false) , m_highlightedTile(-1) , m_paintConnection(false) , m_paintPossibleMoves(false) , m_paintInProgress(false) , m_tileRemove1() , m_tileRemove2() , m_soundPick(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-touch.ogg"))) , m_soundFall(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-fall-tile.ogg"))) { m_tileRemove1.setX(-1); m_random.setSeed(0); resetTimer(); QPalette palette; palette.setBrush(backgroundRole(), m_background.getBackground()); setPalette(palette); loadSettings(); } void Board::loadSettings() { if (!loadTileset(Prefs::tileSet())) { - qCWarning(KSHISEN_General) << "An error occurred when loading the tileset" << Prefs::tileSet() << "KShisen will continue with the default tileset."; + qCWarning(KSHISEN_General) << "An error occurred when loading the tileset. " << Prefs::tileSet() << " and the default tileset have been tried."; } // Load background if (!loadBackground(Prefs::background())) { - qCWarning(KSHISEN_General) << "An error occurred when loading the background" << Prefs::background() << "KShisen will continue with the default background."; + qCWarning(KSHISEN_General) << "An error occurred when loading the background. " << Prefs::background() << " and the default background have been tried."; } // There are tile sets, that have only one tile for e.g. the flowers group. // If these tile sets are played in non-chineseStyle, this one tile face // appears too often and not every tile matches another one with the same // face because they are technically different (e.g different flowers). // The solution is to enforce chineseStyle gameplay for tile sets that are // known to be reduced. Those are Egypt and Alphabet for now. if (Prefs::tileSet().endsWith(QLatin1String("egypt.desktop")) || Prefs::tileSet().endsWith(QLatin1String("alphabet.desktop"))) { setChineseStyleFlag(true); } else { setChineseStyleFlag(Prefs::chineseStyle()); } setTilesCanSlideFlag(Prefs::tilesCanSlide()); // Need to load solvable before size because setSize calls newGame which // uses the solvable flag. Same with shuffle. setSolvableFlag(Prefs::solvable()); m_shuffle = Prefs::level() * 4 + 1; setSize(s_sizeX.at(Prefs::size()), s_sizeY.at(Prefs::size())); setGravityFlag(Prefs::gravity()); setDelay(s_delay.at(Prefs::speed())); setSoundsEnabled(Prefs::sounds()); if (m_level != Prefs::level()) { newGame(); } m_level = Prefs::level(); } bool Board::loadTileset(QString const & pathToTileset) { if (m_tiles.loadTileset(pathToTileset)) { if (m_tiles.loadGraphics()) { Prefs::setTileSet(pathToTileset); Prefs::self()->save(); resizeBoard(); return true; } } //Try default if (m_tiles.loadDefault()) { if (m_tiles.loadGraphics()) { Prefs::setTileSet(m_tiles.path()); Prefs::self()->save(); resizeBoard(); } } return false; } bool Board::loadBackground(QString const & pathToBackground) { if (m_background.load(pathToBackground, width(), height())) { if (m_background.loadGraphics()) { Prefs::setBackground(pathToBackground); Prefs::self()->save(); resizeBoard(); return true; } } //Try default if (m_background.loadDefault()) { if (m_background.loadGraphics()) { Prefs::setBackground(m_background.path()); Prefs::self()->save(); resizeBoard(); } } return false; } int Board::xTiles() const { return m_xTiles; } int Board::yTiles() const { return m_yTiles; } int Board::tiles() const { return m_field.size(); } void Board::setField(TilePos const & tilePos, int value) { if (!isValidPos(tilePos)) { qCCritical(KSHISEN_General) << "Attempted write to invalid field position:" << tilePos.x() << "," << tilePos.y(); } m_field.at(tilePos.y() * xTiles() + tilePos.x()) = value; } int Board::field(TilePos const & tilePos) const { if (!isValidPosWithOutline(tilePos)) { qCCritical(KSHISEN_General) << "Attempted read from invalid field position:" << tilePos.x() << "," << tilePos.y(); } if (!isValidPos(tilePos)) { return EMPTY; } return m_field.at(tilePos.y() * xTiles() + tilePos.x()); } void Board::applyGravity() { if (!m_gravityFlag) { return; } for (decltype(xTiles()) column = 0; column < xTiles(); ++column) { auto rptr = yTiles() - 1; auto wptr = yTiles() - 1; while (rptr >= 0) { auto wptrPos = TilePos(column, wptr); if (field(wptrPos) != EMPTY) { --rptr; --wptr; } else { auto rptrPos = TilePos(column, rptr); if (field(rptrPos) != EMPTY) { setField(wptrPos, field(rptrPos)); setField(rptrPos, EMPTY); repaintTile(rptrPos); repaintTile(wptrPos); --wptr; --rptr; if (Prefs::sounds()) { m_soundFall.start(); } } else { --rptr; } } } } } void Board::unmarkTile() { // if nothing is marked, nothing to do if (m_markX == -1 || m_markY == -1) { return; } drawPossibleMoves(false); m_possibleMoves.clear(); // We need to set m_markX and m_markY to -1 before calling // updateField() to ensure the tile is redrawn as unmarked. auto const oldTilePos = TilePos(m_markX, m_markY); m_markX = -1; m_markY = -1; repaintTile(oldTilePos); } void Board::mousePressEvent(QMouseEvent * e) { // Do not process mouse events while the connection is drawn. // Clicking on one of the already connected tiles would have selected // it before removing it. This is more a workaround than a proper fix // but I have to understand the usage of m_paintConnection first in // order to consider its reusage here. (schwarzer) if (m_paintInProgress) { return; } switch (m_gameState) { case GameState::Normal: break; case GameState::Over: newGame(); return; case GameState::Paused: setPauseEnabled(false); return; case GameState::Stuck: return; } // Calculate field position auto posX = (e->pos().x() - xOffset()) / (m_tiles.qWidth() * 2); auto posY = (e->pos().y() - yOffset()) / (m_tiles.qHeight() * 2); if (e->pos().x() < xOffset() || e->pos().y() < yOffset() || posX >= xTiles() || posY >= yTiles()) { posX = -1; posY = -1; } // Mark tile if (e->button() == Qt::LeftButton) { clearHighlight(); if (posX != -1) { marked(TilePos(posX, posY)); } else { // unmark when clicking outside the board unmarkTile(); } } // Assist by highlighting all tiles of same type if (e->button() == Qt::RightButton) { auto const clickedTile = field(TilePos(posX, posY)); // Clear marked tile if (m_markX != -1 && field(TilePos(m_markX, m_markY)) != clickedTile) { unmarkTile(); } else { m_markX = -1; m_markY = -1; } // Perform highlighting if (clickedTile != m_highlightedTile) { auto const oldHighlighted = m_highlightedTile; m_highlightedTile = clickedTile; for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { auto const fieldTile = field(TilePos(i, j)); if (fieldTile != EMPTY) { if (fieldTile == oldHighlighted) { repaintTile(TilePos(i, j)); } else if (fieldTile == clickedTile) { repaintTile(TilePos(i, j)); } else if (m_chineseStyleFlag) { if (isTileSeason(clickedTile) && isTileSeason(fieldTile)) { repaintTile(TilePos(i, j)); } else if (isTileFlower(clickedTile) && isTileFlower(fieldTile)) { repaintTile(TilePos(i, j)); } // oldHighlighted if (isTileSeason(oldHighlighted) && isTileSeason(fieldTile)) { repaintTile(TilePos(i, j)); } else if (isTileFlower(oldHighlighted) && isTileFlower(fieldTile)) { repaintTile(TilePos(i, j)); } } } } } } } } int Board::xOffset() const { auto tw = m_tiles.qWidth() * 2; return (width() - (tw * xTiles())) / 2; } int Board::yOffset() const { auto th = m_tiles.qHeight() * 2; return (height() - (th * yTiles())) / 2; } void Board::setSize(int x, int y) { if (x == xTiles() && y == yTiles()) { return; } m_field.resize(x * y); m_xTiles = x; m_yTiles = y; std::fill(m_field.begin(), m_field.end(), EMPTY); // set the minimum size of the scalable window auto constexpr minScale = 0.2; auto const w = qRound(m_tiles.qWidth() * 2.0 * minScale) * xTiles() + m_tiles.width(); auto const h = qRound(m_tiles.qHeight() * 2.0 * minScale) * yTiles() + m_tiles.height(); setMinimumSize(w, h); resizeBoard(); newGame(); emit changed(); } void Board::resizeEvent(QResizeEvent * e) { qCDebug(KSHISEN_General) << "[resizeEvent]"; if (e->spontaneous()) { qCDebug(KSHISEN_General) << "[resizeEvent] spontaneous"; } resizeBoard(); emit resized(); } void Board::resizeBoard() { // calculate tile size required to fit all tiles in the window auto const newsize = m_tiles.preferredTileSize(QSize(width(), height()), xTiles(), yTiles()); m_tiles.reloadTileset(newsize); //recalculate bg, if needed m_background.sizeChanged(width(), height()); //reload our bg brush, using the cache in libkmahjongg if possible QPalette palette; palette.setBrush(backgroundRole(), m_background.getBackground()); setPalette(palette); } void Board::newGame() { m_gameState = GameState::Normal; setCheatModeEnabled(false); m_markX = -1; m_markY = -1; m_highlightedTile = -1; // will clear previous highlight resetUndo(); resetRedo(); m_connection.clear(); m_possibleMoves.clear(); // distribute all tiles on board auto curTile = 1; auto tileCount = 0; /* * Note by jwickers: i changed the way to distribute tiles * in chinese mahjongg there are 4 tiles of each * except flowers and seasons (4 flowers and 4 seasons, * but one unique tile of each, that is why they are * the only ones numbered) * That uses the chineseStyle flag */ for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { // do not duplicate flowers or seasons if (!m_chineseStyleFlag || !(isTileSeason(curTile) || isTileFlower(curTile))) { setField(TilePos(x, y), curTile); if (++tileCount >= 4) { tileCount = 0; ++curTile; } } else { tileCount = 0; setField(TilePos(x, y), curTile++); } if (curTile > Board::nTiles) { curTile = 1; } } } if (m_shuffle == 0) { update(); resetTimer(); emit newGameStarted(); emit changed(); return; } // shuffle the field auto const tx = xTiles(); auto const ty = yTiles(); for (decltype(tx * ty * m_shuffle) i = 0; i < tx * ty * m_shuffle; ++i) { auto const tilePos1 = TilePos(m_random.getLong(tx), m_random.getLong(ty)); auto const tilePos2 = TilePos(m_random.getLong(tx), m_random.getLong(ty)); // keep and use t, because the next setField() call changes what field() will return // so there would a significant impact on shuffling with the field() call put into the // place where 't' is used auto const t = field(tilePos1); setField(tilePos1, field(tilePos2)); setField(tilePos2, t); } // if m_solvableFlag is false, the game does not need to be solvable; we can drop out here if (!m_solvableFlag) { update(); resetTimer(); emit newGameStarted(); emit changed(); return; } auto oldfield = m_field; decltype(m_field) tiles(m_field.size()); decltype(m_field) pos(m_field.size()); //jwickers: in case the game cannot made solvable we do not want to run an infinite loop auto maxAttempts = 200; while (!isSolvable(false) && maxAttempts > 0) { // generate a list of free tiles and positions auto numberOfTiles = 0; for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { if (m_field.at(i) != EMPTY) { pos.at(numberOfTiles) = i; tiles.at(numberOfTiles) = m_field.at(i); ++numberOfTiles; } } // restore field m_field = oldfield; // redistribute unsolved tiles while (numberOfTiles > 0) { // get a random tile auto const r1 = m_random.getLong(numberOfTiles); auto const r2 = m_random.getLong(numberOfTiles); auto const tile = tiles.at(r1); auto const apos = pos.at(r2); // truncate list tiles.at(r1) = tiles.at(numberOfTiles - 1); pos.at(r2) = pos.at(numberOfTiles - 1); --numberOfTiles; // put this tile on the new position m_field.at(apos) = tile; } // remember field oldfield = m_field; --maxAttempts; } // debug, tell if make solvable failed if (maxAttempts == 0) { qCCritical(KSHISEN_General) << "NewGame make solvable failed"; } // restore field m_field = oldfield; update(); resetTimer(); emit changed(); } bool Board::tilesMatch(int tile1, int tile2) const { // identical tiles always match if (tile1 == tile2) { return true; } // when chinese style is set, there are special rules // for flowers and seasons if (m_chineseStyleFlag) { // if both tiles are seasons if (isTileSeason(tile1) && isTileSeason(tile2)) { return true; } // if both tiles are flowers if (isTileFlower(tile1) && isTileFlower(tile2)) { return true; } } return false; } bool Board::isTileHighlighted(TilePos const & tilePos) const { if (tilePos.x() == m_markX && tilePos.y() == m_markY) { return true; } if (tilesMatch(m_highlightedTile, field(tilePos))) { return true; } // m_tileRemove1.first != -1 is used because the repaint of the first if // on undrawConnection highlighted the tiles that fell because of gravity if (!m_connection.empty() && m_tileRemove1.x() != -1) { if (tilePos.x() == m_connection.front().x() && tilePos.y() == m_connection.front().y()) { return true; } if (tilePos.x() == m_connection.back().x() && tilePos.y() == m_connection.back().y()) { return true; } } return false; } void Board::repaintTile(TilePos const & tilePos) { auto const r = QRect(xOffset() + tilePos.x() * m_tiles.qWidth() * 2, yOffset() + tilePos.y() * m_tiles.qHeight() * 2, m_tiles.width(), m_tiles.height()); update(r); } void Board::showInfoRect(QPainter & p, QString const & message) { auto const boxWidth = width() * 0.6; auto const boxHeight = height() * 0.6; auto const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight); QFont font; auto const fontsize = static_cast(boxHeight / 13); font.setPointSize(fontsize); p.setFont(font); p.setBrush(QBrush(QColor(100, 100, 100, 150))); p.setRenderHint(QPainter::Antialiasing); p.drawRoundedRect(contentsRect, 10, 10); p.drawText(contentsRect, Qt::AlignCenter | Qt::TextWordWrap, message); } void Board::drawTiles(QPainter & p, QPaintEvent * e) { auto const w = m_tiles.width(); auto const h = m_tiles.height(); auto const fw = m_tiles.qWidth() * 2; auto const fh = m_tiles.qHeight() * 2; for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { auto const tile = field(TilePos(i, j)); if (tile == EMPTY) { continue; } auto const xpos = xOffset() + i * fw; auto const ypos = yOffset() + j * fh; auto const r = QRect(xpos, ypos, w, h); if (e->rect().intersects(r)) { if (isTileHighlighted(TilePos(i, j))) { p.drawPixmap(xpos, ypos, m_tiles.selectedTile(1)); } else { p.drawPixmap(xpos, ypos, m_tiles.unselectedTile(1)); } //draw face p.drawPixmap(xpos, ypos, m_tiles.tileface(tile - 1)); } } } } void Board::paintEvent(QPaintEvent * e) { auto const ur = e->rect(); // rectangle to update QPainter p(this); p.fillRect(ur, m_background.getBackground()); switch (m_gameState) { case GameState::Normal: drawTiles(p, e); break; case GameState::Paused: showInfoRect(p, i18n("Game Paused\nClick to resume game.")); break; case GameState::Stuck: drawTiles(p, e); showInfoRect(p, i18n("Game Stuck\nNo more moves possible.")); break; case GameState::Over: showInfoRect(p, i18n("Game Over\nClick to start a new game.")); break; } if (m_paintConnection) { p.setPen(QPen(QColor("red"), lineWidth())); auto pt1 = m_connection.cbegin(); auto pt2 = pt1 + 1; while (pt2 != m_connection.cend()) { p.drawLine(midCoord(*pt1), midCoord(*pt2)); ++pt1; ++pt2; } QTimer::singleShot(delay(), this, &Board::undrawConnection); m_paintConnection = false; } if (m_paintPossibleMoves) { p.setPen(QPen(QColor("blue"), lineWidth())); // paint all possible moves foreach (auto const move, m_possibleMoves) { auto pt1 = move.path().cbegin(); auto pt2 = pt1 + 1; while (pt2 != move.path().cend()) { p.drawLine(midCoord(*pt1), midCoord(*pt2)); ++pt1; ++pt2; } } m_paintConnection = false; } p.end(); } void Board::reverseSlide(TilePos const & tilePos, Slide const & slide) { // slide[XY]2 is the current location of the last tile to slide // slide[XY]1 is its destination // calculate the offset for the tiles to slide auto const dx = slide.front().x() - slide.back().x(); auto const dy = slide.front().y() - slide.back().y(); auto currentTile = 0; // move all tiles between slideX2, slideY2 and x, y to slide with that offset if (dx == 0) { if (tilePos.y() < slide.back().y()) { for (auto i = tilePos.y() + 1; i <= slide.back().y(); ++i) { currentTile = field(TilePos(tilePos.x(), i)); if (currentTile == EMPTY) { continue; } setField(TilePos(tilePos.x(), i), EMPTY); setField(TilePos(tilePos.x(), i + dy), currentTile); repaintTile(TilePos(tilePos.x(), i)); repaintTile(TilePos(tilePos.x(), i + dy)); } } else { for (auto i = tilePos.y() - 1; i >= slide.back().y(); --i) { currentTile = field(TilePos(tilePos.x(), i)); if (currentTile == EMPTY) { continue; } setField(TilePos(tilePos.x(), i), EMPTY); setField(TilePos(tilePos.x(), i + dy), currentTile); repaintTile(TilePos(tilePos.x(), i)); repaintTile(TilePos(tilePos.x(), i + dy)); } } } else if (dy == 0) { if (tilePos.x() < slide.back().x()) { for (auto i = tilePos.x() + 1; i <= slide.back().x(); ++i) { currentTile = field(TilePos(i, tilePos.y())); if (currentTile == EMPTY) { continue; } setField(TilePos(i, tilePos.y()), EMPTY); setField(TilePos(i + dx, tilePos.y()), currentTile); repaintTile(TilePos(i, tilePos.y())); repaintTile(TilePos(i + dx, tilePos.y())); } } else { for (auto i = tilePos.x() - 1; i >= slide.back().x(); --i) { currentTile = field(TilePos(i, tilePos.y())); if (currentTile == EMPTY) { continue; } setField(TilePos(i, tilePos.y()), EMPTY); setField(TilePos(i + dx, tilePos.y()), currentTile); repaintTile(TilePos(i, tilePos.y())); repaintTile(TilePos(i + dx, tilePos.y())); } } } } void Board::performSlide(TilePos const & tilePos, Slide const & slide) { // check if there is something to slide if (slide.empty()) { return; } // slide.first is the current location of the last tile to slide // slide.last is its destination // calculate the offset for the tiles to slide auto const dx = slide.back().x() - slide.front().x(); auto const dy = slide.back().y() - slide.front().y(); auto currentTile = 0; // move all tiles between m_markX, m_markY and the last tile to slide with that offset if (dx == 0) { if (tilePos.y() < slide.front().y()) { for (auto i = slide.front().y(); i > tilePos.y(); --i) { currentTile = field(TilePos(tilePos.x(), i)); setField(TilePos(tilePos.x(), i), EMPTY); setField(TilePos(tilePos.x(), i + dy), currentTile); repaintTile(TilePos(tilePos.x(), i)); repaintTile(TilePos(tilePos.x(), i + dy)); } } else { for (auto i = slide.front().y(); i < tilePos.y(); ++i) { currentTile = field(TilePos(tilePos.x(), i)); setField(TilePos(tilePos.x(), i), EMPTY); setField(TilePos(tilePos.x(), i + dy), currentTile); repaintTile(TilePos(tilePos.x(), i)); repaintTile(TilePos(tilePos.x(), i + dy)); } } } else if (dy == 0) { if (tilePos.x() < slide.front().x()) { for (auto i = slide.front().x(); i > tilePos.x(); --i) { currentTile = field(TilePos(i, tilePos.y())); setField(TilePos(i, tilePos.y()), EMPTY); setField(TilePos(i + dx, tilePos.y()), currentTile); repaintTile(TilePos(i, tilePos.y())); repaintTile(TilePos(i + dx, tilePos.y())); } } else { for (auto i = slide.front().x(); i < tilePos.x(); ++i) { currentTile = field(TilePos(i, tilePos.y())); setField(TilePos(i, tilePos.y()), EMPTY); setField(TilePos(i + dx, tilePos.y()), currentTile); repaintTile(TilePos(i, tilePos.y())); repaintTile(TilePos(i + dx, tilePos.y())); } } } } void Board::performMove(PossibleMove & possibleMoves) { m_connection = possibleMoves.path(); #ifdef DEBUGGING // DEBUG undo, save board state auto saved1 = m_field; #endif // if the tiles can slide, we have to update the slided tiles too // and store the slide in a Move if (possibleMoves.hasSlide()) { performSlide(TilePos(m_markX, m_markY), possibleMoves.slide()); madeMove(TilePos(m_markX, m_markY), TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y()), possibleMoves.slide()); } else { madeMove(TilePos(m_markX, m_markY), TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y())); } drawPossibleMoves(false); drawConnection(); m_tileRemove1 = TilePos(m_markX, m_markY); m_tileRemove2 = TilePos(possibleMoves.path().back().x(), possibleMoves.path().back().y()); m_markX = -1; m_markY = -1; m_possibleMoves.clear(); #ifdef DEBUGGING // DEBUG undo, force gravity undrawConnection(); // DEBUG undo, save board2 state auto saved2 = m_field; decltype(m_field) saved3; // after undo decltype(m_field) saved4; // after redo // DEBUG undo, undo move auto errorFound = false; if (canUndo()) { undo(); // DEBUG undo, compare to saved board state for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { if (saved1.at(i) != m_field.at(i)) { qCDebug(KSHISEN_General) << "[DEBUG Undo 1], tile (" << i << ") was" << saved1.at(i) << "before move, it is" << m_field.at(i) << "after undo."; errorFound = true; } } // DEBUG undo, save board state saved3 = m_field; // DEBUG undo, redo if (canRedo()) { redo(); undrawConnection(); // DEBUG undo, compare to saved board2 state for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) { if (saved2.at(i) != m_field.at(i)) { qCDebug(KSHISEN_General) << "[DEBUG Undo 2], tile (" << i << ") was" << saved2.at(i) << "after move, it is" << m_field.at(i) << "after redo."; errorFound = true; } } // DEBUG undo, save board state saved4 = m_field; } } // dumpBoard on error if (errorFound) { qCDebug(KSHISEN_General) << "[DEBUG] Before move"; dumpBoard(saved1); qCDebug(KSHISEN_General) << "[DEBUG] After move"; dumpBoard(saved2); qCDebug(KSHISEN_General) << "[DEBUG] Undo"; dumpBoard(saved3); qCDebug(KSHISEN_General) << "[DEBUG] Redo"; dumpBoard(saved4); } #endif } void Board::marked(TilePos const & tilePos) { if (field(tilePos) == EMPTY) { // click on empty space on the board if (m_possibleMoves.size() > 1) { // if the click is on any of the current possible moves, make that move foreach (auto move, m_possibleMoves) { if (move.isInPath(tilePos)) { performMove(move); emit selectATile(); return; } } } else { // unmark when not clicking on a tile unmarkTile(); return; } } // make sure that the previous connection is correctly undrawn undrawConnection(); // is this still needed? (schwarzer) if (Prefs::sounds()) { m_soundPick.start(); } if (tilePos.x() == m_markX && tilePos.y() == m_markY) { // the piece is already marked // unmark the piece unmarkTile(); emit selectATile(); return; } if (m_markX == -1) { // nothing is selected so far m_markX = tilePos.x(); m_markY = tilePos.y(); drawPossibleMoves(false); m_possibleMoves.clear(); repaintTile(tilePos); emit selectAMatchingTile(); return; } if (m_possibleMoves.size() > 1) { // if the click is on any of the current possible moves, make that move foreach (auto move, m_possibleMoves) { if (move.isInPath(tilePos)) { performMove(move); emit selectATile(); return; } } } auto const tile1 = field(TilePos(m_markX, m_markY)); auto const tile2 = field(tilePos); // both tiles do not match if (!tilesMatch(tile1, tile2)) { unmarkTile(); emit tilesDoNotMatch(); return; } // trace and perform the move and get the list of possible moves if (findPath(TilePos(m_markX, m_markY), tilePos, m_possibleMoves) > 0) { if (m_possibleMoves.size() > 1) { auto withSlide = 0; foreach (auto const move, m_possibleMoves) { move.Debug(); if (move.hasSlide()) { ++withSlide; } } // if all moves have no slide, it doesn't matter if (withSlide > 0) { drawPossibleMoves(true); emit selectAMove(); return; } } // only one move possible, perform it performMove(m_possibleMoves.front()); emit selectATile(); // game is over? // Must delay until after tiles fall to make this test // See undrawConnection GP. } else { emit invalidMove(); m_connection.clear(); } } void Board::clearHighlight() { if (m_highlightedTile == -1) { return; } auto const oldHighlighted = m_highlightedTile; m_highlightedTile = -1; for (decltype(xTiles()) i = 0; i < xTiles(); ++i) { for (decltype(yTiles()) j = 0; j < yTiles(); ++j) { if (tilesMatch(oldHighlighted, field(TilePos(i, j)))) { repaintTile(TilePos(i, j)); } } } } bool Board::canMakePath(TilePos const & tilePos1, TilePos const & tilePos2) const { if (tilePos1.x() == tilePos2.x()) { for (auto i = qMin(tilePos1.y(), tilePos2.y()) + 1; i < qMax(tilePos1.y(), tilePos2.y()); ++i) { if (field(TilePos(tilePos1.x(), i)) != EMPTY) { return false; } } return true; } if (tilePos1.y() == tilePos2.y()) { for (auto i = qMin(tilePos1.x(), tilePos2.x()) + 1; i < qMax(tilePos1.x(), tilePos2.x()); ++i) { if (field(TilePos(i, tilePos1.y())) != EMPTY) { return false; } } return true; } return false; } bool Board::canSlideTiles(TilePos const & tilePos1, TilePos const & tilePos2, Slide & slide) const { auto distance = -1; slide.clear(); if (tilePos1.x() == tilePos2.x()) { if (tilePos1.y() > tilePos2.y()) { distance = tilePos1.y() - tilePos2.y(); // count how much free space we have for sliding auto startFree = -1; auto endFree = -1; // find first tile empty for (auto i = tilePos1.y() - 1; i >= 0; --i) { if (field(TilePos(tilePos1.x(), i)) == EMPTY) { startFree = i; break; } } // if not found, cannot slide // if the first free tile is just next to the sliding tile, no slide (should be a normal move) if (startFree == -1 || startFree == (tilePos1.y() - 1)) { return false; } // find last tile empty for (auto i = startFree - 1; i >= 0; --i) { if (field(TilePos(tilePos1.x(), i)) != EMPTY) { endFree = i; break; } } // if not found, it is the border: 0 // so we can slide of start_free - end_free, compare this to the distance if (distance <= (startFree - endFree)) { // first position of the last slided tile slide.push_back(TilePos(tilePos1.x(), startFree + 1)); // final position of the last slided tile slide.push_back(TilePos(tilePos1.x(), startFree + 1 - distance)); return true; } return false; } else if (tilePos2.y() > tilePos1.y()) { distance = tilePos2.y() - tilePos1.y(); // count how much free space we have for sliding auto startFree = -1; auto endFree = yTiles(); // find first tile empty for (auto i = tilePos1.y() + 1; i < yTiles(); ++i) { if (field(TilePos(tilePos1.x(), i)) == EMPTY) { startFree = i; break; } } // if not found, cannot slide // if the first free tile is just next to the sliding tile, no slide (should be a normal move) if (startFree == -1 || startFree == tilePos1.y() + 1) { return false; } // find last tile empty for (auto i = startFree + 1; i < yTiles(); ++i) { if (field(TilePos(tilePos1.x(), i)) != EMPTY) { endFree = i; break; } } // if not found, it is the border: yTiles()-1 // so we can slide of end_free - start_free, compare this to the distance if (distance <= (endFree - startFree)) { // first position of the last slidden tile slide.push_back(TilePos(tilePos1.x(), startFree - 1)); // final position of the last slidden tile slide.push_back(TilePos(tilePos1.x(), startFree - 1 + distance)); return true; } return false; } // y1 == y2 ?! return false; } if (tilePos1.y() == tilePos2.y()) { if (tilePos1.x() > tilePos2.x()) { distance = tilePos1.x() - tilePos2.x(); // count how much free space we have for sliding auto startFree = -1; auto endFree = -1; // find first tile empty for (auto i = tilePos1.x() - 1; i >= 0; --i) { if (field(TilePos(i, tilePos1.y())) == EMPTY) { startFree = i; break; } } // if not found, cannot slide // if the first free tile is just next to the sliding tile, no slide (should be a normal move) if (startFree == -1 || startFree == tilePos1.x() - 1) { return false; } // find last tile empty for (auto i = startFree - 1; i >= 0; --i) { if (field(TilePos(i, tilePos1.y())) != EMPTY) { endFree = i; break; } } // if not found, it is the border: 0 // so we can slide of start_free - end_free, compare this to the distance if (distance <= (startFree - endFree)) { // first position of the last slidden tile slide.push_back(TilePos(startFree + 1, tilePos1.y())); // final position of the last slidden tile slide.push_back(TilePos(startFree + 1 - distance, tilePos1.y())); return true; } return false; } else if (tilePos2.x() > tilePos1.x()) { distance = tilePos2.x() - tilePos1.x(); // count how much free space we have for sliding auto startFree = -1; auto endFree = xTiles(); // find first tile empty for (auto i = tilePos1.x() + 1; i < xTiles(); ++i) { if (field(TilePos(i, tilePos1.y())) == EMPTY) { startFree = i; break; } } // if not found, cannot slide // if the first free tile is just next to the sliding tile, no slide (should be a normal move) if (startFree == -1 || startFree == tilePos1.x() + 1) { return false; } // find last tile empty for (auto i = startFree + 1; i < xTiles(); ++i) { if (field(TilePos(i, tilePos1.y())) != EMPTY) { endFree = i; break; } } // if not found, it is the border: xTiles()-1 // so we can slide of endFree - startFree, compare this to the distance if (distance <= (endFree - startFree)) { // first position of the last slidden tile slide.push_back(TilePos(startFree - 1, tilePos1.y())); // final position of the last slidden tile slide.push_back(TilePos(startFree - 1 + distance, tilePos1.y())); return true; } return false; } // x1 == x2 ?! return false; } return false; } int Board::findPath(TilePos const & tilePos1, TilePos const & tilePos2, PossibleMoves & possibleMoves) const { possibleMoves.clear(); auto simplePath = 0; // first find the simple paths auto numberOfPaths = findSimplePath(tilePos1, tilePos2, possibleMoves); // if the tiles can slide, 2 lines max is allowed if (m_tilesCanSlideFlag) { return numberOfPaths; } // Find paths of 3 segments std::array constexpr dx {{1, 0, -1, 0}}; std::array constexpr dy {{0, 1, 0, -1}}; for (auto i = 0; i < 4; ++i) { auto tempX = tilePos1.x() + dx.at(i); auto tempY = tilePos1.y() + dy.at(i); while (isValidPosWithOutline(TilePos(tempX, tempY)) && field(TilePos(tempX, tempY)) == EMPTY) { if ((simplePath = findSimplePath(TilePos(tempX, tempY), tilePos2, possibleMoves)) > 0) { possibleMoves.back().prependTile(tilePos1); numberOfPaths += simplePath; } tempX += dx.at(i); tempY += dy.at(i); } } return numberOfPaths; } int Board::findSimplePath(TilePos const & tilePos1, TilePos const & tilePos2, PossibleMoves & possibleMoves) const { auto numberOfPaths = 0; Path path; // Find direct line (path of 1 segment) if (canMakePath(tilePos1, tilePos2)) { path.push_back(tilePos1); path.push_back(tilePos2); possibleMoves.push_back(PossibleMove(path)); ++numberOfPaths; } // If the tiles are in the same row or column, then a // a 'simple path' cannot be found between them // That is, canMakePath should have returned true above if // that was possible if (tilePos1.x() == tilePos2.x() || tilePos1.y() == tilePos2.y()) { return numberOfPaths; } // I isolate the special code when tiles can slide even if it duplicates code for now // Can we make a path sliding tiles ?, the slide move is always first, then a normal path if (m_tilesCanSlideFlag) { Slide slide; // Find path of 2 segments (route A) if (canSlideTiles(tilePos1, TilePos(tilePos2.x(), tilePos1.y()), slide) && canMakePath(TilePos(tilePos2.x(), tilePos1.y()), tilePos2)) { path.clear(); path.push_back(tilePos1); path.push_back(TilePos(tilePos2.x(), tilePos1.y())); path.push_back(tilePos2); possibleMoves.push_back(PossibleMove(path, slide)); ++numberOfPaths; } // Find path of 2 segments (route B) if (canSlideTiles(tilePos1, TilePos(tilePos1.x(), tilePos2.y()), slide) && canMakePath(TilePos(tilePos1.x(), tilePos2.y()), tilePos2)) { path.clear(); path.push_back(tilePos1); path.push_back(TilePos(tilePos1.x(), tilePos2.y())); path.push_back(tilePos2); possibleMoves.push_back(PossibleMove(path, slide)); ++numberOfPaths; } } // Even if tiles can slide, a path could still be done without sliding // Find path of 2 segments (route A) if (field(TilePos(tilePos2.x(), tilePos1.y())) == EMPTY && canMakePath(tilePos1, TilePos(tilePos2.x(), tilePos1.y())) && canMakePath(TilePos(tilePos2.x(), tilePos1.y()), tilePos2)) { path.clear(); path.push_back(tilePos1); path.push_back(TilePos(tilePos2.x(), tilePos1.y())); path.push_back(tilePos2); possibleMoves.push_back(PossibleMove(path)); ++numberOfPaths; } // Find path of 2 segments (route B) if (field(TilePos(tilePos1.x(), tilePos2.y())) == EMPTY && canMakePath(tilePos1, TilePos(tilePos1.x(), tilePos2.y())) && canMakePath(TilePos(tilePos1.x(), tilePos2.y()), tilePos2)) { path.clear(); path.push_back(tilePos1); path.push_back(TilePos(tilePos1.x(), tilePos2.y())); path.push_back(tilePos2); possibleMoves.push_back(PossibleMove(path)); ++numberOfPaths; } return numberOfPaths; } void Board::drawPossibleMoves(bool b) { if (m_possibleMoves.empty()) { return; } m_paintPossibleMoves = b; update(); } void Board::drawConnection() { m_paintInProgress = true; if (m_connection.empty()) { return; } auto const tile1 = TilePos(m_connection.front().x(), m_connection.front().y()); auto const tile2 = TilePos(m_connection.back().x(), m_connection.back().y()); // lighten the fields repaintTile(tile1); repaintTile(tile2); m_paintConnection = true; update(); } void Board::undrawConnection() { if (m_tileRemove1.x() != -1) { setField(m_tileRemove1, EMPTY); setField(m_tileRemove2, EMPTY); applyGravity(); m_tileRemove1.setX(-1); update(); emit tileCountChanged(); } // is already undrawn? if (m_connection.empty()) { return; } // Redraw all affected fields auto const oldConnection = m_connection; m_connection.clear(); m_paintConnection = false; auto pt1 = oldConnection.cbegin(); auto pt2 = pt1 + 1; // TODO: std::next? while (pt2 != oldConnection.cend()) { if (pt1->y() == pt2->y()) { for (auto i = qMin(pt1->x(), pt2->x()); i <= qMax(pt1->x(), pt2->x()); ++i) { repaintTile(TilePos(i, pt1->y())); } } else { for (auto i = qMin(pt1->y(), pt2->y()); i <= qMax(pt1->y(), pt2->y()); ++i) { repaintTile(TilePos(pt1->x(), i)); } } ++pt1; ++pt2; } PossibleMoves dummyPossibleMoves; // game is over? if (!hint_I(dummyPossibleMoves)) { m_gameClock.pause(); emit endOfGame(); } m_paintInProgress = false; } QPoint Board::midCoord(TilePos const & tilePos) const { QPoint p; auto const w = m_tiles.qWidth() * 2; auto const h = m_tiles.qHeight() * 2; if (tilePos.x() == -1) { p.setX(xOffset() - (w / 4)); } else if (tilePos.x() == xTiles()) { p.setX(xOffset() + (w * xTiles()) + (w / 4)); } else { p.setX(xOffset() + (w * tilePos.x()) + (w / 2)); } if (tilePos.y() == -1) { p.setY(yOffset() - (w / 4)); } else if (tilePos.y() == yTiles()) { p.setY(yOffset() + (h * yTiles()) + (w / 4)); } else { p.setY(yOffset() + (h * tilePos.y()) + (h / 2)); } return p; } void Board::setDelay(int newValue) { if (m_delay == newValue) { return; } m_delay = newValue; } int Board::delay() const { return m_delay; } void Board::madeMove(TilePos const & tilePos1, TilePos const & tilePos2, Slide slide) { std::unique_ptr move; if (slide.empty()) { move = std::make_unique(tilePos1, tilePos2, field(tilePos1), field(tilePos2)); } else { move = std::make_unique(tilePos1, tilePos2, field(tilePos1), field(tilePos2), slide); } m_undo.push_back(std::move(move)); if (!m_redo.empty()) { m_redo.clear(); } emit changed(); } bool Board::canUndo() const { return !m_undo.empty(); } bool Board::canRedo() const { return !m_redo.empty(); } void Board::undo() { if (!canUndo()) { return; } clearHighlight(); undrawConnection(); auto move = std::move(m_undo.back()); m_undo.pop_back(); if (gravityFlag()) { // When both tiles reside in the same column, the order of undo is // significant (we must undo the lower tile first). // Also in that case there cannot be a slide if (move->x1() == move->x2() && move->y1() < move->y2()) { move->swapTiles(); } // if there is no slide, keep previous implementation: move both column up if (!move->hasSlide()) { qCDebug(KSHISEN_General) << "[undo] gravity from a no slide move"; // move tiles from the first column up for (decltype(move->y1()) y = 0; y < move->y1(); ++y) { setField(TilePos(move->x1(), y), field(TilePos(move->x1(), y + 1))); repaintTile(TilePos(move->x1(), y)); } // move tiles from the second column up for (decltype(move->y2()) y = 0; y < move->y2(); ++y) { setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); repaintTile(TilePos(move->x2(), y)); } } else { // else check all tiles from the slide that may have fallen down 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() << ")"; // horizontal slide // because tiles that slides horizontaly may fall down // in columns different than the taken tiles columns // we need to take them back up then undo the slide if (move->slideY1() == move->slideY2()) { qCDebug(KSHISEN_General) << "[undo] gravity from horizontal slide"; // last slide tile went from slideX1() -> slideX2() // the number of slidden tiles is n = abs(x1 - slideX1()) auto n = move->x1() - move->slideX1(); if (n < 0) { n = -n; } // distance slidden is auto dx = move->slideX2() - move->slideX1(); if (dx < 0) { dx = -dx; } qCDebug(KSHISEN_General) << "[undo] n =" << n; // slidden tiles may fall down after the slide // so any tiles on top of the columns between // slideX2() -> slideX2() +/- n (excluded) should go up to slideY1() if (move->slideX2() > move->slideX1()) { // slide to the right qCDebug(KSHISEN_General) << "[undo] slide right"; for (auto i = move->slideX2(); i > move->slideX2() - n; --i) { // find top tile decltype(yTiles()) j = 0; for (j = 0; j < yTiles(); ++j) { if (field(TilePos(i, j)) != EMPTY) { break; } } // ignore if the tile did not fall if (j <= move->slideY1()) { continue; } qCDebug(KSHISEN_General) << "[undo] moving (" << i << "," << j << ") up to (" << i << "," << move->slideY1() << ")"; // put it back up setField(TilePos(i, move->slideY1()), field(TilePos(i, j))); setField(TilePos(i, j), EMPTY); repaintTile(TilePos(i, j)); repaintTile(TilePos(i, move->slideY1())); } } else { // slide to the left qCDebug(KSHISEN_General) << "[undo] slide left"; for (auto i = move->slideX2(); i < move->slideX2() + n; ++i) { // find top tile decltype(yTiles()) j = 0; for (j = 0; j < yTiles(); ++j) { if (field(TilePos(i, j)) != EMPTY) { break; } } // ignore if the tile did not fall if (j <= move->slideY1()) { continue; } qCDebug(KSHISEN_General) << "[undo] moving (" << i << "," << j << ") up to (" << i << "," << move->slideY1() << ")"; // put it back up setField(TilePos(i, move->slideY1()), field(TilePos(i, j))); setField(TilePos(i, j), EMPTY); repaintTile(TilePos(i, j)); repaintTile(TilePos(i, move->slideY1())); } } qCDebug(KSHISEN_General) << "[undo] moving up column x2" << move->x2(); // move tiles from the second column up for (decltype(move->y2()) y = 0; y <= move->y2(); ++y) { qCDebug(KSHISEN_General) << "[undo] moving up tile" << y + 1; setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); repaintTile(TilePos(move->x2(), y)); } // and all columns that fell after the tiles slidden between // only if they were not replaced by a sliding tile !! // x1 -> x1+dx should go up one // if their height > slideY1() // because they have fallen after the slide if (move->slideX2() > move->slideX1()) { // slide to the right if (move->slideY1() > 0) { for (auto i = move->x1() + dx; i >= move->x1(); --i) { qCDebug(KSHISEN_General) << "[undo] moving up column" << i << "until" << move->slideY1(); for (decltype(move->slideY1()) j = 0; j < move->slideY1(); ++j) { qCDebug(KSHISEN_General) << "[undo] moving up tile" << j + 1; setField(TilePos(i, j), field(TilePos(i, j + 1))); repaintTile(TilePos(i, j)); } qCDebug(KSHISEN_General) << "[undo] clearing last tile" << move->slideY1(); setField(TilePos(i, move->slideY1()), EMPTY); repaintTile(TilePos(i, move->slideY1())); } } } else { // slide to the left if (move->slideY1() > 0) { for (auto i = move->x1() - dx; i <= move->x1(); ++i) { qCDebug(KSHISEN_General) << "[undo] moving up column" << i << "until" << move->slideY1(); for (decltype(move->slideY1()) j = 0; j < move->slideY1(); ++j) { qCDebug(KSHISEN_General) << "[undo] moving up tile" << j + 1; setField(TilePos(i, j), field(TilePos(i, j + 1))); repaintTile(TilePos(i, j)); } qCDebug(KSHISEN_General) << "[undo] clearing last tile" << move->slideY1(); setField(TilePos(i, move->slideY1()), EMPTY); repaintTile(TilePos(i, move->slideY1())); } } } qCDebug(KSHISEN_General) << "[undo] reversing slide"; // then undo the slide to put the tiles back to their original location reverseSlide(TilePos(move->x1(), move->y1()), move->slide()); } else { qCDebug(KSHISEN_General) << "[undo] gravity from vertical slide"; // vertical slide, in fact nothing special is necessary // the default implementation works because it only affects // the two columns were tiles were taken // move tiles from the first column up for (decltype(move->y1()) y = 0; y < move->y1(); ++y) { setField(TilePos(move->x1(), y), field(TilePos(move->x1(), y + 1))); repaintTile(TilePos(move->x1(), y)); } // move tiles from the second column up for (decltype(move->y2()) y = 0; y < move->y2(); ++y) { setField(TilePos(move->x2(), y), field(TilePos(move->x2(), y + 1))); repaintTile(TilePos(move->x2(), y)); } } } } else { // no gravity // undo slide if any if (move->hasSlide()) { // perform the slide in reverse reverseSlide(TilePos(move->x1(), move->y1()), move->slide()); } } // replace taken tiles setField(TilePos(move->x1(), move->y1()), move->tile1()); setField(TilePos(move->x2(), move->y2()), move->tile2()); repaintTile(TilePos(move->x1(), move->y1())); repaintTile(TilePos(move->x2(), move->y2())); m_redo.push_front(std::move(move)); emit changed(); } void Board::redo() { if (canRedo()) { clearHighlight(); undrawConnection(); auto move = std::move(m_redo.front()); m_redo.pop_front(); // redo the slide if any if (move->hasSlide()) { Slide s; s.push_back(TilePos(move->slideX1(), move->slideY1())); s.push_back(TilePos(move->slideX2(), move->slideY2())); performSlide(TilePos(move->x1(), move->y1()), s); } setField(TilePos(move->x1(), move->y1()), EMPTY); setField(TilePos(move->x2(), move->y2()), EMPTY); repaintTile(TilePos(move->x1(), move->y1())); repaintTile(TilePos(move->x2(), move->y2())); applyGravity(); m_undo.push_back(std::move(move)); emit changed(); } } void Board::showHint() { undrawConnection(); if (hint_I(m_possibleMoves)) { m_connection = m_possibleMoves.front().path(); drawConnection(); } } #ifdef DEBUGGING void Board::makeHintMove() { PossibleMoves possibleMoves; if (hint_I(possibleMoves)) { m_markX = -1; m_markY = -1; marked(TilePos(possibleMoves.front().path().front().x(), possibleMoves.front().path().front().y())); marked(TilePos(possibleMoves.front().path().back().x(), possibleMoves.front().path().back().y())); } } void Board::dumpBoard() const { dumpBoard(m_field); } void Board::dumpBoard(const std::vector & field) const { qCDebug(KSHISEN_General) << "Board contents:"; for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { QString row; for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { auto tile = field.at(y * xTiles() + x); if (tile == EMPTY) { row += QLatin1String(" --"); } else { row += QStringLiteral("%1").arg(tile, 3); } } qCDebug(KSHISEN_General) << row; } } #endif int Board::lineWidth() const { auto width = qRound(m_tiles.height() / 10.0); if (width < 3) { width = 3; } return width; } bool Board::hint_I(PossibleMoves & possibleMoves) const { std::array done{}; // Appended {} initialises with zeroes here. for (decltype(xTiles()) x = 0; x < xTiles(); ++x) { for (decltype(yTiles()) y = 0; y < yTiles(); ++y) { auto const tile = field(TilePos(x, y)); if (tile != EMPTY && done.at(tile - 1) != 4) { // for all these types of tile search paths for (decltype(xTiles()) xx = 0; xx < xTiles(); ++xx) { for (decltype(yTiles()) yy = 0; yy < yTiles(); ++yy) { if (xx != x || yy != y) { if (tilesMatch(field(TilePos(xx, yy)), tile)) { if (findPath(TilePos(x, y), TilePos(xx, yy), possibleMoves) > 0) { return true; } } } } } done.at(tile - 1)++; } } } return false; } int Board::tilesLeft() const { return std::count_if(m_field.begin(), m_field.end(), [](auto field) { return field != EMPTY; }); } int Board::currentTime() const { return m_gameClock.seconds(); } bool Board::isSolvable(bool restore) { decltype(m_field) oldField; if (!restore) { oldField = m_field; } PossibleMoves p; while (hint_I(p)) { auto const tile1 = TilePos(p.front().path().front().x(), p.front().path().front().y()); auto const tile2 = TilePos(p.front().path().back().x(), p.front().path().back().y()); if (!tilesMatch(field(tile1), field(tile2))) { auto errMessage = QStringLiteral("Removing unmatched tiles: (%1,%2) => %3 (%4,%5) => %6") .arg(p.front().path().front().x()) .arg(p.front().path().front().y()) .arg(field(tile1)) .arg(p.front().path().back().x()) .arg(p.front().path().back().y()) .arg(field(tile2)); qCCritical(KSHISEN_General) << errMessage; } setField(tile1, EMPTY); setField(tile2, EMPTY); } auto const left = tilesLeft(); if (!restore) { m_field = oldField; } return left == 0; } bool Board::solvableFlag() const { return m_solvableFlag; } void Board::setSolvableFlag(bool enabled) { if (m_solvableFlag == enabled) { return; } m_solvableFlag = enabled; // if the solvable flag was set and the current game is not solvable, start a new game if (m_solvableFlag && !isSolvable(true)) { newGame(); } } bool Board::gravityFlag() const { return m_gravityFlag; } void Board::setGravityFlag(bool enabled) { if (m_gravityFlag == enabled) { return; } m_gravityFlag = enabled; // start a new game if the player is in the middle of a game if (canUndo() || canRedo()) { newGame(); } } void Board::setChineseStyleFlag(bool enabled) { if (m_chineseStyleFlag == enabled) { return; } m_chineseStyleFlag = enabled; // we need to force a newGame() because board generation is different newGame(); } void Board::setTilesCanSlideFlag(bool enabled) { if (m_tilesCanSlideFlag == enabled) { return; } m_tilesCanSlideFlag = enabled; // start a new game if the player is in the middle of a game if (canUndo() || canRedo()) { newGame(); } } void Board::setPauseEnabled(bool enabled) { if ((m_gameState == GameState::Paused && enabled) || m_gameState == GameState::Stuck) { return; } if (enabled) { m_gameState = GameState::Paused; m_gameClock.pause(); } else { m_gameState = GameState::Normal; m_gameClock.resume(); } emit changed(); update(); } QSize Board::sizeHint() const { auto dpi = logicalDpiX(); if (dpi < 75) { dpi = 75; } return QSize(9 * dpi, 7 * dpi); } void Board::resetTimer() { m_gameClock.restart(); } void Board::resetUndo() { if (!canUndo()) { return; } m_undo.clear(); } void Board::resetRedo() { if (!canRedo()) { return; } m_redo.clear(); } void Board::setGameStuckEnabled(bool enabled) { if (m_gameState == GameState::Stuck && enabled) { return; } if (enabled) { m_gameState = GameState::Stuck; m_gameClock.pause(); } else { m_gameState = GameState::Normal; m_gameClock.resume(); } emit changed(); update(); } void Board::setGameOverEnabled(bool enabled) { if (m_gameState == GameState::Over && enabled) { return; } m_gameState = GameState::Over; emit changed(); update(); } void Board::setCheatModeEnabled(bool enabled) { if (m_cheat == enabled) { return; } m_cheat = enabled; emit cheatStatusChanged(); } bool Board::isOver() const { return m_gameState == GameState::Over; } bool Board::isPaused() const { return m_gameState == GameState::Paused; } bool Board::isStuck() const { return m_gameState == GameState::Stuck; } bool Board::hasCheated() const { return m_cheat; } void Board::setSoundsEnabled(bool enabled) { Prefs::setSounds(enabled); Prefs::self()->save(); } bool Board::isValidPos(TilePos const & tilePos) const { return tilePos.x() >= 0 && tilePos.y() >= 0 && tilePos.x() < xTiles() && tilePos.y() < yTiles(); } bool Board::isValidPosWithOutline(TilePos const & tilePos) const { return tilePos.x() >= -1 && tilePos.y() >= -1 && tilePos.x() <= xTiles() && tilePos.y() <= yTiles(); } bool Board::isTileFlower(int tile) const { return tile >= FLOWERS_START && tile <= (FLOWERS_START + 3); } bool Board::isTileSeason(int tile) const { return tile >= SEASONS_START && tile <= (SEASONS_START + 3); } } // namespace KShisen // vim: expandtab:tabstop=4:shiftwidth=4 // kate: space-indent on; indent-width 4