From 52409493532d4c3707f3e9733bc1796e4a842800 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Fri, 6 Dec 2024 17:04:15 +0100 Subject: [PATCH] Significantly optimize the winnability simulation The original approach for calculating winnability first checked for inter-column movements, which isn't ideal, instead, the new logic now first attempts to make foundation pile movements. Additionally, this converts the function to return an option value, which will be null if the winnability check fails to determine the result within given maximum depth. This is a necessary check, as the original logic took really long to finish, especially if ran at the beginning of the game, where it could keep going for hundreds of moves. --- src/gamestate.cpp | 170 +++++++++++++++++++++++++++++----------------- src/gamestate.h | 8 ++- 2 files changed, 113 insertions(+), 65 deletions(-) diff --git a/src/gamestate.cpp b/src/gamestate.cpp index 61f609c..3b9b973 100644 --- a/src/gamestate.cpp +++ b/src/gamestate.cpp @@ -1,6 +1,7 @@ #include "gamestate.h" #include #include +#include #include #include @@ -620,62 +621,47 @@ void GameState::ensureColumnRevealed(int columnId) { qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; } -bool GameState::canWinThroughSimulation(QSet& visitedStates) const { +std::optional GameState::canWinThroughSimulation(QSet& visitedStates, int depth) const { // Check if the game is already won - if (m_gameWon) return true; + if (m_gameWon) + return true; + + // Limit evaluation to a max depth of 20 moves + if (depth > MAX_EVAL_DEPTH) + return std::nullopt; // Generate the current state hash QString currentStateHash = generateStateHash(); // Check if the state has already been explored - if (visitedStates.contains(currentStateHash)) return false; + if (visitedStates.contains(currentStateHash)) + return false; // Mark the current state as visited visitedStates.insert(currentStateHash); - // Simulate moves between columns - for (int fromColumnId = 0; fromColumnId < m_columns.size(); ++fromColumnId) { - const auto& fromColumnStack = m_columns[fromColumnId]; - if (fromColumnStack.isEmpty()) continue; - - for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) { - if (fromColumnId == toColumnId) continue; - - // Try all revealed cards in the column - for (int fromCardIndex = 0; fromCardIndex < fromColumnStack.size(); ++fromCardIndex) { - const ColumnSlot* fromSlot = fromColumnStack[fromCardIndex]; - if (!fromSlot->isRevealed()) continue; - if (!isColumnMoveValid(*fromSlot->card(), toColumnId)) continue; - - GameState* clonedState = this->clone(); - assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); - - if (clonedState->canWinThroughSimulation(visitedStates)) { - delete clonedState; - return true; - } - delete clonedState; - } - } - } - // Simulate column moves to the foundation for (int columnId = 0; columnId < m_columns.size(); ++columnId) { const auto& columnStack = m_columns[columnId]; - if (columnStack.isEmpty()) continue; + if (columnStack.isEmpty()) + continue; const ColumnSlot* topSlot = columnStack.last(); for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { - if (!isFoundationMoveValid(*topSlot->card(), foundationId)) continue; + if (!isFoundationMoveValid(*topSlot->card(), foundationId)) + continue; GameState* clonedState = this->clone(); assert(clonedState->moveColumnCardToFoundation(columnId, foundationId)); + assert(clonedState->generateStateHash() != generateStateHash()); - if (clonedState->canWinThroughSimulation(visitedStates)) { - delete clonedState; - return true; - } + auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); delete clonedState; + + if (res.value_or(false)) + return true; + if (!res.has_value()) + return std::nullopt; } } @@ -683,32 +669,40 @@ bool GameState::canWinThroughSimulation(QSet& visitedStates) const { if (!m_throwawayPile.isEmpty()) { const PlayingCard* topCard = m_throwawayPile.last(); - // Move to columns - for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) { - if (!isColumnMoveValid(*topCard, toColumnId)) continue; - - GameState* clonedState = this->clone(); - assert(clonedState->moveThrownCardToColumn(toColumnId)); - - if (clonedState->canWinThroughSimulation(visitedStates)) { - delete clonedState; - return true; - } - delete clonedState; - } - // Move to foundation for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { - if (!isFoundationMoveValid(*topCard, foundationId)) continue; + if (!isFoundationMoveValid(*topCard, foundationId)) + continue; GameState* clonedState = this->clone(); assert(clonedState->moveThrownCardToFoundation(foundationId)); + assert(clonedState->generateStateHash() != generateStateHash()); - if (clonedState->canWinThroughSimulation(visitedStates)) { - delete clonedState; - return true; - } + auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); delete clonedState; + + if (res.value_or(false)) + return true; + if (!res.has_value()) + return std::nullopt; + } + + // Move to columns + for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) { + if (!isColumnMoveValid(*topCard, toColumnId)) + continue; + + GameState* clonedState = this->clone(); + assert(clonedState->moveThrownCardToColumn(toColumnId)); + assert(clonedState->generateStateHash() != generateStateHash()); + + auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + delete clonedState; + + if (res.value_or(false)) + return true; + if (!res.has_value()) + return std::nullopt; } } @@ -716,20 +710,54 @@ bool GameState::canWinThroughSimulation(QSet& visitedStates) const { if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) { GameState* clonedState = this->clone(); assert(clonedState->drawNextCard()); + assert(clonedState->generateStateHash() != generateStateHash()); - if (clonedState->canWinThroughSimulation(visitedStates)) { - delete clonedState; - return true; - } + auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); delete clonedState; + + if (res.value_or(false)) + return true; + if (!res.has_value()) + return std::nullopt; + } + + // Simulate moves between columns + for (int fromColumnId = 0; fromColumnId < m_columns.size(); ++fromColumnId) { + const auto& fromColumnStack = m_columns[fromColumnId]; + if (fromColumnStack.isEmpty()) + continue; + + for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) { + if (fromColumnId == toColumnId) + continue; + + // Try all revealed cards in the column + for (int fromCardIndex = 0; fromCardIndex < fromColumnStack.size(); ++fromCardIndex) { + const ColumnSlot* fromSlot = fromColumnStack[fromCardIndex]; + if (!fromSlot->isRevealed()) + continue; + if (!isColumnMoveValid(*fromSlot->card(), toColumnId)) + continue; + + GameState* clonedState = this->clone(); + assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); + assert(clonedState->generateStateHash() != generateStateHash()); + + auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + delete clonedState; + + if (res.value_or(false)) + return true; + if (!res.has_value()) + return std::nullopt; + } + } } // No paths lead to a win return false; } - - QVariantList GameState::drawPile() const { QVariantList lst; for (auto& card : m_drawPile) { @@ -766,8 +794,24 @@ bool GameState::gameWon() const { return m_gameWon; } -bool GameState::isWinnable() const -{ +std::optional GameState::isWinnable() const { + qDebug() << "--- Simulating winning scenario ---"; + QElapsedTimer timer; + + // Redirect or suppress output by setting an empty handler + QtMessageHandler originalHandler = qInstallMessageHandler(nullptr); + qInstallMessageHandler([](QtMsgType /*type*/, const QMessageLogContext& /*context*/, const QString& /*msg*/) { + // Do nothing for now, effectively suppressing all messages + }); + + timer.start(); QSet visitedStates; - return !canWinThroughSimulation(visitedStates); + std::optional res = canWinThroughSimulation(visitedStates, 0); + qint64 elapsedTime = timer.elapsed(); + + // Restore the original message handler + qInstallMessageHandler(originalHandler); + + qDebug() << "--- Simulation end (result:" << res << ", took:" << elapsedTime << "ms) ---"; + return res; } diff --git a/src/gamestate.h b/src/gamestate.h index 1bf03ce..36a14a5 100644 --- a/src/gamestate.h +++ b/src/gamestate.h @@ -5,9 +5,13 @@ #include "playingcard.h" #include #include +#include #include #include +// Evaluation depth for checking win-ability (this may impact performance) +#define MAX_EVAL_DEPTH 80 + class GameState : public QObject { Q_OBJECT QML_ELEMENT @@ -33,7 +37,7 @@ class GameState : public QObject { Q_INVOKABLE void dealCards(); Q_INVOKABLE void setupWinningDeck(); Q_INVOKABLE bool drawNextCard(); - Q_INVOKABLE bool isWinnable() const; // TODO: Implement as Q_PROPERTY instead + Q_INVOKABLE std::optional isWinnable() const; // TODO: Implement as Q_PROPERTY instead // Manual moves (from X to Y) Q_INVOKABLE bool moveThrownCardToColumn(int columnId); @@ -74,7 +78,7 @@ class GameState : public QObject { void ensureColumnRevealed(int columnId); - bool canWinThroughSimulation(QSet& visitedStates) const; + std::optional canWinThroughSimulation(QSet& visitedStates, int depth) const; }; #endif // GAMESTATE_H