From fdc340536688dfef71870929dff824e4bd7ee367 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 7 Dec 2024 12:03:49 +0100 Subject: [PATCH] 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. --- src/gamestate.cpp | 25 +++++++++++++++---------- src/gamestate.h | 8 +++++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/gamestate.cpp b/src/gamestate.cpp index b2a5a2e..5912a85 100644 --- a/src/gamestate.cpp +++ b/src/gamestate.cpp @@ -1,7 +1,6 @@ #include "gamestate.h" #include #include -#include #include #include @@ -617,13 +616,19 @@ void GameState::ensureColumnRevealed(int columnId) { qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; } -std::optional GameState::canWinThroughSimulation(QSet& visitedStates, int depth) const { +std::optional GameState::canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer, int depth) const { // Check if the game is already won if (m_gameWon) return true; - // Limit evaluation to a max depth of 20 moves - if (depth > MAX_EVAL_DEPTH) + // Limit evaluation time (ensures this doesn't block the game) + 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; // Generate the current state hash @@ -651,7 +656,7 @@ std::optional GameState::canWinThroughSimulation(QSet& visitedSta assert(clonedState->moveColumnCardToFoundation(columnId, foundationId)); assert(clonedState->generateStateHash() != generateStateHash()); - auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); delete clonedState; if (res.value_or(false)) @@ -674,7 +679,7 @@ std::optional GameState::canWinThroughSimulation(QSet& visitedSta assert(clonedState->moveThrownCardToFoundation(foundationId)); assert(clonedState->generateStateHash() != generateStateHash()); - auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); delete clonedState; if (res.value_or(false)) @@ -692,7 +697,7 @@ std::optional GameState::canWinThroughSimulation(QSet& visitedSta assert(clonedState->moveThrownCardToColumn(toColumnId)); assert(clonedState->generateStateHash() != generateStateHash()); - auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); delete clonedState; if (res.value_or(false)) @@ -710,7 +715,7 @@ std::optional GameState::canWinThroughSimulation(QSet& visitedSta assert(clonedState->drawNextCard()); assert(clonedState->generateStateHash() != generateStateHash()); - auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); delete clonedState; if (res.value_or(false)) @@ -741,7 +746,7 @@ std::optional GameState::canWinThroughSimulation(QSet& visitedSta assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex)); assert(clonedState->generateStateHash() != generateStateHash()); - auto res = clonedState->canWinThroughSimulation(visitedStates, depth + 1); + auto res = clonedState->canWinThroughSimulation(visitedStates, timer, depth + 1); delete clonedState; if (res.value_or(false)) @@ -804,7 +809,7 @@ std::optional GameState::isWinnable() const { timer.start(); QSet visitedStates; - std::optional res = canWinThroughSimulation(visitedStates, 0); + std::optional res = canWinThroughSimulation(visitedStates, timer, 0); qint64 elapsedTime = timer.elapsed(); // Restore the original message handler diff --git a/src/gamestate.h b/src/gamestate.h index 36a14a5..b7daf1c 100644 --- a/src/gamestate.h +++ b/src/gamestate.h @@ -6,11 +6,13 @@ #include #include #include +#include #include #include -// Evaluation depth for checking win-ability (this may impact performance) -#define MAX_EVAL_DEPTH 80 +// Limits for checking winnability +#define MAX_EVAL_TIME 100 // Evaluation time limit (ms) +#define MAX_DEPTH 100 // Max moves into the future limit class GameState : public QObject { Q_OBJECT @@ -78,7 +80,7 @@ class GameState : public QObject { void ensureColumnRevealed(int columnId); - std::optional canWinThroughSimulation(QSet& visitedStates, int depth) const; + std::optional canWinThroughSimulation(QSet& visitedStates, QElapsedTimer timer, int depth) const; }; #endif // GAMESTATE_H