solitare/gamestate.cpp
ItsDrike 72c4e64782
Add proper board cleanup logic
There totally weren't any memory leaks before
2024-12-04 20:34:03 +01:00

573 lines
17 KiB
C++

#include "gamestate.h"
#include <random>
#include <QDebug>
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<PlayingCard*> 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<ColumnSlot*> 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<PlayingCard*> deck = PlayingCard::createDeck();
// Setup the foundation with all cards except one per suit
for (int suit = 0; suit < 4; ++suit) {
QList<PlayingCard*> 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<ColumnSlot*> 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<PlayingCard*> 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<PlayingCard*> &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<int>(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<PlayingCard*>& 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<PlayingCard::Suit>(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<PlayingCard::Value>(static_cast<int>(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<PlayingCard *> GameState::drawPile() const
{
return m_drawPile;
}
QList<PlayingCard *> GameState::throwawayPile() const
{
return m_throwawayPile;
}
QList<QList<ColumnSlot *> > GameState::columns() const
{
return m_columns;
}
QList<QList<PlayingCard *> > GameState::foundation() const
{
return m_foundation;
}
bool GameState::gameWon() const
{
return m_gameWon;
}