Creating Solitaire in Flutter

Making a Solitaire clone in Flutter (without a game engine)

After creating minesweeper in Flutter, we will now create another classic game in Flutter: Soltaire. Again, this uses no game engine and is pure Flutter code. If you already know how Solitaire works and are good at Flutter, skip to the end of the article for the Github link.

What is Solitaire?

Solitaire is a card game where the final objective is to enter all the cards into the four suit decks (top right decks) in order Ace -> King. At the beginning, we have seven columns of cards, all of which have one to seven cards respectively. The remaining cards are in a deck in the top left corner.

The card columns need to have cards in alternating colors (red,black) and need to be in order. We can drag one or multiple cards from one column to the other.

Fun fact: Solitaire was actually introduced in Windows to get people accustomed to new gestures like dragging with a mouse.

Getting Started

Our final aim is going to be this:

Let’s see the list of things we need to do:

Create a basic card model Create 52 cards, split across columns randomly and put the rest back into the deck Creating a card widget Create a card column widget Create seven card columns Create four suit decks Create remaining cards deck

This article will be a bit more Flutter code heavy than the minesweeper article since here the logic is simpler but the components are slightly more complex.

Let’s code

We’ll start with the making a card.

Creating a basic card model

First let’s create a basic data model for the card.

A playing card has a suit (diamonds/hearts/spades/clubs) and a type (1,2,3,4…J,Q,K). Along with this we’ll add two other variables to store if this card is upside down and if this card has been opened before.

The model has three enums to store card type, suit and color. It also has a simple getter for getting the color of the card (red/black).

Create cards and randomly put them into card columns

First, we initialise all the lists that hold our cards.

// Stores the cards on the seven columns

List<PlayingCard> cardColumn1 = [];

List<PlayingCard> cardColumn2 = [];

List<PlayingCard> cardColumn3 = [];

List<PlayingCard> cardColumn4 = [];

List<PlayingCard> cardColumn5 = [];

List<PlayingCard> cardColumn6 = [];

List<PlayingCard> cardColumn7 = []; // Stores the remaining card deck

List<PlayingCard> cardDeckClosed = [];

List<PlayingCard> cardDeckOpened = []; // Stores the card in the final suit decks

List<PlayingCard> finalHeartsDeck = [];

List<PlayingCard> finalDiamondsDeck = [];

List<PlayingCard> finalSpadesDeck = [];

List<PlayingCard> finalClubsDeck = [];

(You can also make a List<List<PlayingCard>> here)

Now, we initialise all 52 cards:

List<PlayingCard> allCards = []; // Add all cards to deck

CardSuit.values.forEach((suit) {

CardType.values.forEach((type) {

allCards.add(PlayingCard(

cardType: type,

cardSuit: suit,

faceUp: false,

));

});

});

Now, when the game starts, column 1 has 1 card, column 2 has 2 cards and so on. We have a total of seven columns. So we random 28 cards in total and add them to respective lists. We also need to open and flip the last cards in every column.

Random random = Random(); // Add cards to columns and remaining to deck

for (int i = 0; i < 28; i++) {

int randomNumber = random.nextInt(allCards.length); if (i == 0) {

PlayingCard card = allCards[randomNumber];

cardColumn1.add(

card

..opened = true

..faceUp = true,

);

allCards.removeAt(randomNumber);

} else if (i > 0 && i < 3) {

if (i == 2) {

PlayingCard card = allCards[randomNumber];

cardColumn2.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn2.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

} else if (i > 2 && i < 6) {

if (i == 5) {

PlayingCard card = allCards[randomNumber];

cardColumn3.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn3.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

} else if (i > 5 && i < 10) {

if (i == 9) {

PlayingCard card = allCards[randomNumber];

cardColumn4.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn4.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

} else if (i > 9 && i < 15) {

if (i == 14) {

PlayingCard card = allCards[randomNumber];

cardColumn5.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn5.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

} else if (i > 14 && i < 21) {

if (i == 20) {

PlayingCard card = allCards[randomNumber];

cardColumn6.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn6.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

} else {

if (i == 27) {

PlayingCard card = allCards[randomNumber];

cardColumn7.add(

card

..opened = true

..faceUp = true,

);

} else {

cardColumn7.add(allCards[randomNumber]);

}

allCards.removeAt(randomNumber);

}

}

Then we simply add the remaining cards in allCards to a remaining cards list. We also open the top card and add it to openedCards , which the user can drag into the card columns.

cardDeckClosed = allCards;

cardDeckOpened.add(

cardDeckClosed.removeLast()

..opened = true

..faceUp = true,

); setState(() {});

Creating a card column

We’ll momentarily break out of order here and study the card column before we study the card widget itself.

A card column is a stack of cards with each card translated more in the Y direction than the last. If there was no translation, we would have all cards on top of each other.

A card column must also be able to accept other cards that are dragged to it. So, we need to use a DragTarget as well. If you are unfamiliar with Draggables and DragTargets, check out my deep dive on them here.

The parameters we accept are:

// List of cards in the stack

final List<PlayingCard> cards;



// Callback when card is added to the stack

final CardAcceptCallback onCardsAdded;



// The index of the list in the game

final int columnIndex;

onCardAdded is a callback to run a function when a card is dragged from one list to the other. columnIndex notes the index of the column in the seven columns we create. This is useful to note since we need the information of where a dragged card came from and went.

From the above information, can now conclude that this needs to be a Stack surrounded by a DragTarget :

DragTarget<Map>(

builder: (context, listOne, listTwo) {

return Stack(

children: widget.cards.map((card) {

int index = widget.cards.indexOf(card);

return TransformedCard(

playingCard: card,

transformIndex: index,

attachedCards: widget.cards.sublist(index, widget.cards.length),

columnIndex: widget.columnIndex,

);

}).toList(),

);

},

),

As we said, a CardColumn should be able to accept dragged cards, but only if they match the conditions of the game, namely, opposite color and in order. We encode the logic in the onWillAccept function of our DragTarget .

onWillAccept: (value) {

// If empty, accept

if (widget.cards.length == 0) {

return true;

}



// Get dragged cards list

List<PlayingCard> draggedCards = value["cards"];

PlayingCard firstCard = draggedCards.first;

if (firstCard.cardColor == CardColor.red) {

if (widget.cards.last.cardColor == CardColor.red) {

return false;

}



int lastColumnCardIndex = CardType.values.indexOf(widget.cards.last.cardType);

int firstDraggedCardIndex = CardType.values.indexOf(firstCard.cardType);



if(lastColumnCardIndex != firstDraggedCardIndex + 1) {

return false;

}



} else {

if (widget.cards.last.cardColor == CardColor.black) {

return false;

}



int lastColumnCardIndex = CardType.values.indexOf(widget.cards.last.cardType);

int firstDraggedCardIndex = CardType.values.indexOf(firstCard.cardType);



if(lastColumnCardIndex != firstDraggedCardIndex + 1) {

return false;

}



}

return true;

},

When a card is accepted, the callback is invoked. As we will soon see, with the cards dragged, we pass the data of the cards that were dragged as well as the column they came from.

onAccept: (value) {

widget.onCardsAdded(

value["cards"],

value["fromIndex"],

);

},

TransformedCard is the name of the card widget we will study next.

Creating a card widget

We need our card to:

Take a PlayingCard model and display card accordingly Be draggable If dragged, carry all the cards below it with it as attached cards Translate in the Y axis depending on the position of the card in the card column (You can implement this behaviour in the card column but I’ve chosen to do it in the card itself).

In the parameters, we take:

// The card model to display

final PlayingCard playingCard; // The distance to translate the card in the Y axis (default: 15.0)

final double transformDistance; // The index of the card in the card column

final int transformIndex; // The index of the column in the seven card columns in the game.

final int columnIndex; // Cards below the current card in the card column

final List<PlayingCard> attachedCards;

Like we discussed in the CardColumn section, we need to translate our cards in the Y direction based on their index in the list.

So first, we have a Transform widget as the outermost widget when building a card.

return Transform(

transform: Matrix4.identity()

..translate(

0.0,

widget.transformIndex * widget.transformDistance,

0.0,

),

child: _buildCard(),

);

Now we need to build the card itself, which has two cases:

Card is facing down

If the card is facing downwards, we don’t need to make it draggable and we can just make a simple card back side.

Container(

height: 60.0,

width: 40.0,

decoration: BoxDecoration(

color: Colors.blue,

border: Border.all(color: Colors.black),

borderRadius: BorderRadius.circular(8.0),

),

)

2. Card is facing upward

This is the harder case. We need to:

Display the playing card passed. Make it draggable (And attach all cards below the card if any). Attach all data so that another DragTarget can receive the card.

We need to make the outer widget a Draggable :

Draggable<Map>(

child: _buildFaceUpCard(),

feedback: CardColumn(

cards: widget.attachedCards,

),

childWhenDragging: _buildFaceUpCard(),

data: {

"cards": widget.attachedCards,

"fromIndex": widget.columnIndex,

},

);

feedback is the widget displayed when you drag the cards, which is another card column since if a card in the middle is dragged, then all cards below are also dragged along with it. So we construct a smaller CardColumn out of the card + the cards below it. (Yes, the CardColumn has cards which each have CardColumn s as their feedbacks which each have cards. It’s like CardColumn-ception… without the Stack Overflow.)

Creating seven card columns

We simply use Expanded in a row for adding the card columns.

Create four suit decks

Since we can also drag cards to the suit decks, we need to use a DragTarget again, albeit with slightly different logic since the cards need to be accepted in the reverse order compared to the CardColumn .

// The suit of the deck

final CardSuit cardSuit; // The cards added to the deck

final List<PlayingCard> cardsAdded; // Callback when card is added

final CardAcceptCallback onCardAdded;

We don’t need a deck of cards, we can simply show the topmost card. If it is empty, we construct an empty card.

Again, this is a DragTarget with a TransformedCard or an empty placeholder inside it.

However, the accept logic ( onWillAccept ) is different since here we accept a 2 after a 1, whereas in the CardColumn , we did the opposite.

Creating remaining cards deck

The remaining cards consist of two lists, remaining cards which are opened and remaining cards which are closed. We simply shift cards between two lists and show the topmost. We use an InkWell around the left card so that when the card is tapped, we move the top card from the closed list to the open list. The right card also needs to be draggable.

Handling card drags

Once a card or multiple cards are dragged from one CardColumn to the other, we need to add the cards to the list of card for that CardColumn and delete it from the other.

onCardsAdded: (cards, index) {

setState(() {

(newColumnName).addAll(cards);

int length = oldColumnName.length;

oldColumnName

.removeRange(length - cards.length, length);

_refreshList(index);

});

},

Handling win condition

After every drag, we simply check if the addition of the cards in the final suit decks is 52. If it is, we declare a win.

if (finalDiamondsDeck.length +

finalHeartsDeck.length +

finalClubsDeck.length +

finalSpadesDeck.length ==

52) {

_handleWin();

}

And finally, our result is:

For the complete code:

That’s it for this article! I hope you enjoyed it, and leave a few claps if you did. Follow me for more Flutter articles and comment for any feedback you might have about this article.

Some of my other articles:

Feel free to check out my other profiles as well: