Add winnability check
This commit is contained in:
parent
eec98ba110
commit
135d16daae
|
@ -7,6 +7,12 @@ CardModel {
|
||||||
card: GameState.drawPile.length > 0 ? GameState.drawPile[GameState.drawPile.length - 1] : null
|
card: GameState.drawPile.length > 0 ? GameState.drawPile[GameState.drawPile.length - 1] : null
|
||||||
isFaceDown: GameState.drawPile.length > 0 ? true : false
|
isFaceDown: GameState.drawPile.length > 0 ? true : false
|
||||||
onClicked: {
|
onClicked: {
|
||||||
GameState.drawNextCard();
|
if (GameState.drawNextCard()) {
|
||||||
|
if (GameState.isWinnable()) {
|
||||||
|
console.log("Still winnable")
|
||||||
|
} else {
|
||||||
|
console.warn("Game is lost")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,15 @@ Row {
|
||||||
card: col ? col.card : null
|
card: col ? col.card : null
|
||||||
isFaceDown: col ? !col.revealed : false
|
isFaceDown: col ? !col.revealed : false
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (col && col.revealed)
|
if (col && col.revealed) {
|
||||||
GameState.autoMoveColumnCard(parent.index, index);
|
if (GameState.autoMoveColumnCard(parent.index, index)) {
|
||||||
|
if (GameState.isWinnable()) {
|
||||||
|
console.log("Still winnable")
|
||||||
|
} else {
|
||||||
|
console.log("Game is lost")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,15 @@ Row {
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Only auto-move the last card in the throwaway pile
|
// Only auto-move the last card in the throwaway pile
|
||||||
// cards below it are shown, but shouldn't have a click effect
|
// cards below it are shown, but shouldn't have a click effect
|
||||||
if (reversedIndex == 0)
|
if (reversedIndex == 0) {
|
||||||
GameState.autoMoveThrownCard();
|
if (GameState.autoMoveThrownCard()) {
|
||||||
|
if (GameState.isWinnable()) {
|
||||||
|
console.log("Still winnable")
|
||||||
|
} else {
|
||||||
|
console.log("Game is lost")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "gamestate.h"
|
#include "gamestate.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QSet>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
|
@ -8,9 +9,12 @@ GameState::GameState(QObject* parent, bool preDealCards) : QObject{parent} {
|
||||||
|
|
||||||
m_foundation.resize(4);
|
m_foundation.resize(4);
|
||||||
m_columns.resize(7);
|
m_columns.resize(7);
|
||||||
|
m_gameWon = false;
|
||||||
|
|
||||||
|
if (preDealCards) {
|
||||||
dealCards();
|
dealCards();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameState::dealCards() {
|
void GameState::dealCards() {
|
||||||
qDebug() << "Dealing cards";
|
qDebug() << "Dealing cards";
|
||||||
|
@ -359,6 +363,43 @@ void GameState::onFoundationChanged() {
|
||||||
emit gameWonChanged();
|
emit gameWonChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameState* GameState::clone() const {
|
||||||
|
GameState* newGameState = new GameState(nullptr, false);
|
||||||
|
|
||||||
|
// Deep copy the necessary data
|
||||||
|
|
||||||
|
for (auto curCard : m_drawPile) {
|
||||||
|
PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value());
|
||||||
|
newGameState->m_drawPile.append(newCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto curCard : m_throwawayPile) {
|
||||||
|
PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value());
|
||||||
|
newGameState->m_throwawayPile.append(newCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_columns.size(); ++i) {
|
||||||
|
for (auto curCol : m_columns[i]) {
|
||||||
|
auto curCard = curCol->card();
|
||||||
|
PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value());
|
||||||
|
ColumnSlot* newCol = new ColumnSlot(newCard, curCol->isRevealed());
|
||||||
|
newGameState->m_columns[i].append(newCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_foundation.size(); ++i) {
|
||||||
|
for (auto curCard : m_foundation[i]) {
|
||||||
|
PlayingCard* newCard = new PlayingCard(nullptr, curCard->suit(), curCard->value());
|
||||||
|
newGameState->m_foundation[i].append(newCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newGameState->m_gameWon = m_gameWon;
|
||||||
|
|
||||||
|
assert(this->generateStateHash() == newGameState->generateStateHash());
|
||||||
|
return newGameState;
|
||||||
|
}
|
||||||
|
|
||||||
void GameState::cleanupBoard(bool emitChanges) {
|
void GameState::cleanupBoard(bool emitChanges) {
|
||||||
// Clean up all PlayingCard objects in the draw pile
|
// Clean up all PlayingCard objects in the draw pile
|
||||||
for (auto& card : m_drawPile) {
|
for (auto& card : m_drawPile) {
|
||||||
|
@ -401,6 +442,38 @@ void GameState::cleanupBoard(bool emitChanges) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString GameState::generateStateHash() const {
|
||||||
|
QString stateHash;
|
||||||
|
|
||||||
|
// The repetition here is annoying, I know
|
||||||
|
for (const auto& column : m_columns) {
|
||||||
|
for (const ColumnSlot* slot : column) {
|
||||||
|
stateHash += QString::number(slot->card()->value()) + QString::number(slot->card()->suit());
|
||||||
|
stateHash += slot->isRevealed() ? "t" : "f";
|
||||||
|
stateHash += ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& foundationPile : m_foundation) {
|
||||||
|
for (const PlayingCard* card : foundationPile) {
|
||||||
|
stateHash += QString::number(card->value()) + QString::number(card->suit()) + ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const PlayingCard* card : m_throwawayPile) {
|
||||||
|
stateHash += QString::number(card->value()) + QString::number(card->suit()) + ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const PlayingCard* card : m_drawPile) {
|
||||||
|
stateHash += QString::number(card->value()) + QString::number(card->suit()) + ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
stateHash += m_gameWon ? "t" : "f";
|
||||||
|
|
||||||
|
return stateHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool GameState::tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId) {
|
bool GameState::tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId) {
|
||||||
// 1. Try moving the card to the foundation
|
// 1. Try moving the card to the foundation
|
||||||
const int foundationId = static_cast<int>(cardToMove.suit());
|
const int foundationId = static_cast<int>(cardToMove.suit());
|
||||||
|
@ -527,6 +600,116 @@ void GameState::ensureColumnRevealed(int columnId) {
|
||||||
qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId;
|
qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameState::canWinThroughSimulation(QSet<QString>& visitedStates) const {
|
||||||
|
// Check if the game is already won
|
||||||
|
if (m_gameWon) return true;
|
||||||
|
|
||||||
|
// Generate the current state hash
|
||||||
|
QString currentStateHash = generateStateHash();
|
||||||
|
|
||||||
|
// Check if the state has already been explored
|
||||||
|
if (visitedStates.contains(currentStateHash)) return false;
|
||||||
|
|
||||||
|
// Mark the current state as visited
|
||||||
|
visitedStates.insert(currentStateHash);
|
||||||
|
|
||||||
|
// Simulate moves between columns
|
||||||
|
for (int fromColumnId = 0; fromColumnId < m_columns.size(); ++fromColumnId) {
|
||||||
|
const auto& fromColumnStack = m_columns[fromColumnId];
|
||||||
|
if (fromColumnStack.isEmpty()) continue;
|
||||||
|
|
||||||
|
for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) {
|
||||||
|
if (fromColumnId == toColumnId) continue;
|
||||||
|
|
||||||
|
// Try all revealed cards in the column
|
||||||
|
for (int fromCardIndex = 0; fromCardIndex < fromColumnStack.size(); ++fromCardIndex) {
|
||||||
|
const ColumnSlot* fromSlot = fromColumnStack[fromCardIndex];
|
||||||
|
if (!fromSlot->isRevealed()) continue;
|
||||||
|
if (!isColumnMoveValid(*fromSlot->card(), toColumnId)) continue;
|
||||||
|
|
||||||
|
GameState* clonedState = this->clone();
|
||||||
|
assert(clonedState->moveColumnCardToColumn(fromColumnId, toColumnId, fromCardIndex));
|
||||||
|
|
||||||
|
if (clonedState->canWinThroughSimulation(visitedStates)) {
|
||||||
|
delete clonedState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete clonedState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate column moves to the foundation
|
||||||
|
for (int columnId = 0; columnId < m_columns.size(); ++columnId) {
|
||||||
|
const auto& columnStack = m_columns[columnId];
|
||||||
|
if (columnStack.isEmpty()) continue;
|
||||||
|
|
||||||
|
const ColumnSlot* topSlot = columnStack.last();
|
||||||
|
for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) {
|
||||||
|
if (!isFoundationMoveValid(*topSlot->card(), foundationId)) continue;
|
||||||
|
|
||||||
|
GameState* clonedState = this->clone();
|
||||||
|
assert(clonedState->moveColumnCardToFoundation(columnId, foundationId));
|
||||||
|
|
||||||
|
if (clonedState->canWinThroughSimulation(visitedStates)) {
|
||||||
|
delete clonedState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete clonedState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate throwaway pile moves
|
||||||
|
if (!m_throwawayPile.isEmpty()) {
|
||||||
|
const PlayingCard* topCard = m_throwawayPile.last();
|
||||||
|
|
||||||
|
// Move to columns
|
||||||
|
for (int toColumnId = 0; toColumnId < m_columns.size(); ++toColumnId) {
|
||||||
|
if (!isColumnMoveValid(*topCard, toColumnId)) continue;
|
||||||
|
|
||||||
|
GameState* clonedState = this->clone();
|
||||||
|
assert(clonedState->moveThrownCardToColumn(toColumnId));
|
||||||
|
|
||||||
|
if (clonedState->canWinThroughSimulation(visitedStates)) {
|
||||||
|
delete clonedState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete clonedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to foundation
|
||||||
|
for (int foundationId = 0; foundationId < m_foundation.size(); ++foundationId) {
|
||||||
|
if (!isFoundationMoveValid(*topCard, foundationId)) continue;
|
||||||
|
|
||||||
|
GameState* clonedState = this->clone();
|
||||||
|
assert(clonedState->moveThrownCardToFoundation(foundationId));
|
||||||
|
|
||||||
|
if (clonedState->canWinThroughSimulation(visitedStates)) {
|
||||||
|
delete clonedState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete clonedState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate draw pile move
|
||||||
|
if (!(m_drawPile.isEmpty() && m_throwawayPile.isEmpty())) {
|
||||||
|
GameState* clonedState = this->clone();
|
||||||
|
assert(clonedState->drawNextCard());
|
||||||
|
|
||||||
|
if (clonedState->canWinThroughSimulation(visitedStates)) {
|
||||||
|
delete clonedState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete clonedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No paths lead to a win
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
QVariantList GameState::drawPile() const {
|
QVariantList GameState::drawPile() const {
|
||||||
QVariantList lst;
|
QVariantList lst;
|
||||||
for (auto& card : m_drawPile) {
|
for (auto& card : m_drawPile) {
|
||||||
|
@ -562,3 +745,9 @@ QVariantList GameState::foundation() const {
|
||||||
bool GameState::gameWon() const {
|
bool GameState::gameWon() const {
|
||||||
return m_gameWon;
|
return m_gameWon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameState::isWinnable() const
|
||||||
|
{
|
||||||
|
QSet<QString> visitedStates;
|
||||||
|
return !canWinThroughSimulation(visitedStates);
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "columnslot.h"
|
#include "columnslot.h"
|
||||||
#include "playingcard.h"
|
#include "playingcard.h"
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QSet>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ class GameState : public QObject {
|
||||||
Q_PROPERTY(bool gameWon READ gameWon NOTIFY gameWonChanged)
|
Q_PROPERTY(bool gameWon READ gameWon NOTIFY gameWonChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GameState(QObject* parent = nullptr);
|
explicit GameState(QObject* parent = nullptr, bool preDealCards = true);
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
QVariantList drawPile() const;
|
QVariantList drawPile() const;
|
||||||
|
@ -31,6 +32,7 @@ class GameState : public QObject {
|
||||||
Q_INVOKABLE void dealCards();
|
Q_INVOKABLE void dealCards();
|
||||||
Q_INVOKABLE void setupWinningDeck();
|
Q_INVOKABLE void setupWinningDeck();
|
||||||
Q_INVOKABLE bool drawNextCard();
|
Q_INVOKABLE bool drawNextCard();
|
||||||
|
Q_INVOKABLE bool isWinnable() const; // TODO: Implement as Q_PROPERTY instead
|
||||||
|
|
||||||
// Manual moves (from X to Y)
|
// Manual moves (from X to Y)
|
||||||
Q_INVOKABLE bool moveThrownCardToColumn(int columnId);
|
Q_INVOKABLE bool moveThrownCardToColumn(int columnId);
|
||||||
|
@ -59,7 +61,9 @@ class GameState : public QObject {
|
||||||
QList<QList<PlayingCard*>> m_foundation;
|
QList<QList<PlayingCard*>> m_foundation;
|
||||||
bool m_gameWon;
|
bool m_gameWon;
|
||||||
|
|
||||||
|
GameState* clone() const;
|
||||||
void cleanupBoard(bool emitChanges);
|
void cleanupBoard(bool emitChanges);
|
||||||
|
QString generateStateHash() const;
|
||||||
|
|
||||||
bool tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId = -1);
|
bool tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId = -1);
|
||||||
bool tryAutoMoveMultipleCards(const QList<PlayingCard*>& cards, int skipColumnId);
|
bool tryAutoMoveMultipleCards(const QList<PlayingCard*>& cards, int skipColumnId);
|
||||||
|
@ -68,6 +72,8 @@ class GameState : public QObject {
|
||||||
bool isColumnMoveValid(const PlayingCard& cardToMove, int columnId) const;
|
bool isColumnMoveValid(const PlayingCard& cardToMove, int columnId) const;
|
||||||
|
|
||||||
void ensureColumnRevealed(int columnId);
|
void ensureColumnRevealed(int columnId);
|
||||||
|
|
||||||
|
bool canWinThroughSimulation(QSet<QString>& visitedStates) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // GAMESTATE_H
|
#endif // GAMESTATE_H
|
||||||
|
|
|
@ -10,7 +10,7 @@ int main(int argc, char* argv[]) {
|
||||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
|
||||||
|
|
||||||
auto gameState = engine.singletonInstance<GameState*>("Solitare", "GameState");
|
auto gameState = engine.singletonInstance<GameState*>("Solitare", "GameState");
|
||||||
gameState->setupWinningDeck();
|
qDebug() << "Game winnable:" << gameState->isWinnable();
|
||||||
|
|
||||||
engine.load(QStringLiteral("qrc:/qml/Main.qml"));
|
engine.load(QStringLiteral("qrc:/qml/Main.qml"));
|
||||||
return app.exec();
|
return app.exec();
|
||||||
|
|
Loading…
Reference in a new issue