diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ebfa76..17d830d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ qt_add_qml_module(appSolitare QML_FILES ScoreBar.qml QML_FILES CardModel.qml SOURCES playingcard.h playingcard.cpp + SOURCES gamestate.h gamestate.cpp ) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. diff --git a/Main.qml b/Main.qml index 0408694..4f8b0ba 100644 --- a/Main.qml +++ b/Main.qml @@ -20,7 +20,7 @@ ApplicationWindow { CardModel { anchors.centerIn: parent - card: myCard + card: gameState.drawPile[0]; isFaceDown: false } } diff --git a/gamestate.cpp b/gamestate.cpp new file mode 100644 index 0000000..a03b501 --- /dev/null +++ b/gamestate.cpp @@ -0,0 +1,204 @@ +#include "gamestate.h" +#include + +GameState::GameState(QObject *parent) + : QObject{parent} +{ + // Initialize the foundation piles (4 suits) + for (int i = 0; i < 4; ++i) { + m_foundation.append(QList()); + } + + // Initialize the columns (7 piles) + for (int i = 0; i < 7; ++i) { + m_columns.append(QList()); + } +} + +void GameState::dealCards() +{ + QList deck = PlayingCard::createDeck(); + + // Randomly shuffle the deck + std::random_device rd; + std::default_random_engine rng(rd()); + std::shuffle(deck.begin(), deck.end(), rng); + + // Deal the cards into the columns + int index = 0; + for (int i = 0; i < 7; i++) { + // Deal exactly i+1 cards to the i-th column + m_columns[i] = deck.mid(index, i + 1); + index += i + 1; + } + + // Use the remaining cards as the draw pile + m_drawPile = deck.mid(index); + + // Reset the foundation & throwaway pile + m_foundation.clear(); + m_throwawayPile.clear(); + + emit onDrawPileChanged(); + emit onThrowawayPileChanged(); + emit onColumnsChanged(); + emit onFoundationChanged(); +} + +void GameState::drawNextCard() +{ + // If drawPile is empty, flip the throwawayPile to drawPile + if (m_drawPile.isEmpty()) { + m_drawPile = m_throwawayPile; + m_throwawayPile.clear(); + std::reverse(m_drawPile.begin(), m_drawPile.end()); + } + + // Draw the top card from drawPile, dropping it into throwawayPile + m_throwawayPile.append(m_drawPile.takeFirst()); + + emit onDrawPileChanged(); + emit onThrowawayPileChanged(); +} + +bool GameState::moveCardToColumn(int columnId) +{ + assert(columnId >= 0 && columnId < 7); + + if (m_throwawayPile.isEmpty()) { + // Consider raising an exception here instead + return false; + } + + // We'll be moving the last card in the throwaway pile (maybe) + PlayingCard *cardToMove = m_throwawayPile.last(); + + if (m_columns[columnId].isEmpty()) { + // If the column is empty, we can only place a king + if (cardToMove->value() != PlayingCard::Value::King) + return false; + + m_columns[columnId].append(cardToMove); + m_throwawayPile.removeLast(); + + emit onThrowawayPileChanged(); + emit onColumnsChanged(); + return true; + } + + // We'll be comparing this card against the last card in the column + PlayingCard* columnCard = m_columns[columnId].last(); + + // This card's value must be one less than the card in the column + if (cardToMove->value() != columnCard->value() - 1) + return false; + + // This card must be of opposite color + if (!PlayingCard::areOppositeColors(*cardToMove, *columnCard)) + return false; + + m_columns[columnId].append(cardToMove); + m_throwawayPile.removeLast(); + + emit onThrowawayPileChanged(); + emit onColumnsChanged(); + return true; +} + +bool GameState::moveThrownCardToFoundation(PlayingCard::Suit foundationId) +{ + if (m_throwawayPile.isEmpty()) { + // Consider raising an exception here instead + return false; + } + + // We'll be moving the last card in the foundation pile (maybe) + PlayingCard *cardToMove = m_throwawayPile.last(); + + // Try moving the card into the foundation + if (!tryMoveCardToFoundation(foundationId, cardToMove)) + return false; + + // We succeeded, the card is now in the appropriate foundation pile, + // let's remove the card from the throwaway pile. + m_throwawayPile.removeLast(); + + emit onThrowawayPileChanged(); + return true; +} + +bool GameState::moveColumnCardToFoundation(int columnId, PlayingCard::Suit foundationId) +{ + assert(columnId >= 0 && columnId < 7); + + if (m_columns[columnId].isEmpty()) { + // Consider raising an exception here instead + return false; + } + + // We'll be moving the top card in the column (maybe) + PlayingCard *cardToMove = m_columns[columnId].first(); + + // Try moving the card into the foundation + if (!tryMoveCardToFoundation(foundationId, cardToMove)) + return false; + + // We succeeded, the card is now in the appropriate foundation pile, + // let's remove the card from the column. + m_columns[columnId].removeFirst(); + + emit onColumnsChanged(); + return true; + +} + +bool GameState::tryMoveCardToFoundation(PlayingCard::Suit foundationId, PlayingCard* cardToMove) +{ + assert(foundationId >= PlayingCard::Suit::Clubs && foundationId < PlayingCard::Suit::Spades); + + if (cardToMove->suit() != foundationId) + return false; + + PlayingCard::Value requiredValue; + if (m_foundation[foundationId].isEmpty()) { + // If the pile is empty, only an ace can go in + requiredValue = PlayingCard::Value::Ace; + } else { + // Otherwise it's the next card by value, unless we're already at king + PlayingCard::Value curValue = m_foundation[foundationId].first()->value(); + if (curValue == PlayingCard::Value::King) + return false; + + // Clever trick to get the next value. Note that this relies on the enum having + // the variants defined in correct order. + requiredValue = static_cast(static_cast(curValue) + 1); + } + + if (cardToMove->value() != requiredValue) + return false; + + m_foundation[foundationId].push_front(cardToMove); + + emit onFoundationChanged(); + return true; +} + +QList GameState::drawPile() const +{ + return m_drawPile; +} + +QList GameState::throwawayPile() const +{ + return m_throwawayPile; +} + +QList > GameState::columns() const +{ + return m_columns; +} + +QList > GameState::foundation() const +{ + return m_foundation; +} diff --git a/gamestate.h b/gamestate.h new file mode 100644 index 0000000..44693d0 --- /dev/null +++ b/gamestate.h @@ -0,0 +1,44 @@ +#ifndef GAMESTATE_H +#define GAMESTATE_H + +#include +#include "playingcard.h" + +class GameState : public QObject +{ + Q_OBJECT + Q_PROPERTY(QList drawPile READ drawPile NOTIFY onDrawPileChanged) + Q_PROPERTY(QList throwawayPile READ throwawayPile NOTIFY onThrowawayPileChanged) + Q_PROPERTY(QList> columns READ columns NOTIFY onColumnsChanged) + Q_PROPERTY(QList> foundation READ foundation NOTIFY onFoundationChanged) + +public: + explicit GameState(QObject *parent = nullptr); + + QList drawPile() const; + QList throwawayPile() const; + QList> columns() const; + QList> foundation() const; + + void dealCards(); + void drawNextCard(); + bool moveCardToColumn(int columnId); + bool moveThrownCardToFoundation(PlayingCard::Suit foundationId); + bool moveColumnCardToFoundation(int columnId, PlayingCard::Suit foundationId); + +signals: + void onDrawPileChanged(); + void onThrowawayPileChanged(); + void onColumnsChanged(); + void onFoundationChanged(); + +private: + QList m_drawPile; + QList m_throwawayPile; + QList> m_columns; + QList> m_foundation; + + bool tryMoveCardToFoundation(PlayingCard::Suit foundationId, PlayingCard* cardToMove); +}; + +#endif // GAMESTATE_H diff --git a/main.cpp b/main.cpp index 453353f..891e26e 100644 --- a/main.cpp +++ b/main.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "playingcard.h" +#include "gamestate.h" int main(int argc, char *argv[]) { @@ -17,8 +17,9 @@ int main(int argc, char *argv[]) qmlRegisterUncreatableType("Solitare", 1, 0, "PlayingCard", "PlayingCard cannot be directly created in QML. Use C++ logic to instantiate."); - PlayingCard myCard(PlayingCard::Suit::Hearts, PlayingCard::Value::Seven); - engine.rootContext()->setContextProperty("myCard", &myCard); + GameState gameState; + gameState.dealCards(); + engine.rootContext()->setContextProperty("gameState", &gameState); engine.loadFromModule("Solitare", "Main"); diff --git a/playingcard.cpp b/playingcard.cpp index b69345f..d37c521 100644 --- a/playingcard.cpp +++ b/playingcard.cpp @@ -78,3 +78,11 @@ QList PlayingCard::createDeck() return deck; } + +bool PlayingCard::areOppositeColors(const PlayingCard &card1, const PlayingCard &card2) +{ + bool card1IsRed = (card1.suit() == Suit::Hearts || card1.suit() == Suit::Diamonds); + bool card2IsRed = (card2.suit() == Suit::Hearts || card2.suit() == Suit::Diamonds); + + return card1IsRed != card2IsRed; +} diff --git a/playingcard.h b/playingcard.h index 3ebbda5..6780fdb 100644 --- a/playingcard.h +++ b/playingcard.h @@ -51,6 +51,7 @@ public: void setValue(const Value &value); static QList createDeck(); + static bool areOppositeColors(const PlayingCard &card1, const PlayingCard &card2); signals: void onSuitChanged();