Limit runtime for win scenario calculations

Depth limit alone often does a poor job at ensuring the simulation
doesn't take too long, as the amount of branches may differ depending on
the game and in some cases, the function can take way too long.

This solution introduces another stop condition, based on the runtime
of the evaluation, ensuring we don't block the game for too long.

Note that the original depth limiting, while fairly effective is a hacky
solution, instead, it may be a good idea to change the simulation logic
from DFS to BFS based search.
This commit is contained in:
ItsDrike 2024-12-07 12:03:49 +01:00
parent 1eb72163b5
commit fdc3405366
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
2 changed files with 20 additions and 13 deletions

View file

@ -1,7 +1,6 @@
#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>
@ -617,13 +616,19 @@ void GameState::ensureColumnRevealed(int columnId) {
qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId;
} }
std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const { std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedStates, QElapsedTimer timer, int depth) const {
// Check if the game is already won // Check if the game is already won
if (m_gameWon) if (m_gameWon)
return true; return true;
// Limit evaluation to a max depth of 20 moves // Limit evaluation time (ensures this doesn't block the game)
if (depth > MAX_EVAL_DEPTH) if (timer.hasExpired(MAX_EVAL_TIME))
return std::nullopt;
// Limit depth (ensures we don't spend all eval time looking at a single branch.)
// Note that it might be a good idea to switch to a BFS type search, instead of depth
// limiting DFS.
if (depth > MAX_DEPTH)
return std::nullopt; return std::nullopt;
// Generate the current state hash // Generate the current state hash
@ -651,7 +656,7 @@ std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedSta
assert(clonedState->moveColumnCardToFoundation(columnId, foundationId)); assert(clonedState->moveColumnCardToFoundation(columnId, foundationId));
assert(clonedState->generateStateHash() != generateStateHash()); assert(clonedState->generateStateHash() != generateStateHash());
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false)) if (res.value_or(false))
@ -674,7 +679,7 @@ std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedSta
assert(clonedState->moveThrownCardToFoundation(foundationId)); assert(clonedState->moveThrownCardToFoundation(foundationId));
assert(clonedState->generateStateHash() != generateStateHash()); assert(clonedState->generateStateHash() != generateStateHash());
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false)) if (res.value_or(false))
@ -692,7 +697,7 @@ std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedSta
assert(clonedState->moveThrownCardToColumn(toColumnId)); assert(clonedState->moveThrownCardToColumn(toColumnId));
assert(clonedState->generateStateHash() != generateStateHash()); assert(clonedState->generateStateHash() != generateStateHash());
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false)) if (res.value_or(false))
@ -710,7 +715,7 @@ std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedSta
assert(clonedState->drawNextCard()); assert(clonedState->drawNextCard());
assert(clonedState->generateStateHash() != generateStateHash()); assert(clonedState->generateStateHash() != generateStateHash());
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false)) if (res.value_or(false))
@ -741,7 +746,7 @@ std::optional<bool> GameState::canWinThroughSimulation(QSet<QString>& visitedSta
assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex));
assert(clonedState->generateStateHash() != generateStateHash()); assert(clonedState->generateStateHash() != generateStateHash());
auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1);
delete clonedState; delete clonedState;
if (res.value_or(false)) if (res.value_or(false))
@ -804,7 +809,7 @@ std::optional<bool> GameState::isWinnable() const {
timer.start(); timer.start();
QSet<QString> visitedStates; QSet<QString> visitedStates;
std::optional<bool> res = canWinThroughSimulation(visitedStates, 0); std::optional<bool> res = canWinThroughSimulation(visitedStates, timer, 0);
qint64 elapsedTime = timer.elapsed(); qint64 elapsedTime = timer.elapsed();
// Restore the original message handler // Restore the original message handler

View file

@ -6,11 +6,13 @@
#include <QObject> #include <QObject>
#include <QSet> #include <QSet>
#include <optional> #include <optional>
#include <qelapsedtimer.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qqmllist.h> #include <qqmllist.h>
// Evaluation depth for checking win-ability (this may impact performance) // Limits for checking winnability
#define MAX_EVAL_DEPTH 80 #define MAX_EVAL_TIME 100 // Evaluation time limit (ms)
#define MAX_DEPTH 100 // Max moves into the future limit
class GameState : public QObject { class GameState : public QObject {
Q_OBJECT Q_OBJECT
@ -78,7 +80,7 @@ class GameState : public QObject {
void ensureColumnRevealed(int columnId); void ensureColumnRevealed(int columnId);
std::optional<bool> canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const; std::optional<bool> canWinThroughSimulation(QSet<QString>& visitedStates, QElapsedTimer timer, int depth) const;
}; };
#endif // GAMESTATE_H #endif // GAMESTATE_H