535 lines
16 KiB
C++
535 lines
16 KiB
C++
#include "gamestate.h"
|
|
#include <random>
|
|
#include <QDebug>
|
|
|
|
GameState::GameState(QObject *parent)
|
|
: QObject{parent}
|
|
{
|
|
m_foundation.resize(4);
|
|
m_columns.resize(7);
|
|
|
|
dealCards();
|
|
}
|
|
|
|
void GameState::dealCards()
|
|
{
|
|
// BUG: This causes a memory leak when called again
|
|
qDebug() << "Dealing cards";
|
|
|
|
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);
|
|
|
|
// Reset the foundation & throwaway pile
|
|
m_throwawayPile.clear();
|
|
for (auto &column : m_foundation)
|
|
column.clear();
|
|
|
|
// Note that we don't need to reset gameWon from here, as it's
|
|
// auto-checked from onFoundationChanged, which the emits trigger
|
|
|
|
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";
|
|
|
|
// 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();
|
|
}
|
|
|
|
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;
|
|
}
|