Since we now have a good understanding of how we want to implement Solitaire in Blazor, let's start building our "back-end" model by creating a set of C# classes to represent cards, the draw pile, the discard pile, the suit piles, and the stacks.
Basic Classes
The most basic class we'll encounter when modeling Solitaire is an individual playing card. In the real world, these objects only need two properties:
- A suit (Diamonds, Clubs, Spades, or Hears)
- A value (e.g. Ace, Two. King, Jack, Nine, etc.)
Both of these will be enumerations in our implementation; they look like this:
public enum CardSuit
{
Hearts,
Clubs,
Diamonds,
Spades
}
public enum CardValue
{
Ace = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 11,
Queen = 12,
King = 13
}
Eagle-eyed readers will notice that these are the same enumerations we used for our Blackjack implementation:
In our Solitaire implementation for a single card, we're going to include a few additional properties:
- An
ImageName
for the name of the image to show when the card is displayed. - A boolean
IsVisible
for times when the card should not be shown to the player (i.e. if it's in a stack and covered by another card). - Helper methods for
IsBlack
andIsRed
to make our code cleaner.
All of these properties together results in a C# class Card
:
public class Card
{
public CardSuit Suit { get; set; }
public CardValue Value { get; set; }
public string ImageName { get; set; }
public bool IsVisible { get; set; }
public bool IsRed
{
get {
return Suit == CardSuit.Diamonds || Suit == CardSuit.Hearts;
}
}
public bool IsBlack
{
get
{
return !IsRed;
}
}
}
Deck of Cards
Just like in Blackjack, the next object we need to build is a deck of cards. It is from this deck that the player will draw single cards for use in their stacks or in the suit piles.
At its most basic, a Solitaire draw pile object (which we will call CardDeck
) contains a collection of cards. We will implement that collection as a Stack<T>
, because it must be drawn in a certain order.
public class CardDeck
{
protected Stack<Card> Cards { get; set; } = new Stack<Card>();
}
In a game of Solitaire, the player can draw cards from the draw pile. Cards drawn from the pile in this manner are visible to the player. We will need a method to represent this:
public class CardDeck
{
protected Stack<Card> Cards { get; set; } = new Stack<Card>();
public Card Draw()
{
var card = Cards.Pop();
card.IsVisible = true;
return card;
}
}
We also need two other methods, which are used in certain situations.
- First, we need a
DrawHidden()
method. When the game is set up, we will need to draw "hidden" cards to form the stacks.
public class CardDeck
{
protected Stack<Card> Cards { get; set; } = new Stack<Card>();
public Card DrawHidden()
{
var card = Cards.Pop();
card.IsVisible = false;
return card;
}
//...Rest of implementation
}
- Second, we need an
Add()
method which can add cards back to the draw pile. This will be used when the player has run out of cards in the draw pile, and decides to flip the discard pile over to form a new draw pile.
public class CardDeck
{
protected Stack<Card> Cards { get; set; } = new Stack<Card>();
public void Add(Card card)
{
Cards.Push(card);
}
//...Rest of implementation
}
With all of these methods in place, our CardDeck
object is complete! Now we can begin thinking about to represent the discard pile...
But wait? If we think about it, the remaining objects we need to create (the discard pile, the suit piles, and the stacks) are all pretty similar. Maybe we don't need to model them individually?
The Base Pile Class
Here's a quick reminder of the layout of our Solitaire game:
An interesting thing about the remaining objects (discards, suit piles, and stacks) is that, ultimately, they are all the same root thing: an ordered pile of cards. Each of them behaves somewhat differently (e.g. the Discards only allows the user to select the topmost card, the Suit Piles must be stacked in order, the Stacks have hidden cards, the player may move entire substacks of cards from one Stack to another, etc.), but they're all still a ordered pile of cards.
If we enumerate the functionality of each kind of stack, we might find that they have some things in common. For example:
- The player may interact with the top card of each pile (for Discards and Suit Piles, the player may only interact with the top card).
- Cards must be able to be "pushed" into the pile, and "popped" from it.
To my mind, this is enough to have a base class PileBase
that the Discards, Suit Piles, and Stacks will inherit from. Let's create that base class:
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
}
Note that our collection of cards in PileBase
is a List<Card>
, not a Stack<Card>
, because a List<Card>
is easier to interact with.
If we work through some of the features of Solitaire, we can add some methods to this base clase.
- Discards, Suit Piles, and Stacks all need to have cards added to their pile:
- Discards, Suit Piles, and Stacks all need to be able to "pop" the top card off, and return it:
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
//...Rest of implementation
public Card Pop()
{
var lastCard = Cards.LastOrDefault();
if(lastCard != null)
{
Cards.Remove(lastCard);
}
return lastCard;
}
}
- All three piles need to know if there are no cards in the pile (for Discards, this causes the discarded cards to become a new draw pile; for Suit Piles, this means we can only place the respective Aces; for Stacks, this means we can only put Kings on this stack).
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
//...Rest of implementation
public bool Any()
{
return Cards.Any();
}
}
- The Suit Piles and the Stacks have rules about what card can be placed on them at any time. To calculate these rules, we need to know what the "topmost" card is. In our implementation, the topmost card will be the last card in the pile:
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
//...Rest of implementation
public Card Last()
{
return Cards.LastOrDefault();
}
}
- The Suit Piles and the Stacks will need to know if a particular card is in a particular pile. We can implement a
Contains()
method to find a card by its suit and value:
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
//...Rest of implementation
public bool Contains(Card card)
{
return Cards.Any(x => x.Suit == card.Suit
&& x.Value == card.Value);
}
}
- Finally, we need a method to remove a card from a pile, if it exists there. This method will be used when cards are dragged from one Stack to another, or from the Discards to the Stacks, etc.
public class PileBase
{
public List<Card> Cards { get; set; } = new List<Card>();
//...Rest of implementation
public void RemoveIfExists(Card card)
{
var matchingCard = Cards.FirstOrDefault(x => x.Suit == card.Suit
&& x.Value == card.Value);
if(matchingCard != null)
Cards.Remove(matchingCard);
}
}
We can now build three classes which need to inherit from the PileBase class, starting with the Discards.
The Discard Pile
The discard pile is the simplest of the three objects that inherit from PileBase
. Because our implementation treats the draw pile and the discard pile as separate objects, we will need a way to get all cards from the discard pile, so that we can add them back into the draw pile.
Therefore, our implemention of DiscardPile
inherits from PileBase
and has one unique method:
public class DiscardPile : PileBase
{
public List<Card> GetAll()
{
return Cards.ToList();
}
}
The Suit Piles
The suit piles are slightly more complex than the discard pile.
For starters, only cards of one particular suit can be added to each suit pile. To make this easy to track, let's create an object SuitPile
which inherits from PileBase
, has a property for the pile's CardSuit
, and a basic constructor:
public class SuitPile : PileBase
{
public CardSuit Suit { get; set; }
public SuitPile(CardSuit suit)
{
Suit = suit;
}
}
Allowed Values
The suit piles can only allow one specific value in their suit. If the pile is empty, the pile can only accept an Ace; if it already has an Ace, it can only accept a two; and so on up the line until the pile is completed by placing a King on it.
Let's create a property of SuitPile
that will check the topmost card, and return the value the pile is currently looking for:
public class SuitPile : PileBase
{
//...Rest of implementation
public CardValue AllowedValue
{
get
{
var topCard = Last();
if (topCard == null) return CardValue.Ace;
int currentValue = (int)topCard.Value;
return (CardValue)(currentValue + 1);
}
}
}
Convenience Method: Is the Pile Complete?
We'll also include a simple convenience method that returns a bool
specifying whether or not the pile is complete (i.e. the topmost card is a King):
public class SuitPile : PileBase
{
//...Rest of implementation
public bool IsComplete
{
get
{
return (int)AllowedValue == 14;
}
}
}
Our SuitPile
class is now complete!
The Stacks
The Stacks are far and away the most complicated of the classes the inherit from PileBase
, because they have much more functionality. Among other things, the Stacks must ensure that:
- Cards are placed by alternating colors, e.g. black-red-black-red-black
- Cards are ordered by descending value, e.g. Jack-Ten-Nine-Eight-Seven
- Only Kings can be placed on empty stacks.
But the most important functionality, the one that will be the most difficult to implement is:
- Substacks of cards can be moved from one stack to another, provided they follow the color-and-rank rules.
In this implementation, I have chosen to make the rank-and-color rules exist outside of the StackPile
class, so we will be implementing them in a later part of this series. You may choose to do this a different way.
The Basic StackPile
Class
Let's start with an empty class, which we will call StackPile
:
The StackPile
needs some basic functionality, including:
- A method to get all the cards, so we can display them to the user.
public class StackPile : PileBase
{
public List<Card> GetAllCards()
{
return Cards;
}
}
- A method that returns the count of the cards in the stack, so we can know if the stack is empty:
public class StackPile : PileBase
{
//...Rest of implementation
public int Count()
{
return Cards.Count();
}
}
- A method that checks to see if all cards in the stack are not hidden. This will be used in the Auto-Complete feature we will make in Part 6 of this series.
We also need a specialty method, one that gets the index of a particular card in a given Stack. We will use this method when we implement dragging substacks from one Stack to another:
public class StackPile : PileBase
{
//...Rest of implementation
public int IndexOf(Card card)
{
var matchingCard = Cards.FirstOrDefault(x => x.Suit == card.Suit
&& x.Value == card.Value);
if (matchingCard != null)
return Cards.IndexOf(matchingCard);
return 0;
}
}
With that, our StackPile
implementation is also complete!
The Sample Project
Don't forget to check out the GitHub repo for BlazorGames if you haven't already:
Summary
As you might have noticed, the C# classes in this part of our Solitaire in Blazor series are more straightforward than in other games. This is because much of the functionality we need will be implemented in Blazor Components rather than C# classes, and we will start doing that in Part 3 of this series, which is coming up next.
Did you see something I could do better? Got a way to make my code more readable, more understandable, more concise? I wanna hear what you have to say! Share in the comments below.
Happy Coding!