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:
parent
2ec6206e26
commit
5240949353
|
@ -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;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
delete clonedState;
|
delete clonedState;
|
||||||
|
|
||||||
|
if (res.value_or(false))
|
||||||
|
return true;
|
||||||
|
if (!res.has_value())
|
||||||
|
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;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
delete clonedState;
|
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())) {
|
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;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
delete clonedState;
|
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
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue