So far in this series, we have:

  • Outlined how we want our Solitaire game to work as a Blazor WebAssembly application.
Solitaire in Blazor Part 1 - Overview
The biggest time waster in history, now in Blazor WebAssembly!
  • Written a set of C# classes defining the components of our Solitaire game
Solitaire in Blazor Part 2 - The C# Classes
We need classes for a Card, the DrawPile, the DiscardPile, SuitPiles, and the Stacks.
  • 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.
Solitaire in Blazor Part 3 - Drawing, Discarding, and the Stacks
The first part of the main Solitaire functionality will be done in this post!

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:

  1. GIVEN a target stack.
  2. GET the top card of that stack.
  3. IF that card is null, THEN the stack is empty and the only card that can be placed here is a King.
  4. ELSE the card that can be placed here must be the opposite color and one rank lower than the top card of the stack.
  5. IF the dragged card can be placed on this stack.
  6. ADD the dragged card to the target stack, as well as any cards in the source stack that are lower than the dragged card.
  7. REMOVE the dragged card from its source.
  8. 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:

exceptionnotfound/BlazorGames
Solitaire, Minesweeper, ConnectFour, Tetris, Blackjack, Conway’s Game of Life, and more, all written in Blazor WebAssembly. - exceptionnotfound/BlazorGames

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!