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 <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)) {
|
||||
delete clonedState;
|
||||
return true;
|
||||
}
|
||||
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1);
|
||||
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()) {
|
||||
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)) {
|
||||
delete clonedState;
|
||||
return true;
|
||||
}
|
||||
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)) {
|
||||
delete clonedState;
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue