In previous parts of this series, we outlined what we want our Solitaire in Blazor game to do, and we built a set of C# classes to represent the different parts of the game.
In this part, we're going to start building our Blazor components, and implement drawing and discarding cards. After we implement the code in this section, we'll have some key functionality done, and a lot of background work ready to be used. Let's get started!
The Main Blazor Component
We first need to create a main Razor component that will represent a game of Solitaire. The component acts as the container for all other components. Let's recall our game layout to determine what kinds of components we will need:
From this layout, we can see that we need:
- A draw pile
- Three shown discarded cards
- A pile of discarded cards that are not shown to the user
- Four suit piles
- Seven stacks
There are also two other things we need. One of these is straightforward: an instance of GameStatus
to keep track of whether or not the game is currently being played.
The other thing is less obvious, and it comes from a particular restriction: the user may only interact with (e.g. drag) one card at any time.
When a user drags a card, they are dragging only that card. From the discards to the stacks, from the discards to the suit piles, and from the stacks to the suit piles, only one card at a time can be moved. Even when the user is dragging a mini-stack to a different stack, the user is only dragging a single card; the game will determine if that card has child cards and whether or not they must be moved to.
Consequently, we need a property of Card
that keeps track of the card the user is currently dragging. Here's our initial Blazor component "Solitaire.razor":
This main component will be referred to as the Solitaire
component, and will be used throughout the rest of this series.
For now, let's move on and build a couple of smaller Razor Components that will help us.
HiddenCard Component
We are going to say that, in Solitaire, there are "hidden" cards (cards that have a suit and value but are not show to the user, e.g. "face-down" cards). These might include the cards in the draw pile, as well as cards in the stacks that haven't been revealed yet.
We therefore are going to create a Razor Component HiddenCard
to represent these cards:
@code {
[Parameter]
public string CssClass { get; set; }
[Parameter]
public EventCallback ClickEvent { get; set; }
}
<div class="@CssClass" @onclick="ClickEvent">
<img src="images/solitaire/cardBack.png" />
</div>
Notice the property ClickEvent
. Different things happen when the player clicks on the draw pile and when the player clicks on a hidden card in the stack. The EventCallback
type allows us to pass events to Razor components.
NonDraggableCard Component
We also need another type, called NonDraggableCard
. In one case, cards are shown to the user that are not draggable: the bottom two cards in the discard pile.
We need one more Razor Component before we can build the interface for drawing and discarding. Since the player can drag the topmost card in the discards, we need to implement a component for a DraggableCard
.
DraggableCard Component
"Draggable" cards include nearly every face-up card in a Solitaire game. All face-up cards in the stacks, each top card in the suit piles, and the topmost card in the discards are draggable cards.
Draggable cards need to know more about the current state of the game than either hidden or non-draggable cards do. For example, a draggable card needs to know what other card is being dragged, since that card might be able to be stacked or placed in the suit piles.
In fact, by my reckoning, a DraggableCard
component needs the following properties:
@code {
//The card represented by this component
[Parameter]
public Card Card { get; set; }
//The card currently being dragged
[Parameter]
public Card DraggedCard { get; set; }
[Parameter]
public EventCallback HandleDragStartEvent { get; set; }
[Parameter]
public EventCallback HandleDropEvent { get; set; }
[Parameter]
public string CssClass { get; set; }
public string AdditionalCss { get; set; }
}
The two properties of type EventCallback
do different things:
HandleDragStartEvent
is needed to set the current card as the dragged card, should the user start to drag it.HandleDropEvent
is needed to handle other cards being dropped on this card.
Both of those events will be set by the calling component, which (in this case) will also be our main Solitaire
component. We'll start building that later.
When the player drags a card over another card, if the card can be placed there, we want to show an additional CSS border, like so:
This is what the property AdditionalCSS
is for, but we will also need events for the drag entering and leaving the current card. Here's those events in the DraggableCard
component:
@code {
//...Rest of implementation
public void CardDragEnter()
{
@if (DraggedCard != null)
{
//Card being dropped must be opposite color from this card...
bool isOppositeColor = (Card.IsBlack && DraggedCard.IsRed)
|| (Card.IsRed && DraggedCard.IsBlack);
//...and also one less than this card.
bool isOneLessThan
= (int)DraggedCard.Value == (((int)Card.Value) - 1);
if (isOppositeColor && isOneLessThan)
{
AdditionalCss = " solitaire-can-drop";
}
}
}
public void CardDragLeave()
{
AdditionalCss = "";
}
}
The last bit we need is the markup for the card element. We'll use an <img>
element for this. Here's the markup for the entire DraggableCard
component:
OK then! We now have three smaller components HiddenCard
, NonDraggableCard
, and DraggableCard
written and ready to go. Now let's start coding up drawing and discarding.
Drawing and Discarding Cards
In this version of Solitaire, players will draw one card at a time. That card is shown to the user.
As more cards are drawn, they are placed over the top of previously-drawn cards.
Only the topmost card can be moved to another place.
As more cards are drawn, the earlier-drawn cards begin to disappear from view.
In our implementation, we have the properties FirstDiscard
, SecondDiscard
, and ThirdDiscard
to represent the three shown cards, and DiscardPile
to represent all discarded cards not currently shown. Only cards in the FirstDiscard
position can be moved.
Methods and Markup
In our implementation, the user can perform one action on the draw pile Draw()
. If there are no more cards to draw, the user can perform a different action ResetDrawPile()
.
@code {
//...Rest of implementation
public void Draw()
{
}
public void ResetDrawPile()
{
}
}
Let's now build the markup for the draw pile. Here's part of this markup, which uses the HiddenCard
component from earlier:
If there are still cards in the draw pile, we display a green card back (via the HiddenCard
component); the more cards in the draw pile, the more card backs displayed. If the user clicks the green cards, a new card is drawn via the Draw()
method.
If instead, there aren't any cards in the draw pile, we show a grey card back, which can be clicked to invoke the ResetDrawPile()
method.
We now need to fill in the Draw()
and ResetDrawPile()
methods:
@code {
//...Rest of implementation
public void Draw()
{
//If the bottommost discard is there...
if(ThirdDiscard != null)
DiscardPile.Add(ThirdDiscard); //Add it to the discard pile
//If the middle discard is there...
if(SecondDiscard != null)
ThirdDiscard = SecondDiscard; //Make it the bottommost
//If the topmost discard is there...
if(FirstDiscard != null)
SecondDiscard = FirstDiscard; //Make it the middle discard
//Set the topmost discard to the top card of the draw pile
FirstDiscard = DrawDeck.Draw();
//Let Blazor know to update the display
StateHasChanged();
}
public void ResetDrawPile()
{
//First, add all three discards to the discard pile.
DiscardPile.Add(ThirdDiscard);
DiscardPile.Add(SecondDiscard);
DiscardPile.Add(FirstDiscard);
//Get all cards in the discard pile
var allCards = DiscardPile.GetAll();
//Put your thing down, flip it and reverse it
allCards.Reverse();
//Add them back to the draw pile
foreach (var card in allCards)
{
DrawDeck.Add(card);
}
//Reset the discards and discard pile
FirstDiscard = null;
SecondDiscard = null;
ThirdDiscard = null;
DiscardPile = new DiscardPile();
}
}
Markup for Discards
The last bit we need for discards is their markup, which uses the NonDraggableCard
and DraggableCard
components from earlier and looks like this:
<div class="row">
<div class="col-2">
<!-- Markup for draw pile -->
</div>
<div class="col-2">
@if (ThirdDiscard != null)
{
<NonDraggableCard Card="ThirdDiscard"/>
}
@if (SecondDiscard != null)
{
<NonDraggableCard Card="SecondDiscard" />
}
@if (FirstDiscard != null)
{
<DraggableCard Card="FirstDiscard"
CssClass="solitaire-discards"
HandleDragStartEvent="(() => HandleDragStart(FirstDiscard))"/>
}
</div>
<div class="col-8">
<!-- Markup for suit piles -->
</div>
</div>
Note that we are passing in a callback to an event called HandleDragStart
, and we will implement that in the next part of this series.
GIF Time!
At this point, we have a working draw and discard functionality. Here's a GIF showing it:
Suit Piles
The next part of our implementation is the suit piles, where cards must be stacked by suits in ascending order (Ace, two, three, four, so on).
SuitPile Blazor Component
Each suit pile has the following characteristics:
- It must know what suit (clubs, spades, hearts, or diamonds) can be dropped on the pile.
- It must be aware of the dragged card and display the green border if the dragged card can be dropped on the suit pile.
- It must display the suit it wants before any card is dropped on it.
Due to these rules, the markup for the SuitDiscardPile
component has these properties:
@code {
[Parameter]
public SuitPile SuitPile { get; set; }
[Parameter]
public Card DraggedCard { get; set; }
[Parameter]
public EventCallback DragStartEvent { get; set; }
[Parameter]
public EventCallback MoveActiveCardEvent { get; set; }
private string CssClass { get; set; }
private string ImagePath { get; set; }
}
The event MoveActiveCardCallback
will be used in the next part of this series.
The suit piles need to handle situations where a dragged card enters, leaves, and is dropped. We can do that with the following methods:
@code {
//Properties
//If the dragged card can be dropped onto this card,
//add a special CSS class.
public void HandleDragEnter()
{
if (DraggedCard.Value == SuitPile.AllowedValue
&& DraggedCard.Suit == SuitPile.Suit)
{
CssClass = "solitaire-can-drop";
}
}
//Once the dragged card leaves, always reset the CSS class.
public void HandleDragLeave()
{
CssClass = "";
}
public async Task HandleDrop()
{
CssClass = ""; //Reset the CSS
//If the dragged card can be dropped here
if (DraggedCard.Value == SuitPile.AllowedValue
&& DraggedCard.Suit == SuitPile.Suit)
{
//Invoke the callback to move the dragged card.
//This callback will be different for stacks and suit piles.
await MoveActiveCardEvent.InvokeAsync(SuitPile);
}
}
}
Finally, we need the <img>
element markup, with just a little more C# to determine the correct image path. Here's the complete markup for the SuitDiscardPile
Blazor component:
@code {
[Parameter]
public SuitPile SuitPile { get; set; }
[Parameter]
public Card DraggedCard { get; set; }
[Parameter]
public EventCallback DragStartEvent { get; set; }
[Parameter]
public EventCallback MoveActiveCardCallback { get; set; }
private string CssClass { get; set; }
private string ImagePath { get; set; }
//If the dragged card can be dropped onto this card,
//add a special CSS class.
public void HandleDragEnter()
{
if (DraggedCard.Value == SuitPile.AllowedValue
&& DraggedCard.Suit == SuitPile.Suit)
{
CssClass = "solitaire-can-drop";
}
}
//Once the dragged card leaves, always reset the CSS class.
public void HandleDragLeave()
{
CssClass = "";
}
public async Task HandleDrop()
{
CssClass = ""; //Reset the CSS
//If the dragged card can be dropped here
if (DraggedCard.Value == SuitPile.AllowedValue
&& DraggedCard.Suit == SuitPile.Suit)
{
//Invoke the callback to move the dragged card.
//This callback will be different for stacks and suit piles.
await MoveActiveCardCallback.InvokeAsync(SuitPile);
}
}
}
@if (SuitPile.Any())
ImagePath = $"images/common/{SuitPile.Last().ImageName}.png";
else
ImagePath = $"images/solitaire/cardDashed{SuitPile.Suit.GetDisplayName()}.png";
<div @ondragstart="DragStartEvent"
@ondragend="StateHasChanged"
@ondragenter="HandleDragEnter"
@ondragleave="HandleDragLeave"
@ondrop="HandleDrop"
ondragover="event.preventDefault();">
<img class="solitaire-card @CssClass"
src="@ImagePath" />
</div>
On the main Solitaire
component, the markup for these suit piles looks like this:
<div class="row">
<div class="col-2">
<!-- Markup for Draw Pile -->
</div>
<div class="col-2">
<!-- Markup for Discards -->
</div>
<div class="col-8">
<div class="solitaire-suitpile-container">
<div class="row">
<div class="col-2">
<SuitDiscardPile SuitPile="ClubsPile"
DraggedCard="DraggedCard"
MoveActiveCardEvent="(() =>
MoveActiveCard(ClubsPile))"
DragStartEvent="(() =>
HandleDragStart(ClubsPile.Last()))"/>
</div>
<div class="col-2">
<SuitDiscardPile SuitPile="DiamondsPile"
DraggedCard="DraggedCard"
MoveActiveCardEvent="(() =>
MoveActiveCard(DiamondsPile))"
DragStartEvent="(() =>
HandleDragStart(DiamondsPile.Last()))"/>
</div>
<div class="col-2">
<SuitDiscardPile SuitPile="SpadesPile"
DraggedCard="DraggedCard"
MoveActiveCardEvent="(() =>
MoveActiveCard(SpadesPile))"
DragStartEvent="(() =>
HandleDragStart(SpadesPile.Last()))"/>
</div>
<div class="col-2">
<SuitDiscardPile SuitPile="HeartsPile"
DraggedCard="DraggedCard"
MoveActiveCardEvent="(() =>
MoveActiveCard(HeartsPile))"
DragStartEvent="(() =>
HandleDragStart(HeartsPile.Last()))"/>
</div>
</div>
</div>
</div>
</div>
We will be implementing the MoveActiveCard()
and HandleDragStart()
methods in the next part of this series.
The next component we need are the stacks, but before we write that markup, we should write the code to create a new game of Solitaire.
Creating a New Game
You might recall from Part 3 that the main Solitaire component has properties for each stack, each suit pile, the discards, and the draw pile:
@code {
public Card DraggedCard { get; set; }
public GameStatus Status { get; set; } = GameStatus.NotStarted;
public Card FirstDiscard { get; set; }
public Card SecondDiscard { get; set; }
public Card ThirdDiscard { get; set; }
CardDeck DrawDeck = new CardDeck();
DiscardPile DiscardPile = new DiscardPile();
SuitPile ClubsPile = new SuitPile(CardSuit.Clubs);
SuitPile DiamondsPile = new SuitPile(CardSuit.Diamonds);
SuitPile SpadesPile = new SuitPile(CardSuit.Spades);
SuitPile HeartsPile = new SuitPile(CardSuit.Hearts);
//Stack piles are numbered from left to right (1 is leftmost).
StackPile StackPile1 = new StackPile();
StackPile StackPile2 = new StackPile();
StackPile StackPile3 = new StackPile();
StackPile StackPile4 = new StackPile();
StackPile StackPile5 = new StackPile();
StackPile StackPile6 = new StackPile();
StackPile StackPile7 = new StackPile();
}
Let's create a button that starts a new game:
<div class="row">
<div class="col-2">
<button class="btn-primary" @onclick="NewGame">New Game</button>
</div>
</div>
That NewGame()
event will need to do the following:
- Clear out the remaining cards in all stacks, suit piles, discards, and the draw pile.
- Create a new 52-card deck and shuffle it.
- Add the correct amount of cards to each stack (one in the first, two in the second, three in the third, up to seven in the seventh).
- Put the remaining cards in the draw pile.
- Update the
GameStatus
All of that results in a rather long method:
@code {
//...Rest of implementation
public void NewGame()
{
//Set the game status to Playing
Status = GameStatus.Playing;
//Create a new draw deck and discard pile
DrawDeck = new CardDeck();
DiscardPile = new DiscardPile();
//Reset the discards
FirstDiscard = null;
SecondDiscard = null;
ThirdDiscard = null;
//Create new suit piles
ClubsPile = new SuitPile(CardSuit.Clubs);
DiamondsPile = new SuitPile(CardSuit.Diamonds);
SpadesPile = new SuitPile(CardSuit.Spades);
HeartsPile = new SuitPile(CardSuit.Hearts);
//Create new stacks
StackPile1 = new StackPile();
StackPile2 = new StackPile();
StackPile3 = new StackPile();
StackPile4 = new StackPile();
StackPile5 = new StackPile();
StackPile6 = new StackPile();
StackPile7 = new StackPile();
//Deal cards to the stacks
StackPile1.Add(DrawDeck.Draw());
StackPile2.Add(DrawDeck.DrawHidden());
StackPile3.Add(DrawDeck.DrawHidden());
StackPile4.Add(DrawDeck.DrawHidden());
StackPile5.Add(DrawDeck.DrawHidden());
StackPile6.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile2.Add(DrawDeck.Draw());
StackPile3.Add(DrawDeck.DrawHidden());
StackPile4.Add(DrawDeck.DrawHidden());
StackPile5.Add(DrawDeck.DrawHidden());
StackPile6.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile3.Add(DrawDeck.Draw());
StackPile4.Add(DrawDeck.DrawHidden());
StackPile5.Add(DrawDeck.DrawHidden());
StackPile6.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile4.Add(DrawDeck.Draw());
StackPile5.Add(DrawDeck.DrawHidden());
StackPile6.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile5.Add(DrawDeck.Draw());
StackPile6.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile6.Add(DrawDeck.Draw());
StackPile7.Add(DrawDeck.DrawHidden());
StackPile7.Add(DrawDeck.Draw());
StateHasChanged();
}
}
Now we can work on the markup for the stacks. First, we need another small component, one that represents an empty stack.
Empty Stacks
In Solitaire, it is possible for there to be no cards in a stack:
Only a King can be placed on an empty stack. Therefore, an empty stack needs to handle the Drop event (which must be passed to this component by another), needs to know about the currently-dragged card, and needs to know what stack is currently empty. That results in the markup for the EmptyStack
component:
@code {
//The Stack that this empty stack represents
[Parameter]
public StackPile Pile { get; set; }
//The currently dragged card
[Parameter]
public Card DraggedCard { get; set; }
[Parameter]
public EventCallback DropEvent { get; set; }
private string CssClass { get; set; }
public void EmptyStackDragEnter()
{
if (DraggedCard.Value == CardValue.King)
{
CssClass = "solitaire-can-drop";
}
}
public void EmptyStackDragLeave()
{
CssClass = "";
}
}
<img class="@CssClass"
src="images/solitaire/cardBackGrey.png"
@ondragenter="EmptyStackDragEnter"
@ondragleave="EmptyStackDragLeave"
@ondrop="DropEvent"
ondragover="event.preventDefault();"
ondragstart="event.dataTransfer.setData('', event.target.id);" />
The Stacks
Unlike the other parts of Solitaire (the draw pile, the suit piles, etc.) I was unable to make the stacks a separate component; they require a lot of information to work. Instead, I broke each Stack down into components.
- If there are no cards in the stack, use the
EmptyStack
component. - If there are cards in the stack, for each card that is "face up", use the
DraggableCard
component. - For each card that is "face down", use the
HiddenCard
component.
Which results in this markup for a single stack:
There are many events being passed into the Blazor components here, and we'll need to implement each of them in the next part of this series, when we begin work on the actual drag-and-drop implementation.
But for now, we should celebrate that we have each part of a Solitaire game in Blazor ready to be wired up to events!
The Sample Project
Don't forget to take a look at the sample project on GitHub if you haven't already!
Summary
In this part of the series, we implemented:
- A set of small components, including
HiddenCard
,NonDraggableCard
, andDraggableCard
. - A draw pile, which appears to shrink as more cards are drawn.
- A set of discards, which stack appropriately.
- The suit piles, which stack cards in ascending order by suit.
- The first part of the stacks implementation.
In the next part of this series, we will begin wiring up drag-and-drop events for each of these components.
See something I could've done better? Got a way to improve my code? I wanna know about it! Share in the comments below.
Happy Coding!