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 <QDebug>
#include <QSet>
#include <qelapsedtimer.h>
#include <qqmllist.h>
#include <random>
@ -617,13 +616,19 @@ void GameState::ensureColumnRevealed(int 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
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<bool> GameState::canWinThroughSimulation(QSet<QString>& 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<bool> GameState::canWinThroughSimulation(QSet<QString>& 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<bool> GameState::canWinThroughSimulation(QSet<QString>& 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<bool> GameState::canWinThroughSimulation(QSet<QString>& 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<bool> GameState::canWinThroughSimulation(QSet<QString>& 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<bool> GameState::isWinnable() const {
timer.start();
QSet<QString> visitedStates;
std::optional<bool> res = canWinThroughSimulation(visitedStates, 0);
std::optional<bool> res = canWinThroughSimulation(visitedStates, timer, 0);
qint64 elapsedTime = timer.elapsed();
// Restore the original message handler

View file

@ -6,11 +6,13 @@
#include <QObject>
#include <QSet>
#include <optional>
#include <qelapsedtimer.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
// 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<bool> canWinThroughSimulation(QSet<QString>& visitedStates, int depth) const;
std::optional<bool> canWinThroughSimulation(QSet<QString>& visitedStates, QElapsedTimer timer, int depth) const;
};
#endif // GAMESTATE_H