diff --git a/src/gamestate.cpp b/src/gamestate.cpp index 999d758..1ce3811 100644 --- a/src/gamestate.cpp +++ b/src/gamestate.cpp @@ -1,5 +1,6 @@ #include "gamestate.h" #include +#include #include #include #include @@ -616,149 +617,180 @@ void GameState::ensureColumnRevealed(int columnId) { qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; } -std::optional GameState::canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer, int depth) const { - // Check if the game is already won +std::pair, int> GameState::canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer) const { if (m_gameWon) - return true; + return std::make_pair(true, 0); // Already won at depth 0 - // Limit evaluation time (ensures this doesn't block the game) - if (timer.hasExpired(MAX_EVAL_TIME)) - return std::nullopt; + // Go over all possible moves using BFS - // Limit depth (ensures we don't spend all eval time looking at a single branch.) - // Note that it might be a good idea to switch to a BFS type search, instead of depth - // limiting DFS. - if (depth > MAX_DEPTH) - return std::nullopt; + QQueue> stateQueue; // BFS queue (game state, depth) - // Generate the current state hash - QString currentStateHash = generateStateHash(); + // Clone the current state as the root of BFS + GameState* initialState = this->clone(); + stateQueue.enqueue({initialState, 0}); + visitedStates.insert(initialState->generateStateHash()); - // Check if the state has already been explored - if (visitedStates.contains(currentStateHash)) - return false; + int lastDepth = 0; + while (!stateQueue.isEmpty()) { + auto [currentState, depth] = stateQueue.dequeue(); + auto currentHash = currentState->generateStateHash(); + lastDepth = depth; - // Mark the current state as visited - visitedStates.insert(currentStateHash); - - // 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; - - const ColumnSlot* topSlot = columnStack.last(); - for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { - if (!isFoundationMoveValid(*topSlot->card(), foundationId)) - continue; - - GameState* clonedState = this->clone(); - assert(clonedState->moveColumnCardToFoundation(columnId, foundationId)); - assert(clonedState->generateStateHash() != generateStateHash()); - - auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); - delete clonedState; - - if (res.value_or(false)) - return true; - if (!res.has_value()) - return std::nullopt; - } - } - - // Simulate throwaway pile moves - if (!m_throwawayPile.isEmpty()) { - const PlayingCard* topCard = m_throwawayPile.last(); - - // Move to foundation - for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { - if (!isFoundationMoveValid(*topCard, foundationId)) - continue; - - GameState* clonedState = this->clone(); - assert(clonedState->moveThrownCardToFoundation(foundationId)); - assert(clonedState->generateStateHash() != generateStateHash()); - - auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); - delete clonedState; - - if (res.value_or(false)) - return true; - if (!res.has_value()) - return std::nullopt; + // Limit evaluation time (ensures this doesn't block the game) + if (timer.hasExpired(MAX_EVAL_TIME)) { + delete currentState; + return std::make_pair(std::nullopt, depth); } - // Move to columns - for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) { - if (!isColumnMoveValid(*topCard, toColumnId)) + // Try moves from columns to foundation + for (int columnId = 0; columnId < currentState->m_columns.size(); ++columnId) { + const auto& columnStack = currentState->m_columns[columnId]; + if (columnStack.isEmpty()) continue; - GameState* clonedState = this->clone(); - assert(clonedState->moveThrownCardToColumn(toColumnId)); - assert(clonedState->generateStateHash() != generateStateHash()); - - auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); - delete clonedState; - - if (res.value_or(false)) - return true; - if (!res.has_value()) - return std::nullopt; - } - } - - // Simulate draw pile move - // (The condition also handles the case where there's only one card in the throwaway pile, - // which means drawing would just result in flipping and re-drawing the same card.) - if (!(m_drawPile.isEmpty() && m_throwawayPile.size() <= 1)) { - GameState* clonedState = this->clone(); - assert(clonedState->drawNextCard()); - assert(clonedState->generateStateHash() != generateStateHash()); - - auto res = clonedState->canWinThroughSimulation(visitedStates, timer, 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)) + const ColumnSlot* topSlot = columnStack.last(); + for (int foundationId = 0; foundationId < currentState->m_foundation.size(); ++foundationId) { + if (!currentState->isFoundationMoveValid(*topSlot->card(), foundationId)) continue; - GameState* clonedState = this->clone(); - assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); - assert(clonedState->generateStateHash() != generateStateHash()); + GameState* newState = currentState->clone(); + assert(newState->moveColumnCardToFoundation(columnId, foundationId)); + QString stateHash = newState->generateStateHash(); + assert(currentHash != stateHash); - auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); - delete clonedState; + if (newState->m_gameWon) { + delete newState; + delete currentState; + return std::make_pair(true, depth + 1); // Return depth if game won + } - if (res.value_or(false)) - return true; - if (!res.has_value()) - return std::nullopt; + if (!visitedStates.contains(stateHash)) { + visitedStates.insert(stateHash); + stateQueue.enqueue({newState, depth + 1}); + } else { + delete newState; + } } } + + // Try moves from throwaway pile to foundation/columns + if (!currentState->m_throwawayPile.isEmpty()) { + const PlayingCard* topCard = currentState->m_throwawayPile.last(); + + // Move to foundation + for (int foundationId = 0; foundationId < currentState->m_foundation.size(); ++foundationId) { + if (!currentState->isFoundationMoveValid(*topCard, foundationId)) + continue; + + GameState* newState = currentState->clone(); + assert(newState->moveThrownCardToFoundation(foundationId)); + QString stateHash = newState->generateStateHash(); + assert(currentHash != stateHash); + + if (newState->m_gameWon) { + delete newState; + delete currentState; + return std::make_pair(true, depth + 1); + } + + if (!visitedStates.contains(stateHash)) { + visitedStates.insert(stateHash); + stateQueue.enqueue({newState, depth + 1}); + } else { + delete newState; + } + } + + // Move to columns + for (int toColumnId = 0; toColumnId < currentState->m_columns.size(); ++toColumnId) { + if (!currentState->isColumnMoveValid(*topCard, toColumnId)) + continue; + + GameState* newState = currentState->clone(); + assert(newState->moveThrownCardToColumn(toColumnId)); + QString stateHash = newState->generateStateHash(); + assert(currentHash != stateHash); + + if (newState->m_gameWon) { + delete newState; + delete currentState; + return std::make_pair(true, depth + 1); + } + + if (!visitedStates.contains(stateHash)) { + visitedStates.insert(stateHash); + stateQueue.enqueue({newState, depth + 1}); + } else { + delete newState; + } + } + } + + // Try draw pile move + // (The condition also handles the case where there's only one card in the throwaway pile, + // which means drawing would just result in flipping and re-drawing the same card.) + if (!(currentState->m_drawPile.isEmpty() && currentState->m_throwawayPile.size() <= 1)) { + GameState* newState = currentState->clone(); + assert(newState->drawNextCard()); + QString stateHash = newState->generateStateHash(); + assert(currentHash != stateHash); + + if (newState->m_gameWon) { + delete newState; + delete currentState; + return std::make_pair(true, depth + 1); + } + + if (!visitedStates.contains(stateHash)) { + visitedStates.insert(stateHash); + stateQueue.enqueue({newState, depth + 1}); + } else { + delete newState; + } + } + + // Try column-to-column moves + for (int fromColumnId = 0; fromColumnId < currentState->m_columns.size(); ++fromColumnId) { + const auto& fromColumnStack = currentState->m_columns[fromColumnId]; + if (fromColumnStack.isEmpty()) + continue; + + for (int toColumnId = 0; toColumnId < currentState->m_columns.size(); ++toColumnId) { + if (fromColumnId == toColumnId) + continue; + + for (int fromCardIndex = 0; fromCardIndex < fromColumnStack.size(); ++fromCardIndex) { + const ColumnSlot* fromSlot = fromColumnStack[fromCardIndex]; + if (!fromSlot->isRevealed()) + continue; + if (!currentState->isColumnMoveValid(*fromSlot->card(), toColumnId)) + continue; + + GameState* newState = currentState->clone(); + assert(newState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); + QString stateHash = newState->generateStateHash(); + assert(currentHash != stateHash); + + if (newState->m_gameWon) { + delete newState; + delete currentState; + return std::make_pair(true, depth + 1); + } + + if (!visitedStates.contains(stateHash)) { + visitedStates.insert(stateHash); + stateQueue.enqueue({newState, depth + 1}); + } else { + delete newState; + } + } + } + } + + delete currentState; // Cleanup current state } - // No paths lead to a win - return false; + return std::make_pair(false, lastDepth); // No solution } QVariantList GameState::drawPile() const { @@ -797,7 +829,7 @@ bool GameState::gameWon() const { return m_gameWon; } -std::optional GameState::isWinnable() const { +std::pair, int> GameState::isWinnable() const { qDebug() << "--- Simulating winning scenario ---"; QElapsedTimer timer; @@ -809,7 +841,7 @@ std::optional GameState::isWinnable() const { timer.start(); QSet visitedStates; - std::optional res = canWinThroughSimulation(visitedStates, timer, 0); + std::pair, int> res = canWinThroughSimulation(visitedStates, timer); qint64 elapsedTime = timer.elapsed(); // Restore the original message handler diff --git a/src/gamestate.h b/src/gamestate.h index b7daf1c..f5d746b 100644 --- a/src/gamestate.h +++ b/src/gamestate.h @@ -12,7 +12,6 @@ // Limits for checking winnability #define MAX_EVAL_TIME 100 // Evaluation time limit (ms) -#define MAX_DEPTH 100 // Max moves into the future limit class GameState : public QObject { Q_OBJECT @@ -39,7 +38,7 @@ class GameState : public QObject { Q_INVOKABLE void dealCards(); Q_INVOKABLE void setupWinningDeck(); Q_INVOKABLE bool drawNextCard(); - Q_INVOKABLE std::optional isWinnable() const; // TODO: Implement as Q_PROPERTY instead + Q_INVOKABLE std::pair, int> isWinnable() const; // TODO: Implement as Q_PROPERTY instead // Manual moves (from X to Y) Q_INVOKABLE bool moveThrownCardToColumn(int columnId); @@ -80,7 +79,7 @@ class GameState : public QObject { void ensureColumnRevealed(int columnId); - std::optional canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer, int depth) const; + std::pair, int> canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer) const; }; #endif // GAMESTATE_H