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