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
|
||||
isFaceDown: GameState.drawPile.length > 0 ? true : false
|
||||
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
|
||||
isFaceDown: col ? !col.revealed : false
|
||||
onClicked: {
|
||||
if (col && col.revealed)
|
||||
GameState.autoMoveColumnCard(parent.index, index);
|
||||
if (col && col.revealed) {
|
||||
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: {
|
||||
// Only auto-move the last card in the throwaway pile
|
||||
// cards below it are shown, but shouldn't have a click effect
|
||||
if (reversedIndex == 0)
|
||||
GameState.autoMoveThrownCard();
|
||||
if (reversedIndex == 0) {
|
||||
if (GameState.autoMoveThrownCard()) {
|
||||
if (GameState.isWinnable()) {
|
||||
console.log("Still winnable")
|
||||
} else {
|
||||
console.log("Game is lost")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "gamestate.h"
|
||||
#include <QDebug>
|
||||
#include <QSet>
|
||||
#include <qqmllist.h>
|
||||
#include <random>
|
||||
|
||||
|
@ -8,9 +9,12 @@ GameState::GameState(QObject* parent, bool preDealCards) : QObject{parent} {
|
|||
|
||||
m_foundation.resize(4);
|
||||
m_columns.resize(7);
|
||||
m_gameWon = false;
|
||||
|
||||
if (preDealCards) {
|
||||
dealCards();
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::dealCards() {
|
||||
qDebug() << "Dealing cards";
|
||||
|
@ -359,6 +363,43 @@ void GameState::onFoundationChanged() {
|
|||
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) {
|
||||
// Clean up all PlayingCard objects in the draw pile
|
||||
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) {
|
||||
// 1. Try moving the card to the foundation
|
||||
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;
|
||||
}
|
||||
|
||||
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 lst;
|
||||
for (auto& card : m_drawPile) {
|
||||
|
@ -562,3 +745,9 @@ QVariantList GameState::foundation() const {
|
|||
bool GameState::gameWon() const {
|
||||
return m_gameWon;
|
||||
}
|
||||
|
||||
bool GameState::isWinnable() const
|
||||
{
|
||||
QSet<QString> visitedStates;
|
||||
return !canWinThroughSimulation(visitedStates);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "columnslot.h"
|
||||
#include "playingcard.h"
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
|
||||
|
@ -18,7 +19,7 @@ class GameState : public QObject {
|
|||
Q_PROPERTY(bool gameWon READ gameWon NOTIFY gameWonChanged)
|
||||
|
||||
public:
|
||||
explicit GameState(QObject* parent = nullptr);
|
||||
explicit GameState(QObject* parent = nullptr, bool preDealCards = true);
|
||||
|
||||
// Getters
|
||||
QVariantList drawPile() const;
|
||||
|
@ -31,6 +32,7 @@ class GameState : public QObject {
|
|||
Q_INVOKABLE void dealCards();
|
||||
Q_INVOKABLE void setupWinningDeck();
|
||||
Q_INVOKABLE bool drawNextCard();
|
||||
Q_INVOKABLE bool isWinnable() const; // TODO: Implement as Q_PROPERTY instead
|
||||
|
||||
// Manual moves (from X to Y)
|
||||
Q_INVOKABLE bool moveThrownCardToColumn(int columnId);
|
||||
|
@ -59,7 +61,9 @@ class GameState : public QObject {
|
|||
QList<QList<PlayingCard*>> m_foundation;
|
||||
bool m_gameWon;
|
||||
|
||||
GameState* clone() const;
|
||||
void cleanupBoard(bool emitChanges);
|
||||
QString generateStateHash() const;
|
||||
|
||||
bool tryAutoMoveSingleCard(PlayingCard& cardToMove, int skipColumnId = -1);
|
||||
bool tryAutoMoveMultipleCards(const QList<PlayingCard*>& cards, int skipColumnId);
|
||||
|
@ -68,6 +72,8 @@ class GameState : public QObject {
|
|||
bool isColumnMoveValid(const PlayingCard& cardToMove, int columnId) const;
|
||||
|
||||
void ensureColumnRevealed(int columnId);
|
||||
|
||||
bool canWinThroughSimulation(QSet<QString>& visitedStates) const;
|
||||
};
|
||||
|
||||
#endif // GAMESTATE_H
|
||||
|
|
|
@ -10,7 +10,7 @@ int main(int argc, char* argv[]) {
|
|||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
|
||||
|
||||
auto gameState = engine.singletonInstance<GameState*>("Solitare", "GameState");
|
||||
gameState->setupWinningDeck();
|
||||
qDebug() << "Game winnable:" << gameState->isWinnable();
|
||||
|
||||
engine.load(QStringLiteral("qrc:/qml/Main.qml"));
|
||||
return app.exec();
|
||||
|
|
Loading…
Reference in a new issue