In the previous part of this series, we ended with a fully-functional Solitaire game in Blazor WebAssembly. Now, the only things left to do are to add a couple of nice-to-have features: a double-click shortcut, and an autocomplete.

The Sample Project

The sample project is, as always, hosted on GitHub:

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

The Double-Click Shortcut

In essence, when a user double-clicks a card, that card should automatically be stacked on the corresponding suit pile, if it is allowed. This way the user doesn't need to drag that card all the way across the board.

Or, in GIF form:

To do this we need make changes to many of the methods we have already written, as well as create some new ones.

First, we need to modify our DraggableCard component to have a new property for HandleDoubleClickEvent:

@code {
    //...Other properties

    [Parameter]
    public EventCallback HandleDoubleClickEvent { get; set; }

    //...Rest of implementation
}

<img class="@CssClass @AdditionalCss"
     src="images/common/@(Card.ImageName).png"
     @ondragstart="HandleDragStartEvent"
     @ondragend="StateHasChanged"
     @ondblclick="HandleDoubleClickEvent" <-- NEW
     @ondragenter="CardDragEnter"
     @ondragleave="CardDragLeave"
     @ondrop="(async () => { await HandleDropEvent.InvokeAsync(this); AdditionalCss = null; })"
     ondragover="event.preventDefault();"/>

On the main Solitaire component, we can use this new property like so:

<DraggableCard Card="card"
               DraggedCard="DraggedCard"
               HandleDoubleClickEvent="(() => CardDoubleClick(card))"/>
Other properties excluded for brevity

Now we just need to define the CardDoubleClick() method. Our implementation will take advantage of the MoveActiveCard() method we defined in Part 4.

@code {
    //... Rest of implementation
    
    public void CardDoubleClick(Card card)
    {
        //Select the target suit pile based on the card suit
        SuitPile selectedPile = ClubsPile;
        switch (card.Suit)
        {
            case CardSuit.Diamonds:
                selectedPile = DiamondsPile;
                break;

            case CardSuit.Spades:
                selectedPile = SpadesPile;
                break;

            case CardSuit.Hearts:
                selectedPile = HeartsPile;
                break;
        }


        CheckMoveCardToSuitPile(card, selectedPile);
    }

    private void CheckMoveCardToSuitPile(Card card, SuitPile suitPile)
    {
        if (suitPile.Suit == card.Suit
            && suitPile.AllowedValue == card.Value)
        {
            MoveActiveCard(card, suitPile);
        }
    }
}

This allows us to move cards when they are double-clicked!

AutoComplete

Sometimes in a game of Solitaire, winning becomes inevitable. When this happens, we want our system to automatically complete the game, saving the user the effort of doing so.

But what is that point where winning is certain? We'll say that the user is guaranteed to win if all of the following are true:

  • There are no more cards in the draw pile.
  • There are no more cards in the discards.
  • There are no hidden cards left in the stacks

A game that fits all of these conditions could look like this:

Or like this:

What all this means is that we need to trigger whatever method does the auto complete after two different situations:

  1. When a card is added to the stacks.
  2. When a card in the stacks is flipped "face up".

Once this happens, and the other conditions are met, the game can run the auto complete.

That means we can create a method AutoComplete(), which will be called by methods DropCardOntoStack() and RevealCard() that we created in Part 4. Because the new method will be asynchronous, the other two methods will also need to be async:

@code {
    //...Rest of implementation
    
    public async Task DropCardOntoStack(StackPile targetStack)
    {
        //Method implementation
        await AutoComplete();
    }
    
    public async Task RevealCard(Card card, StackPile pile)
    {
        //Method implementation
        await AutoComplete();
    }
    
    private async Task AutoComplete()
    {
        //TODO
    }
}

So what's the algorithm for AutoComplete()? It goes like this:

  1. IF no cards remain in the draw pile AND no cards remain in the discards AND there are no hidden cards in any stack
  2. THEN WHILE at least one of the suit piles is not "complete", i.e. doesn't have a King on top
  3. CYCLE through each stack, placing the topmost card on each stack into the suit pile, if possible.
  4. CONTINUE until no cards are left in any stack.

In order to implement this method, we must add a property to the SuitPile class from Part 2:

public class SuitPile : PileBase
{
    //...Rest of implementation        

    public bool IsComplete
    {
        get
        {
            return (int)AllowedValue == 14; //Only possible if the
                                            //top card is a King
        }
    }
}

We also need a new method in the StackPile class, which was also written in Part 2:

public class StackPile : PileBase
{
    //...Rest of implementation

    public bool HasNoHiddenCards()
    {
        return !Any() || Cards.All(x => x.IsVisible);
    }
}

Once SuitPile.IsComplete and StackPile.HasNoHiddenCards() are available, we can write the code for the AutoComplete() method, which ends up like this:

@code {
    //...Rest of implementation
    
    private async Task AutoComplete()
    {
        if(StackPile1.HasNoHiddenCards()
            && StackPile2.HasNoHiddenCards()
            && StackPile3.HasNoHiddenCards()
            && StackPile4.HasNoHiddenCards()
            && StackPile5.HasNoHiddenCards()
            && StackPile6.HasNoHiddenCards()
            && StackPile7.HasNoHiddenCards()
            && !DiscardPile.Any()
            && DrawDeck.Count == 0
            && FirstDiscard == null)
        {
            while(!ClubsPile.IsComplete 
                  || !DiamondsPile.IsComplete 
                  || !SpadesPile.IsComplete 
                  || !HeartsPile.IsComplete)
            {
                await CheckMoveStackTopCard(StackPile1);
                await CheckMoveStackTopCard(StackPile2);
                await CheckMoveStackTopCard(StackPile3);
                await CheckMoveStackTopCard(StackPile4);
                await CheckMoveStackTopCard(StackPile5);
                await CheckMoveStackTopCard(StackPile6);
                await CheckMoveStackTopCard(StackPile7);
            }
        }
    }

    public async Task CheckMoveStackTopCard(StackPile stackPile)
    {
        //Get the top card of the stack
        var card = stackPile.Last();
        if (card != null) //If the top card is not null
        {
            //Move the top card to the correct suit pile, if possible.
            CheckMoveCardToSuitPile(card, ClubsPile);
            CheckMoveCardToSuitPile(card, DiamondsPile);
            CheckMoveCardToSuitPile(card, SpadesPile);
            CheckMoveCardToSuitPile(card, HeartsPile);
        }
        await Task.Delay(100);
        StateHasChanged();
    }
}

That method allowed me to get a pretty neat GIF:

Guess how many games I had to play to get this shot.

Guess what? We're done! Our Solitaire in Blazor game is complete, and ready to play. If you haven't already, check out the playable game on my sister site, BlazorGames.net.

Potential Improvements

There are a few potential improvements that could be made to this system.

First, a minor one: if a card in the stacks has cards already on top of it, but would allow the dragged card to stack on it, it still gets the green dashed border, even though the game won't let you actually place that card there. Watch as I attempt to move the six of clubs in this GIF:

Another minor one is this: with large stacks on top of lots of hidden cards, the playing field gets really big. I'd like to minimize the space between the hidden cards, but not the shown cards.

You can't tell that the five of clubs and four of hearts are stacked on the six of diamonds.

All said, though, this is a pretty nice game, if I do say so myself.

Summary

In this, the last post of this series, we added on two nice-to-have features to our Solitaire game: a double-click shortcut to move cards to the suit piles, and the autocomplete.

This game was the most complex one I've had to build yet for BlazorGames: it took me 20 hours of initial coding, 10 hours of fine-tuning, and a further 6 hours to write the posts, refine the code, write the comments, and make sure everything was ready for publishing. It beats out the Tetris game by five hours, mostly because I had to spend that time learning drag-and-drop for Blazor. But now that it's done, I'm pretty dang proud of it.

We have a fully-functional Solitaire game in Blazor WebAssembly!

If you liked this game, or want to see more of them, would you consider buying me a coffee to support my projects?

Thanks for reading, and Happy Coding!