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.
This commit is contained in:
ItsDrike 2024-12-06 17:04:15 +01:00
parent 2ec6206e26
commit 5240949353
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
2 changed files with 113 additions and 65 deletions

View file

@ -1,6 +1,7 @@
#include "gamestate.h" #include "gamestate.h"
#include <QDebug> #include <QDebug>
#include <QSet> #include <QSet>
#include <qelapsedtimer.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <random> #include <random>
@ -620,62 +621,47 @@ void GameState::ensureColumnRevealed(int columnId) {
qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId;
} }
bool GameState::canWinThroughSimulation(QSet<QString>& visitedStates) const { std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const {
// Check if the game is already won // 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 // Generate the current state hash
QString currentStateHash = generateStateHash(); QString currentStateHash = generateStateHash();
// Check if the state has already been explored // 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 // Mark the current state as visited
visitedStates.insert(currentStateHash); 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 // Simulate column moves to the foundation
for (int columnId = 0; columnId < m_columns.size(); ++columnId) { for (int columnId = 0; columnId < m_columns.size(); ++columnId) {
const auto& columnStack = m_columns[columnId]; const auto& columnStack = m_columns[columnId];
if (columnStack.isEmpty()) continue; if (columnStack.isEmpty())
continue;
const ColumnSlot* topSlot = columnStack.last(); const ColumnSlot* topSlot = columnStack.last();
for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { 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(); GameState* clonedState = this->clone();
assert(clonedState->moveColumnCardToFoundation(columnId, foundationId)); assert(clonedState->moveColumnCardToFoundation(columnId, foundationId));
assert(clonedState->generateStateHash() != generateStateHash());
if (clonedState->canWinThroughSimulation(visitedStates)) { auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false))
return true; return true;
} if (!res.has_value())
delete clonedState; return std::nullopt;
} }
} }
@ -683,32 +669,40 @@ bool GameState::canWinThroughSimulation(QSet<QString>& visitedStates) const {
if (!m_throwawayPile.isEmpty()) { if (!m_throwawayPile.isEmpty()) {
const PlayingCard* topCard = m_throwawayPile.last(); 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 // Move to foundation
for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) { for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) {
if (!isFoundationMoveValid(*topCard, foundationId)) continue; if (!isFoundationMoveValid(*topCard, foundationId))
continue;
GameState* clonedState = this->clone(); GameState* clonedState = this->clone();
assert(clonedState->moveThrownCardToFoundation(foundationId)); assert(clonedState->moveThrownCardToFoundation(foundationId));
assert(clonedState->generateStateHash() != generateStateHash());
if (clonedState->canWinThroughSimulation(visitedStates)) { auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false))
return true; 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; delete clonedState;
if (res.value_or(false))
return true;
if (!res.has_value())
return std::nullopt;
} }
} }
@ -716,20 +710,54 @@ bool GameState::canWinThroughSimulation(QSet<QString>& visitedStates) const {
if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) { if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) {
GameState* clonedState = this->clone(); GameState* clonedState = this->clone();
assert(clonedState->drawNextCard()); assert(clonedState->drawNextCard());
assert(clonedState->generateStateHash() != generateStateHash());
if (clonedState->canWinThroughSimulation(visitedStates)) { auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false))
return true; 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; delete clonedState;
if (res.value_or(false))
return true;
if (!res.has_value())
return std::nullopt;
}
}
} }
// No paths lead to a win // No paths lead to a win
return false; return false;
} }
QVariantList GameState::drawPile() const { QVariantList GameState::drawPile() const {
QVariantList lst; QVariantList lst;
for (auto& card : m_drawPile) { for (auto& card : m_drawPile) {
@ -766,8 +794,24 @@ bool GameState::gameWon() const {
return m_gameWon; return m_gameWon;
} }
bool GameState::isWinnable() const std::optional<bool> 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<QString> visitedStates; QSet<QString> visitedStates;
return !canWinThroughSimulation(visitedStates); std::optional<bool> 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;
} }

View file

@ -5,9 +5,13 @@
#include "playingcard.h" #include "playingcard.h"
#include <QObject> #include <QObject>
#include <QSet> #include <QSet>
#include <optional>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qqmllist.h> #include <qqmllist.h>
// Evaluation depth for checking win-ability (this may impact performance)
#define MAX_EVAL_DEPTH 80
class GameState : public QObject { class GameState : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
@ -33,7 +37,7 @@ class GameState : public QObject {
Q_INVOKABLE void dealCards(); Q_INVOKABLE void dealCards();
Q_INVOKABLE void setupWinningDeck(); Q_INVOKABLE void setupWinningDeck();
Q_INVOKABLE bool drawNextCard(); Q_INVOKABLE bool drawNextCard();
Q_INVOKABLE bool isWinnable() const; // TODO: Implement as Q_PROPERTY instead Q_INVOKABLE std::optional<bool> isWinnable() const; // TODO: Implement as Q_PROPERTY instead
// Manual moves (from X to Y) // Manual moves (from X to Y)
Q_INVOKABLE bool moveThrownCardToColumn(int columnId); Q_INVOKABLE bool moveThrownCardToColumn(int columnId);
@ -74,7 +78,7 @@ class GameState : public QObject {
void ensureColumnRevealed(int columnId); void ensureColumnRevealed(int columnId);
bool canWinThroughSimulation(QSet<QString>& visitedStates) const; std::optional<bool> canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const;
}; };
#endif // GAMESTATE_H #endif // GAMESTATE_H