diff --git a/qml/DrawPile.qml b/qml/DrawPile.qml index ffca6cf..771c643 100644 --- a/qml/DrawPile.qml +++ b/qml/DrawPile.qml @@ -78,14 +78,6 @@ Item { MouseArea { anchors.fill: parent - onClicked: { - if (GameState.drawNextCard()) { - if (GameState.isWinnable()) { - console.log("Still winnable"); - } else { - console.warn("Game is lost"); - } - } - } + onClicked: GameState.drawNextCard() } } diff --git a/qml/Tableau.qml b/qml/Tableau.qml index 9fb64f8..57572f5 100644 --- a/qml/Tableau.qml +++ b/qml/Tableau.qml @@ -50,13 +50,7 @@ Row { isFaceDown: parent.colSlot ? !parent.colSlot.revealed : false onClicked: { if (parent.colSlot && parent.colSlot.revealed) { - if (GameState.autoMoveColumnCard(parent.columnId, parent.cardId)) { - if (GameState.isWinnable()) { - console.log("Still winnable"); - } else { - console.log("Game is lost"); - } - } + GameState.autoMoveColumnCard(parent.columnId, parent.cardId); } } } diff --git a/qml/ThrowawayPile.qml b/qml/ThrowawayPile.qml index c0e16e6..199a460 100644 --- a/qml/ThrowawayPile.qml +++ b/qml/ThrowawayPile.qml @@ -26,13 +26,7 @@ Row { // 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) { - if (GameState.autoMoveThrownCard()) { - if (GameState.isWinnable()) { - console.log("Still winnable"); - } else { - console.log("Game is lost"); - } - } + GameState.autoMoveThrownCard(); } } } diff --git a/src/gamestate.cpp b/src/gamestate.cpp index 5340168..088b295 100644 --- a/src/gamestate.cpp +++ b/src/gamestate.cpp @@ -1,14 +1,18 @@ #include "gamestate.h" #include #include +#include #include #include #include #include -GameState::GameState(QObject* parent, bool preDealCards) : QObject{parent}, m_columns(7), m_foundation(4), m_gameWon(false) { +GameState::GameState(QObject* parent, bool preDealCards, bool enableWinnabilitySim) : + QObject{parent}, m_columns(7), m_foundation(4), m_moveAmt(0), m_gameWon(false), m_prelimWin(false), m_isWinnable({std::nullopt, 0}), + m_enableWinnabilitySim(enableWinnabilitySim) { assert(connect(this, &GameState::foundationChanged, this, &GameState::onFoundationChanged)); assert(connect(this, &GameState::throwawayPileChanged, this, &GameState::onThrowawayPileChanged)); + assert(connect(this, &GameState::moveAmountChanged, this, &GameState::onMoveAmountChanged)); if (preDealCards) dealCards(); @@ -363,8 +367,12 @@ void GameState::onThrowawayPileChanged() { prelimWinCheck(); } -GameState* GameState::clone() const { - GameState* newGameState = new GameState(nullptr, false); +void GameState::onMoveAmountChanged() { + checkWinnable(); +} + +GameState* GameState::clone(bool enableWinnabilitySim) const { + GameState* newGameState = new GameState(nullptr, false, enableWinnabilitySim); // Deep copy the necessary data @@ -435,7 +443,8 @@ void GameState::cleanupBoard(bool emitChanges) { m_moveAmt = 0; // Note that we don't need to reset gameWon/prelimWin from here, as it's - // auto-checked from onFoundationChanged, which the emits trigger + // auto-checked from onFoundationChanged, which the emits trigger. Similarly + // isWinnable status will run from onMoveAmountChanged slot. if (emitChanges) { emit drawPileChanged(); @@ -871,6 +880,13 @@ bool GameState::gameWon() const { } std::pair, int> GameState::isWinnable() const { + return m_isWinnable; +} + +std::pair, int> GameState::checkWinnable() { + if (!m_enableWinnabilitySim) + return {std::nullopt, 0}; + qDebug() << "--- Simulating winning scenario ---"; QElapsedTimer timer; @@ -889,5 +905,11 @@ std::pair, int> GameState::isWinnable() const { qInstallMessageHandler(originalHandler); qDebug() << "--- Simulation end (result:" << res << ", took:" << elapsedTime << "ms) ---"; + + if (m_isWinnable != res) { + m_isWinnable = res; + emit isWinnableChanged(); + } + return res; } diff --git a/src/gamestate.h b/src/gamestate.h index 1d08fe1..a3e2f1c 100644 --- a/src/gamestate.h +++ b/src/gamestate.h @@ -9,6 +9,7 @@ #include #include #include +#include // Limits for checking winnability #define MAX_EVAL_TIME 100 // Evaluation time limit (ms) @@ -24,9 +25,10 @@ class GameState : public QObject { Q_PROPERTY(int moveAmount READ moveAmount NOTIFY moveAmountChanged) Q_PROPERTY(bool gameWon READ gameWon NOTIFY gameWonChanged) Q_PROPERTY(bool preliminaryWin READ preliminaryWin NOTIFY preliminaryWinChanged) + Q_PROPERTY(std::pair, int> isWinnable READ isWinnable NOTIFY isWinnableChanged) public: - explicit GameState(QObject* parent = nullptr, bool preDealCards = true); + explicit GameState(QObject* parent = nullptr, bool preDealCards = true, bool enableWinnabilitySim = true); ~GameState(); // Getters @@ -37,12 +39,12 @@ class GameState : public QObject { int moveAmount() const; bool preliminaryWin() const; bool gameWon() const; + std::pair, int> isWinnable() const; // General functions Q_INVOKABLE void dealCards(); Q_INVOKABLE void setupWinningDeck(); Q_INVOKABLE bool drawNextCard(); - 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); @@ -62,10 +64,12 @@ class GameState : public QObject { void moveAmountChanged(); void gameWonChanged(); void preliminaryWinChanged(); + void isWinnableChanged(); private slots: void onFoundationChanged(); void onThrowawayPileChanged(); + void onMoveAmountChanged(); private: QList m_drawPile; @@ -75,8 +79,10 @@ class GameState : public QObject { int m_moveAmt; bool m_gameWon; bool m_prelimWin; + std::pair, int> m_isWinnable; + bool m_enableWinnabilitySim; - GameState* clone() const; + GameState* clone(bool enableWinnabilitySim = false) const; void cleanupBoard(bool emitChanges); QString generateStateHash() const; @@ -91,7 +97,7 @@ class GameState : public QObject { bool winCheck(); bool prelimWinCheck(); - + std::pair, int> checkWinnable(); std::pair, int> canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer) const; };