diff --git a/qml/DrawPile.qml b/qml/DrawPile.qml index a03b45a..a806428 100644 --- a/qml/DrawPile.qml +++ b/qml/DrawPile.qml @@ -7,6 +7,12 @@ CardModel { card: GameState.drawPile.length > 0 ? GameState.drawPile[GameState.drawPile.length - 1] : null isFaceDown: GameState.drawPile.length > 0 ? true : false onClicked: { - GameState.drawNextCard(); + if (GameState.drawNextCard()) { + if (GameState.isWinnable()) { + console.log("Still winnable") + } else { + console.warn("Game is lost") + } + } } } diff --git a/qml/Tableau.qml b/qml/Tableau.qml index c76c127..20c97e4 100644 --- a/qml/Tableau.qml +++ b/qml/Tableau.qml @@ -23,8 +23,15 @@ Row { card: col ? col.card : null isFaceDown: col ? !col.revealed : false onClicked: { - if (col && col.revealed) - GameState.autoMoveColumnCard(parent.index, index); + if (col && col.revealed) { + if (GameState.autoMoveColumnCard(parent.index, index)) { + if (GameState.isWinnable()) { + console.log("Still winnable") + } else { + console.log("Game is lost") + } + } + } } } } diff --git a/qml/ThrowawayPile.qml b/qml/ThrowawayPile.qml index 047a006..9140836 100644 --- a/qml/ThrowawayPile.qml +++ b/qml/ThrowawayPile.qml @@ -18,8 +18,15 @@ Row { onClicked: { // Only auto-move the last card in the throwaway pile // cards below it are shown, but shouldn't have a click effect - if (reversedIndex == 0) - GameState.autoMoveThrownCard(); + if (reversedIndex == 0) { + if (GameState.autoMoveThrownCard()) { + if (GameState.isWinnable()) { + console.log("Still winnable") + } else { + console.log("Game is lost") + } + } + } } } } diff --git a/src/gamestate.cpp b/src/gamestate.cpp index 71640fe..9cb6c17 100644 --- a/src/gamestate.cpp +++ b/src/gamestate.cpp @@ -1,5 +1,6 @@ #include "gamestate.h" #include +#include #include #include @@ -8,8 +9,11 @@ GameState::GameState(QObject* parent, bool preDealCards) : QObject{parent} { m_foundation.resize(4); m_columns.resize(7); + m_gameWon = false; - dealCards(); + if (preDealCards) { + dealCards(); + } } void GameState::dealCards() { @@ -359,6 +363,43 @@ void GameState::onFoundationChanged() { emit gameWonChanged(); } +GameState* GameState::clone() const { + GameState* newGameState = new GameState(nullptr, false); + + // Deep copy the necessary data + + for (auto curCard : m_drawPile) { + PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value()); + newGameState->m_drawPile.append(newCard); + } + + for (auto curCard : m_throwawayPile) { + PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value()); + newGameState->m_throwawayPile.append(newCard); + } + + for (int i = 0; i < m_columns.size(); ++i) { + for (auto curCol : m_columns[i]) { + auto curCard = curCol->card(); + PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value()); + ColumnSlot* newCol = new ColumnSlot(newCard, curCol->isRevealed()); + newGameState->m_columns[i].append(newCol); + } + } + + for (int i = 0; i < m_foundation.size(); ++i) { + for (auto curCard : m_foundation[i]) { + PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value()); + newGameState->m_foundation[i].append(newCard); + } + } + + newGameState->m_gameWon = m_gameWon; + + assert(this->generateStateHash() == newGameState->generateStateHash()); + return newGameState; +} + void GameState::cleanupBoard(bool emitChanges) { // Clean up all PlayingCard objects in the draw pile for (auto& card : m_drawPile) { @@ -401,6 +442,38 @@ void GameState::cleanupBoard(bool emitChanges) { } } +QString GameState::generateStateHash() const { + QString stateHash; + + // The repetition here is annoying, I know + for (const auto& column : m_columns) { + for (const ColumnSlot* slot : column) { + stateHash += QString::number(slot->card()->value()) + QString::number(slot->card()->suit()); + stateHash += slot->isRevealed() ? "t" : "f"; + stateHash += ","; + } + } + + for (const auto& foundationPile : m_foundation) { + for (const PlayingCard* card : foundationPile) { + stateHash += QString::number(card->value()) + QString::number(card->suit()) + ","; + } + } + + for (const PlayingCard* card : m_throwawayPile) { + stateHash += QString::number(card->value()) + QString::number(card->suit()) + ","; + } + + for (const PlayingCard* card : m_drawPile) { + stateHash += QString::number(card->value()) + QString::number(card->suit()) + ","; + } + + stateHash += m_gameWon ? "t" : "f"; + + return stateHash; +} + + bool GameState::tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId) { // 1. Try moving the card to the foundation const int foundationId = static_cast(cardToMove.suit()); @@ -527,6 +600,116 @@ void GameState::ensureColumnRevealed(int columnId) { qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; } +bool GameState::canWinThroughSimulation(QSet& visitedStates) const { + // Check if the game is already won + if (m_gameWon) return true; + + // Generate the current state hash + QString currentStateHash = generateStateHash(); + + // Check if the state has already been explored + 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; + + 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)); + + if (clonedState->canWinThroughSimulation(visitedStates)) { + delete clonedState; + return true; + } + delete clonedState; + } + } + + // Simulate throwaway pile moves + 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; + + GameState* clonedState = this->clone(); + assert(clonedState->moveThrownCardToFoundation(foundationId)); + + if (clonedState->canWinThroughSimulation(visitedStates)) { + delete clonedState; + return true; + } + delete clonedState; + } + } + + // Simulate draw pile move + if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) { + GameState* clonedState = this->clone(); + assert(clonedState->drawNextCard()); + + if (clonedState->canWinThroughSimulation(visitedStates)) { + delete clonedState; + return true; + } + delete clonedState; + } + + // No paths lead to a win + return false; +} + + + QVariantList GameState::drawPile() const { QVariantList lst; for (auto& card : m_drawPile) { @@ -562,3 +745,9 @@ QVariantList GameState::foundation() const { bool GameState::gameWon() const { return m_gameWon; } + +bool GameState::isWinnable() const +{ + QSet visitedStates; + return !canWinThroughSimulation(visitedStates); +} diff --git a/src/gamestate.h b/src/gamestate.h index 04d5f1d..582e2bf 100644 --- a/src/gamestate.h +++ b/src/gamestate.h @@ -4,6 +4,7 @@ #include "columnslot.h" #include "playingcard.h" #include +#include #include #include @@ -18,7 +19,7 @@ class GameState : public QObject { Q_PROPERTY(bool gameWon READ gameWon NOTIFY gameWonChanged) public: - explicit GameState(QObject* parent = nullptr); + explicit GameState(QObject* parent = nullptr, bool preDealCards = true); // Getters QVariantList drawPile() const; @@ -31,6 +32,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 // Manual moves (from X to Y) Q_INVOKABLE bool moveThrownCardToColumn(int columnId); @@ -59,7 +61,9 @@ class GameState : public QObject { QList> m_foundation; bool m_gameWon; + GameState* clone() const; void cleanupBoard(bool emitChanges); + QString generateStateHash() const; bool tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId = -1); bool tryAutoMoveMultipleCards(const QList& cards, int skipColumnId); @@ -68,6 +72,8 @@ class GameState : public QObject { bool isColumnMoveValid(const PlayingCard& cardToMove, int columnId) const; void ensureColumnRevealed(int columnId); + + bool canWinThroughSimulation(QSet& visitedStates) const; }; #endif // GAMESTATE_H diff --git a/src/main.cpp b/src/main.cpp index 74fe474..27864bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ int main(int argc, char* argv[]) { QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); auto gameState = engine.singletonInstance("Solitare", "GameState"); - gameState->setupWinningDeck(); + qDebug() << "Game winnable:" << gameState->isWinnable(); engine.load(QStringLiteral("qrc:/qml/Main.qml")); return app.exec();