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:
parent
1eb72163b5
commit
fdc3405366
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue