#include "gamestate.h" #include #include GameState::GameState(QObject *parent) : QObject{parent} { assert(connect(this, SIGNAL(foundationChanged()), this, SLOT(onFoundationChanged()))); m_foundation.resize(4); m_columns.resize(7); dealCards(); } void GameState::dealCards() { qDebug() << "Dealing cards"; cleanupBoard(false); 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++) { QList column; // Deal exactly i+1 cards to the i-th column for (int j = 0; j <= i; j++) { bool revealed = (j == i); ColumnSlot *col = new ColumnSlot(deck[index], revealed); column.append(col); index++; } m_columns[i] = column; } // Use the remaining cards as the draw pile assert(index == 28); m_drawPile = deck.mid(index); emit drawPileChanged(); emit throwawayPileChanged(); emit columnsChanged(); emit foundationChanged(); } void GameState::setupWinningDeck() { // BUG: This causes a memory leak when called again qDebug() << "Setting up a winning deck"; cleanupBoard(false); // Create a sorted deck of cards (4 suits, ordered) QList deck = PlayingCard::createDeck(); // Setup the foundation with all cards except one per suit for (int suit = 0; suit < 4; ++suit) { QList foundationPile; for (int rank = 1; rank <= 12; ++rank) { // Leave the King (rank 13) out foundationPile.prepend(deck[suit * 13 + rank - 1]); } m_foundation[suit] = foundationPile; } // The remaining four Kings are placed in the columns for (int i = 0; i < 4; ++i) { QList column; PlayingCard* kingCard = deck[i * 13 + 12]; // King of each suit ColumnSlot* slot = new ColumnSlot(kingCard, true); // Revealed column.append(slot); m_columns[i] = column; } // Ensure other columns are empty for (int i = 4; i < 7; ++i) { m_columns[i].clear(); } // The draw pile and throwaway pile are empty m_drawPile.clear(); m_throwawayPile.clear(); emit drawPileChanged(); emit throwawayPileChanged(); emit columnsChanged(); emit foundationChanged(); } void GameState::drawNextCard() { qDebug() << "Drawing next card."; // If drawPile is empty, flip the throwawayPile to drawPile if (m_drawPile.isEmpty()) { if (m_throwawayPile.isEmpty()) { qWarning() << "Drawing a card failed, no more cards to draw from"; return; } m_drawPile = m_throwawayPile; m_throwawayPile.clear(); std::reverse(m_drawPile.begin(), m_drawPile.end()); qDebug() << "> Draw pile empty, flipping throwaway pile"; } // Draw the top card from drawPile, dropping it into throwawayPile m_throwawayPile.append(m_drawPile.takeFirst()); qDebug() << "> Drawn card: " << m_throwawayPile.last()->toString(); emit drawPileChanged(); emit throwawayPileChanged(); } bool GameState::moveThrownCardToColumn(int columnId) { assert(columnId >= 0 && columnId < 7); auto& columnStack = m_columns[columnId]; if (m_throwawayPile.isEmpty()) { qWarning() << "Attempted to move thrown card to column with empty throwaway pile"; return false; } // We'll be moving the last card in the throwaway pile (maybe) PlayingCard* cardToMove = m_throwawayPile.last(); qDebug() << "Attempting to move thrown card " << cardToMove->toString() << " to column " << columnId; if (!isColumnMoveValid(*cardToMove, columnId)) { qDebug() << "> Moving aborted, illegal move"; return false; } // Success, perform the move ColumnSlot* col = new ColumnSlot(cardToMove, true); columnStack.append(col); m_throwawayPile.removeLast(); qDebug() << "> Moving complete"; emit throwawayPileChanged(); emit columnsChanged(); return true; } bool GameState::moveThrownCardToFoundation(int foundationId) { assert(foundationId >= 0 && foundationId < 4); auto& foundationStack = m_foundation[foundationId]; if (m_throwawayPile.isEmpty()) { qWarning() << "Attempted to move thrown card to foundation with empty throwaway pile"; return false; } // We'll be moving the last card in the foundation pile (maybe) PlayingCard* cardToMove = m_throwawayPile.last(); qDebug() << "Attempting to move thrown card " << cardToMove->toString() << " to foundation " << foundationId; // Try moving the card into the foundation if (!isFoundationMoveValid(*cardToMove, foundationId)) { qDebug() << "> Moving aborted, illegal move"; return false; } // Succeess, perform the move foundationStack.prepend(cardToMove); m_throwawayPile.removeLast(); qDebug() << "> Moving complete"; emit throwawayPileChanged(); emit foundationChanged(); return true; } bool GameState::moveColumnCardToColumn(int fromColumnId, int toColumnId, int fromCardIndex) { assert(fromColumnId >= 0 && fromColumnId < 7); assert(toColumnId >= 0 && toColumnId < 7); auto fromColumnStack = m_columns[fromColumnId]; auto toColumnStack = m_columns[toColumnId]; if (fromColumnStack.isEmpty()) { qWarning() << "Attempted to move card(s) to column from an empty column"; return false; } ColumnSlot* col = fromColumnStack[fromCardIndex]; if (!col->isRevealed()) { qWarning() << "Attempted to card(s) to column from unrevealed column slot"; return false; } PlayingCard* cardToMove = col->card(); qDebug() << "Attempting to move card " << cardToMove->toString() << " from column " << fromColumnId << " to column " << toColumnId; // Try moving the card if (!isColumnMoveValid(*cardToMove, toColumnId)) { qDebug() << "> Moving aborted, illegal move"; return false; } // Success, move the card toColumnStack.append(col); fromColumnStack.removeLast(); ensureColumnRevealed(fromColumnId); qDebug() << "> Moving complete"; emit columnsChanged(); return true; } bool GameState::moveColumnCardToFoundation(int columnId, int foundationId) { assert(columnId >= 0 && columnId < 7); assert(foundationId >= 0 && foundationId < 4); auto& columnStack = m_columns[columnId]; auto& foundationStack = m_foundation[foundationId]; if (m_columns[columnId].isEmpty()) { qWarning() << "Attempted to move card to foundation from an empty column"; return false; } // We'll be moving the last card in the column (maybe) ColumnSlot* col = columnStack.last(); PlayingCard* cardToMove = col->card(); qDebug() << "Attempting to move card " << cardToMove->toString() << " from column " << columnId << " to foundation " << foundationId; // Try moving the card into the foundation if (!isFoundationMoveValid(*cardToMove, foundationId)) { qDebug() << "> Moving aborted, illegal move"; return false; } // Success, move the card foundationStack.prepend(cardToMove); columnStack.removeLast(); col->deleteLater(); ensureColumnRevealed(columnId); qDebug() << "> Moving complete"; emit columnsChanged(); // CRASH (not if I remove the delete col line though) emit foundationChanged(); return true; } bool GameState::autoMoveThrownCard() { if (m_throwawayPile.isEmpty()) { qWarning() << "Attempted to move thrown card to foundation with empty throwaway pile"; return false; } // We'll be moving the last card in the foundation pile (maybe) PlayingCard* cardToMove = m_throwawayPile.last(); qDebug() << "Attempting auto-move of thrown card " << cardToMove->toString(); // Try moving the card into the foundation if (!tryAutoMoveSingleCard(*cardToMove)) { qDebug() << "> Moving failed, no available move found"; return false; } // We succeeded, the card was moved, remove it from throwaway pile m_throwawayPile.removeLast(); qDebug() << "> Moving complete"; emit throwawayPileChanged(); // We don't know which pile the card was moved to, to be safe, emit // a change from both // NOTE: consider returning what changed from tryAutoMoveSingleCard emit columnsChanged(); emit foundationChanged(); return true; } bool GameState::autoMoveColumnCard(int columnId, int cardIndex) { assert(columnId >= 0 && columnId < 7); auto& columnStack = m_columns[columnId]; if (columnStack.isEmpty()) { qWarning() << "Attempted to move card(s) to foundation from an empty column"; return false; } ColumnSlot* col = columnStack[cardIndex]; if (!col->isRevealed()) { qWarning() << "Attempted to card(s) to column from unrevealed column slot"; return false; } if (cardIndex == columnStack.size() - 1) { // This is a single card move (last card) PlayingCard* cardToMove = col->card(); qDebug() << "Attempting auto-move of column " << columnId << " card " << cardToMove->toString(); if (!tryAutoMoveSingleCard(*cardToMove, columnId)) { qDebug() << "> Moving failed, no available move found"; return false; } // We succeeded, the card was moved, remove it from the original column columnStack.removeLast(); col->deleteLater(); ensureColumnRevealed(columnId); qDebug() << "> Moving complete"; emit columnsChanged(); // we don't know where the card was moved, it could've been the foundation too // to be safe, emit a change signal for it too // NOTE: consider returning what changed from tryAutoMoveSingleCard emit foundationChanged(); return true; } // This is a multiple cards move qDebug() << "Attempting auto-move of column " << columnId << " card range " << cardIndex << " to " << columnStack.size() - 1; QList selectedCards; for (int i = cardIndex; i < columnStack.size(); ++i) { ColumnSlot* col = columnStack[i]; selectedCards.append(col->card()); } if (!tryAutoMoveMultipleCards(selectedCards, cardIndex)) { qDebug() << "> Moving failed, no available move found"; return false; } // We succeeded, the cards were moved, // now remove the moved cards from the column while (columnStack.size() > cardIndex) { ColumnSlot* curSlot = columnStack.takeLast(); curSlot->deleteLater(); } ensureColumnRevealed(columnId); emit columnsChanged(); return true; } void GameState::onFoundationChanged() { // Check if the game is won (can only happen on a foundation pile change) bool gameWon = true; for (const QList &foundationPile : std::as_const(m_foundation)) { // The piles need to contain all 13 card values each, otherwise the game isn't won if (foundationPile.size() != 13) { gameWon = false; break; } } if (gameWon == m_gameWon) return; if (gameWon) qDebug() << "The game was won!"; m_gameWon = gameWon; emit gameWonChanged(); } void GameState::cleanupBoard(bool emitChanges) { // Clean up all PlayingCard objects in the draw pile for (auto &card : m_drawPile) { card->deleteLater(); } m_drawPile.clear(); // Clean up all PlayingCard objects in the throwaway pile for (auto &card : m_throwawayPile) { card->deleteLater(); } m_throwawayPile.clear(); // Clean up all PlayingCard objects in the foundation piles for (auto &foundationPile : m_foundation) { for (auto card : foundationPile) card->deleteLater(); foundationPile.clear(); } // Clean up all ColumnSlot objects in the columns // alongside with the associated PlayingCard objects // that they hold for (auto &column : m_columns) { for (auto slot : column) { slot->card()->deleteLater(); slot->deleteLater(); } column.clear(); } // Note that we don't need to reset gameWon from here, as it's // auto-checked from onFoundationChanged, which the emits trigger if (emitChanges) { emit drawPileChanged(); emit throwawayPileChanged(); emit foundationChanged(); emit columnsChanged(); } } bool GameState::tryAutoMoveSingleCard(PlayingCard &cardToMove, int skipColumnId) { // 1. Try moving the card to the foundation const int foundationId = static_cast(cardToMove.suit()); if (isFoundationMoveValid(cardToMove, foundationId)) { m_foundation[foundationId].prepend(&cardToMove); qDebug() << "* Auto-moved card " << cardToMove.toString() << " to foundation " << foundationId; return true; } // 2. Try moving the card to another column for (int columnId = 0; columnId < m_columns.size(); ++columnId) { if (columnId == skipColumnId) continue; if (isColumnMoveValid(cardToMove, columnId)) { ColumnSlot* col = new ColumnSlot(&cardToMove, true); m_columns[columnId].append(col); qDebug() << "* Auto-moved card " << cardToMove.toString() << " to column " << columnId; return true; } } // No available auto-move qDebug() << "* Auto-move failed, no available moves"; return false; } bool GameState::tryAutoMoveMultipleCards(const QList& cards, int skipColumnId) { assert(cards.size() > 1); // If we can move the first (selected) card to another column, // we can also move the rest of the cards below to that column, // so we only need to care about the first card. // (Foundation moves are impossible with multiple card movements). PlayingCard* firstCard = cards.first(); for (int columnId = 0; columnId < m_columns.size(); ++columnId) { if (columnId == skipColumnId) continue; if (isColumnMoveValid(*firstCard, columnId)) { for (auto card : cards) { ColumnSlot* col = new ColumnSlot(card, true); m_columns[columnId].append(col); qDebug() << "* Auto-moved card " << card->toString() << " to column " << columnId; } return true; } } qDebug() << "* Auto-move failed, no available moves"; return false; } bool GameState::isFoundationMoveValid(const PlayingCard& cardToMove, int foundationId) { assert(foundationId >= 0 && foundationId < 4); const auto foundationSuit = static_cast(foundationId); const auto& foundationStack = m_foundation[foundationId]; // The card must match the suit of the foundation if (cardToMove.suit() != foundationSuit) { return false; } PlayingCard::Value requiredValue; if (foundationStack.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 = foundationStack.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); } return cardToMove.value() == requiredValue; } bool GameState::isColumnMoveValid(const PlayingCard& cardToMove, int columnId) { assert(columnId >= 0 && columnId < 7); const auto& columnStack = m_columns[columnId]; if (columnStack.isEmpty()) { // Column is empty: only a King can be placed in an empty column return cardToMove.value() == PlayingCard::Value::King; } // Compare against the last card in the column const PlayingCard& columnCard = *columnStack.last()->card(); // The card's value must be one less than the card in the column if (cardToMove.value() != columnCard.value() - 1) { qDebug() << "* Move attempt failed (wrong value)"; return false; } // The card must be of opposite color return PlayingCard::areOppositeColors(cardToMove, columnCard); } void GameState::ensureColumnRevealed(int columnId) { assert(columnId >= 0 && columnId < 7); auto& columnStack = m_columns[columnId]; // Nothing to reveal if (m_columns[columnId].isEmpty()) return; // Get the last column slot ColumnSlot *col = columnStack.last(); // If it's already revealed, there's nothing to do if (col->isRevealed()) return; // First slot in the column must always be revealed, reveal it col->reveal(); qDebug() << "Revealed card " << col->card()->toString() << " in column " << columnId; } 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; } bool GameState::gameWon() const { return m_gameWon; }