QML doesn't natively support the complex type returned from isWinnable
property (`std::pair<std::optional<bool>, int>`), instead use
QVariantMap to implement custom attributes and return as QVariant.
The original approach here was using depth-limited depth first search,
which was a hacky workaround to avoid exploring a single branch too
deeply. A much saner approach is to just explore in a breadth-first
manner.
This also completely negates the need for depth limitations and we can
instead let the algorithm run until it either finishes or hits the time
limit, as we always want to explore as much depth as we can, without it
slowing down the responsiveness of the UI.
The current implementation of state hash doesn't represent empty columns
or foundations properly. This leads to a potential collision if there is
a full column next to an empty column, as it's indistinguishable which
column the data lies on. (In practice, this can't happen for
foundations, as they only hold cards of distinct types, so the collision
only occurs with columns.)
This commit fixes the issue and makes sure to represent empty piles
properly.
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.
The original implementation didn't use references for the QList
instances, which meant they were getting copied, so the changes made
didn't mutate the actual values held by the class.
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.
QML doesn't have a proper type-safe generic list type, returning QList
instances does technically work, however, qmlls (LSP) complains about
using this as it isn't a proper QML type. Instead, return QVarianList
objects, that are meant for QML.