So far in this series, we have:
- Outlined how we want our Solitaire game to work as a Blazor WebAssembly application.
- Written a set of C# classes defining the components of our Solitaire game
- As well as created a set of Razor components for the individual parts of the game area, including the discard pile, the draw pile, the suit piles, and the stacks.
In this part, we're going to put them all together with some drag-and-drop goodness to create a working game of Solitaire!
Revealing a Hidden Card
There's one little thing we must implement before moving on to the drag-and-drop implementation, and that is how to reveal a hidden card.
You might recall from Part 3 that we created a HiddenCard
Blazor component, and it looked like this:
@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>
Which was then used on the main Solitaire component like this:
<HiddenCard CssClass="solitaire-stackpile"
ClickEvent="(() => RevealCard(card, StackPile1))" />
We don't have the RevealCard()
method yet, so let's write it up:
@code {
//...Rest of implementation
public async Task RevealCard(Card card, StackPile pile)
{
var lastPileCard = pile.Last();
if(lastPileCard.Suit == card.Suit
&& lastPileCard.Value == card.Value)
{
lastPileCard.IsVisible = true;
}
}
}
Now, when we click on a face-down card, we will "flip" the card and make it face up:
Drag-and-Drop Implementation
We should note that, in Solitaire, cards can be dragged from three places (discards, stacks, suit piles) but only dropped on to two places (stacks, suit piles).
Moving a card consists of three separate things:
- Setting the
DraggedCard
property to the current card, - Removing that card from its source position AND
- Adding that card to its new position.
We must do all of these things in the rest of this post, and we'll start with the first one.
DraggedCard and HandleDragStart
When a card is starting to be dragged, we must set the DraggedCard
property of the Solitaire
Blazor component, which will then be filtered down into other components that need it like DraggableCard
.
To do this, we create a method HandleDragStart()
, which does exactly one thing:
public void HandleDragStart(Card selectedCard)
{
DraggedCard = selectedCard;
}
We previously assigned this method to the DraggableCard
and SuitDiscardPile
components like so:
<DraggableCard Card="FirstDiscard"
CssClass="solitaire-discards"
HandleDragStartEvent="(() => HandleDragStart(FirstDiscard))"/>
<SuitDiscardPile SuitPile="ClubsPile"
DraggedCard="DraggedCard"
MoveActiveCardEvent="(() => MoveActiveCard(ClubsPile))"
DragStartEvent="(() => HandleDragStart(ClubsPile.Last()))"/>
Now, when any card is dragged, the entire system will know which card it is.
Removing Cards from their Sources
You may remember that back in Part 2 we defined a method on the PileBase
class called RemoveIfExists()
:
public class PileBase
{
//... 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);
}
}
That method is central to ensuring that we don't accidentally get two of the same card in our deck. Once we have moved the card to its new position, we must remove it from the source pile.
To make it even easier, I created a few methods in the Solitaire
Blazor component that will ensure the given card is removed from various source locations:
@code {
//...Rest of implementation
private void RemoveIfExistsInAnyStack(Card card)
{
StackPile1.RemoveIfExists(card);
StackPile2.RemoveIfExists(card);
StackPile3.RemoveIfExists(card);
StackPile4.RemoveIfExists(card);
StackPile5.RemoveIfExists(card);
StackPile6.RemoveIfExists(card);
StackPile7.RemoveIfExists(card);
}
private void RemoveFromDiscards(Card card)
{
if (FirstDiscard != null
&& FirstDiscard.Suit == card.Suit
&& FirstDiscard.Value == card.Value)
{
FirstDiscard = null;
MoveUpDiscards();
}
}
private void MoveUpDiscards()
{
FirstDiscard = SecondDiscard;
SecondDiscard = ThirdDiscard;
ThirdDiscard = DiscardPile.Pop();
}
private void RemoveFromSuitPiles(Card card)
{
HeartsPile.RemoveIfExists(card);
ClubsPile.RemoveIfExists(card);
DiamondsPile.RemoveIfExists(card);
SpadesPile.RemoveIfExists(card);
}
}
Adding a Single Card to the Suit Piles
When dragging from the discards or the stacks to the suit piles we can only move one card at a time. Since the action is very similar no matter where the card originates from, we can create common methods MoveActiveCard()
which adds the current DraggedCard
to the destination SuitPile
instance (these methods exists on the main Solitaire
component):
@code {
//... Rest of implementation
private void MoveActiveCard(SuitPile suitPile)
{
MoveActiveCard(DraggedCard, suitPile);
}
private void MoveActiveCard(Card card, SuitPile suitPile)
{
if (FirstDiscard != null
&& FirstDiscard.Suit == card.Suit
&& FirstDiscard.Value == card.Value)
{
RemoveFromDiscards(card);
}
RemoveIfExistsInAnyStack(card);
RemoveFromSuitPiles(card);
suitPile.Add(card);
StateHasChanged();
}
}
Which is then passed as the MoveActiveCardEvent
to the SuitDiscardPile
instances. That event gets invoked when a card is dropped onto the suit piles.
Adding Cards to the Stacks
Adding a single card to the stacks isn't much more complicated. The HandleDropEvent
property on DraggableCard
allows us to drop a single card onto a stack. We're going to call the method that moves cards onto the stacks DropCardOntoStack()
:
@code {
//...Rest of implementation
public async Task DropCardOntoStack(StackPile targetStack)
{
//Method implementation
}
}
<!-- Rest of markup -->
<DraggableCard Card="card"
DraggedCard="DraggedCard"
CssClass="solitaire-stackpile"
HandleDragStartEvent="(() => HandleDragStart(card))"
HandleDropEvent="(() => DropCardOntoStack(StackPile1))"/>
The algorithm for this method goes something like this:
- GIVEN a target stack.
- GET the top card of that stack.
- IF that card is null, THEN the stack is empty and the only card that can be placed here is a King.
- ELSE the card that can be placed here must be the opposite color and one rank lower than the top card of the stack.
- IF the dragged card can be placed on this stack.
- ADD the dragged card to the target stack, as well as any cards in the source stack that are lower than the dragged card.
- REMOVE the dragged card from its source.
- REFRESH the interface.
Which results in this method:
@code {
//... Rest of implementation
public async Task DropCardOntoStack(StackPile targetStack)
{
//Get the topmost card of the target stack
var card = targetStack.Last();
bool canStack = false;
if (card == null) //No cards on the stack, we can only allow kings
{
//If the stack is empty, dragged card can only be placed
//if it is a King.
canStack = DraggedCard.Value == CardValue.King;
}
else
{
bool isOppositeColor = (card.IsBlack && DraggedCard.IsRed)
|| (card.IsRed && DraggedCard.IsBlack);
bool isOneLessThan
= (int)DraggedCard.Value == (((int)card.Value) - 1);
//Dragged card can be stacked if it is the opposite color
//and one less rank from the current top card of the stack.
canStack = isOneLessThan && isOppositeColor;
}
if (canStack)
{
//Determine the stack the card came from
StackPile sourceStack = null;
if (StackPile7.Contains(DraggedCard))
sourceStack = StackPile7;
else if (StackPile6.Contains(DraggedCard))
sourceStack = StackPile6;
else if (StackPile5.Contains(DraggedCard))
sourceStack = StackPile5;
else if (StackPile4.Contains(DraggedCard))
sourceStack = StackPile4;
else if (StackPile3.Contains(DraggedCard))
sourceStack = StackPile3;
else if (StackPile2.Contains(DraggedCard))
sourceStack = StackPile2;
else if (StackPile1.Contains(DraggedCard))
sourceStack = StackPile1;
//If the card came from a stack, move the card's stack
if(sourceStack != null)
{
MoveCardStack(targetStack, sourceStack);
}
//If the card came from discards, remove it from there
//and add it to the target stack
if(DraggedCard == FirstDiscard)
{
RemoveFromDiscards(DraggedCard);
targetStack.Add(DraggedCard);
}
//If the card came from the suit piles, remove it from
//the suit pile and add it to the stack.
if(ClubsPile.Contains(DraggedCard)
|| DiamondsPile.Contains(DraggedCard)
|| SpadesPile.Contains(DraggedCard)
|| HeartsPile.Contains(DraggedCard))
{
RemoveFromSuitPiles(DraggedCard);
targetStack.Add(DraggedCard);
}
}
//Refresh the interface
StateHasChanged();
}
}
Now, when a dragged card is dropped onto an instance of DraggableCard
, it will be stacked there if it matches the rank-and-color rules.
You may have noticed the method MoveCardStack()
which is called when the source is another stack. This is used for moving stacks of cards from one stack column to another. Here's that method:
@code {
//...Rest of implementation
private void MoveCardStack(StackPile targetStack, StackPile sourceStack)
{
//Check if any cards are stacked on top of than dragged card
var index = sourceStack.IndexOf(DraggedCard);
if (sourceStack.Count() >= index)
{
List<Card> MoveCards = new List<Card>();
//Get all cards stacked on top of the dragged card
while (index < sourceStack.Count())
{
MoveCards.Insert(0,sourceStack.Pop());
}
//For each card stacked on top of the dragged card...
foreach (var card in MoveCards)
{
//...add those cards, in order, to the target stack
targetStack.Add(card);
}
}
}
}
GIF Overload!
With these implementations complete, we can now do a bunch of drag-and-drop operations, such as moving a card from the discards to the stacks:
From the stacks to the suitpiles:
From the suit piles to the stacks:
We can also drag entire stacks of cards from one place to another.
Guess what? At this point, we have a fully-functional game of Solitaire written in Blazor WebAssembly!
The Sample Project
Don't forget about the sample repository, BlazorGames, hosted on GitHub:
Summary
In this part of our Solitaire in Blazor series, we implemented a set of drag-and-drop functionality that allows the user to drag cards and drop them in specific, permitted places. The user can now see where cards are allowed to be dropped, as well as drag stacks of cards from one stack pile to another.
But we're not done yet. In the next and final part of this series, we're going to implement two useful extra features: a double-click shortcut, and a game autocomplete.
See something I screwed up, or could make better? Like this code? I wanna know about all opinions! Sound off in the comments below.
Happy Coding!