solitare/gamestate.cpp

380 lines
12 KiB
C++

#include "gamestate.h"
#include <random>
#include <QDebug>
GameState::GameState(QObject *parent)
: QObject{parent}
{
// Initialize the foundation piles (4 suits)
for (int i = 0; i < 4; ++i) {
m_foundation.append(QList<PlayingCard*>());
}
// Initialize the columns (7 piles)
for (int i = 0; i < 7; ++i) {
m_columns.append(QList<ColumnSlot*>());
}
}
void GameState::dealCards()
{
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
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::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::moveCardToColumn(int columnId)
{
assert(columnId >= 0 && columnId < 7);
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 (!isMoveToColumnLegal(cardToMove, columnId)) {
qDebug() << "> Moving aborted, illegal move";
return false;
}
ColumnSlot *col = new ColumnSlot(cardToMove, true);
m_columns[columnId].append(col);
m_throwawayPile.removeLast();
ensureColumnRevealed(columnId);
qDebug() << "> Moving complete";
emit throwawayPileChanged();
emit columnsChanged();
return true;
}
bool GameState::moveThrownCardToFoundation(PlayingCard::Suit 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 (!tryMoveCardToFoundation(foundationId, cardToMove)) {
qDebug() << "> Moving aborted, illegal move";
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();
qDebug() << "> Moving complete";
emit throwawayPileChanged();
return true;
}
bool GameState::moveColumnCardToFoundation(int columnId, PlayingCard::Suit foundationId)
{
assert(columnId >= 0 && columnId < 7);
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)
PlayingCard *cardToMove = m_columns[columnId].last()->card();
qDebug() << "Attempting to move card " << cardToMove->toString() << " from column " << columnId << " to foundation " << foundationId;
// Try moving the card into the foundation
if (!tryMoveCardToFoundation(foundationId, cardToMove)) {
qDebug() << "> Moving aborted, illegal move";
return false;
}
// We succeeded, the card is now in the appropriate foundation pile,
// let's remove the column slot.
m_columns[columnId].removeLast();
ensureColumnRevealed(columnId);
qDebug() << "> Moving complete";
emit columnsChanged();
return true;
}
bool GameState::autoMoveThrownCard()
{
// NOTE: This method is very similar to moveThrownCardToFoundation,
// consider reducing the repetitino here somehow.
if (m_throwawayPile.isEmpty()) {
// Consider raising an exception here instead
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 (!tryAutoMoveCard(cardToMove)) {
qDebug() << "> Moving failed, no available move found";
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();
qDebug() << "> Moving complete";
emit throwawayPileChanged();
return true;
}
bool GameState::autoMoveColumnCard(int columnId)
{
// NOTE: This method is very similar to moveColumnCardToFoundation,
// consider reducing the repetitino here somehow.
assert(columnId >= 0 && columnId < 7);
if (m_columns[columnId].isEmpty()) {
// Consider raising an exception here instead
qWarning() << "Attempted to move card to foundation from an empty column";
return false;
}
// We'll be moving the last card in the column (maybe)
PlayingCard *cardToMove = m_columns[columnId].last()->card();
qDebug() << "Attempting auto-move of column card " << cardToMove->toString();
if (!tryAutoMoveCard(cardToMove)) {
qDebug() << "> Moving failed, no available move found";
return false;
}
// We succeeded, the card is now in the appropriate foundation pile,
// let's remove the column slot.
m_columns[columnId].removeLast();
ensureColumnRevealed(columnId);
qDebug() << "> Moving complete";
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::tryMoveCardToFoundation(PlayingCard::Suit foundationId, PlayingCard* cardToMove)
{
assert(foundationId >= PlayingCard::Suit::Clubs && foundationId <= PlayingCard::Suit::Spades);
qDebug() << "* Trying to move card " << cardToMove->toString() << " to foundation " << foundationId;
if (cardToMove->suit() != foundationId) {
qDebug() << "* Move attempt failed (wrong suit)";
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) {
qDebug() << "* Move attempt failed (expected 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);
}
if (cardToMove->value() != requiredValue) {
qDebug() << "* Move attempt failed (expected value: " << requiredValue << ")";
return false;
}
m_foundation[foundationId].push_front(cardToMove);
qDebug() << "* Moved card " << cardToMove->toString() << " to foundation " << foundationId;
emit foundationChanged();
return true;
}
bool GameState::tryAutoMoveCard(PlayingCard *cardToMove)
{
// 1. Try moving the card to the foundation
for (PlayingCard::Suit suit : {PlayingCard::Suit::Clubs, PlayingCard::Suit::Diamonds, PlayingCard::Suit::Hearts, PlayingCard::Suit::Spades})
if (cardToMove->suit() == suit && tryMoveCardToFoundation(suit, cardToMove)) {
qDebug() << "* Auto-moved card " << cardToMove->toString() << " to foundation " << suit;
return true;
}
// 2. Try moving the card to another column
for (int columnId = 0; columnId < m_columns.size(); ++columnId)
if (isMoveToColumnLegal(cardToMove, columnId)) {
moveCardToColumn(columnId);
qDebug() << "* Auto-moved card " << cardToMove->toString() << " to column " << columnId;
return true;
}
// No available auto-move
return false;
}
bool GameState::isMoveToColumnLegal(PlayingCard *cardToMove, int columnId)
{
assert(columnId >= 0 && columnId < 7);
if (m_columns[columnId].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
PlayingCard* columnCard = m_columns[columnId].last()->card();
// The card's value must be one less than the card in the column
if (cardToMove->value() != columnCard->value() - 1)
return false;
// The card must be of opposite color
return PlayingCard::areOppositeColors(*cardToMove, *columnCard);
}
void GameState::ensureColumnRevealed(int columnId)
{
assert(columnId >= 0 && columnId < 7);
// Nothing to reveal
if (m_columns[columnId].isEmpty())
return;
// Get the last column slot
ColumnSlot *col = m_columns[columnId].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();
}
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;
}