Add winnability check

This commit is contained in:
ItsDrike 2024-12-06 04:55:48 +01:00
parent eec98ba110
commit 135d16daae
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
6 changed files with 223 additions and 8 deletions

View file

@ -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")
}
}
} }
} }

View file

@ -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")
}
}
}
} }
} }
} }

View file

@ -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")
}
}
}
} }
} }
} }

View file

@ -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,8 +9,11 @@ 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() {
@ -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);
}

View file

@ -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

View file

@ -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();