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 <QDebug>
#include <QSet>
#include <qelapsedtimer.h>
#include <qqmllist.h>
#include <random>
@ -620,62 +621,47 @@ void GameState::ensureColumnRevealed(int 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
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)) {
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1);
delete clonedState;
if (res.value_or(false))
return true;
}
delete clonedState;
if (!res.has_value())
return std::nullopt;
}
}
@ -683,32 +669,40 @@ bool GameState::canWinThroughSimulation(QSet<QString>& 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)) {
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<QString>& visitedStates) const {
if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) {
GameState* clonedState = this->clone();
assert(clonedState->drawNextCard());
assert(clonedState->generateStateHash() != generateStateHash());
if (clonedState->canWinThroughSimulation(visitedStates)) {
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<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;
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 <QObject>
#include <QSet>
#include <optional>
#include <qqmlintegration.h>
#include <qqmllist.h>
// 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<bool> 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<QString>& visitedStates) const;
std::optional<bool> canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const;
};
#endif // GAMESTATE_H