Stunning Solitaire clone made in Flutter
The best way to learn is to do. Making a clone of a preexisting games is a great way to sharpen you flutter skills. Today we are going to take a look at how to make a Solitaire clone flutter app. We are going to add the required dependencies, import libraries and code the guts of our app.
solitaire_flutter
A Solitaire clone made in Flutter.
Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter, view our online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.
Example
- Create a flutter app, using flutter command: flutter create app solitaire. Here solitaire being the name of the app.
- In the lib folder is where we store all our .dart codes.
- Create files main.dart, card_column.dart, empty_card.dart, game_screen.dart, playing_card.dart and transformed_card.dart.
- main.dart
import 'package:flutter/material.dart';
import 'package:solitaire_flutter/game_screen.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: GameScreen(),
);
}
}
- card_column.dart
import 'package:flutter/material.dart';
import 'package:solitaire_flutter/playing_card.dart';
import 'package:solitaire_flutter/transformed_card.dart';
typedef Null CardAcceptCallback(List<PlayingCard> card, int fromIndex);
// This is a stack of overlayed cards (implemented using a stack)
class CardColumn extends StatefulWidget {
// 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;
CardColumn(
{@required this.cards,
@required this.onCardsAdded,
@required this.columnIndex});
@override
_CardColumnState createState() => _CardColumnState();
}
class _CardColumnState extends State<CardColumn> {
@override
Widget build(BuildContext context) {
return Container(
//alignment: Alignment.topCenter,
height: 13.0 * 15.0,
width: 70.0,
margin: EdgeInsets.all(2.0),
child: 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(),
);
},
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;
},
onAccept: (value) {
widget.onCardsAdded(
value["cards"],
value["fromIndex"],
);
},
),
);
}
}
- empty_card.dart
import 'package:flutter/material.dart';
import 'package:solitaire_flutter/card_column.dart';
import 'package:solitaire_flutter/playing_card.dart';
import 'package:solitaire_flutter/transformed_card.dart';
// The deck of cards which accept the final cards (Ace to King)
class EmptyCardDeck extends StatefulWidget {
final CardSuit cardSuit;
final List<PlayingCard> cardsAdded;
final CardAcceptCallback onCardAdded;
final int columnIndex;
EmptyCardDeck({
@required this.cardSuit,
@required this.cardsAdded,
@required this.onCardAdded,
this.columnIndex,
});
@override
_EmptyCardDeckState createState() => _EmptyCardDeckState();
}
class _EmptyCardDeckState extends State<EmptyCardDeck> {
@override
Widget build(BuildContext context) {
return DragTarget<Map>(
builder: (context, listOne, listTwo) {
return widget.cardsAdded.length == 0
? Opacity(
opacity: 0.7,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: Colors.white,
),
height: 60.0,
width: 40,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Center(
child: Container(
height: 20.0,
child: _suitToImage(),
),
)
],
),
),
),
)
: TransformedCard(
playingCard: widget.cardsAdded.last,
columnIndex: widget.columnIndex,
attachedCards: [
widget.cardsAdded.last,
],
);
},
onWillAccept: (value) {
PlayingCard cardAdded = value["cards"].last;
if (cardAdded.cardSuit == widget.cardSuit) {
if (CardType.values.indexOf(cardAdded.cardType) ==
widget.cardsAdded.length) {
return true;
}
}
return false;
},
onAccept: (value) {
widget.onCardAdded(
value["cards"],
value["fromIndex"],
);
},
);
}
Image _suitToImage() {
switch (widget.cardSuit) {
case CardSuit.hearts:
return Image.asset('images/hearts.png');
case CardSuit.diamonds:
return Image.asset('images/diamonds.png');
case CardSuit.clubs:
return Image.asset('images/clubs.png');
case CardSuit.spades:
return Image.asset('images/spades.png');
default:
return null;
}
}
}
- game_screen.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:solitaire_flutter/card_column.dart';
import 'package:solitaire_flutter/empty_card.dart';
import 'package:solitaire_flutter/playing_card.dart';
import 'package:solitaire_flutter/transformed_card.dart';
class GameScreen extends StatefulWidget {
@override
_GameScreenState createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
// 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 card deck
List<PlayingCard> cardDeckClosed = [];
List<PlayingCard> cardDeckOpened = [];
// Stores the card in the upper boxes
List<PlayingCard> finalHeartsDeck = [];
List<PlayingCard> finalDiamondsDeck = [];
List<PlayingCard> finalSpadesDeck = [];
List<PlayingCard> finalClubsDeck = [];
@override
void initState() {
super.initState();
_initialiseGame();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
appBar: AppBar(
title: Text("Flutter Solitaire"),
elevation: 0.0,
backgroundColor: Colors.green,
actions: <Widget>[
InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.refresh,
color: Colors.white,
),
),
splashColor: Colors.white,
onTap: () {
_initialiseGame();
},
)
],
),
body: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildCardDeck(),
_buildFinalDecks(),
],
),
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: CardColumn(
cards: cardColumn1,
onCardsAdded: (cards, index) {
setState(() {
cardColumn1.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 1,
),
),
Expanded(
child: CardColumn(
cards: cardColumn2,
onCardsAdded: (cards, index) {
setState(() {
cardColumn2.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 2,
),
),
Expanded(
child: CardColumn(
cards: cardColumn3,
onCardsAdded: (cards, index) {
setState(() {
cardColumn3.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 3,
),
),
Expanded(
child: CardColumn(
cards: cardColumn4,
onCardsAdded: (cards, index) {
setState(() {
cardColumn4.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 4,
),
),
Expanded(
child: CardColumn(
cards: cardColumn5,
onCardsAdded: (cards, index) {
setState(() {
cardColumn5.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 5,
),
),
Expanded(
child: CardColumn(
cards: cardColumn6,
onCardsAdded: (cards, index) {
setState(() {
cardColumn6.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 6,
),
),
Expanded(
child: CardColumn(
cards: cardColumn7,
onCardsAdded: (cards, index) {
setState(() {
cardColumn7.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
});
},
columnIndex: 7,
),
),
],
),
],
),
);
}
// Build the deck of cards left after building card columns
Widget _buildCardDeck() {
return Container(
child: Row(
children: <Widget>[
InkWell(
child: cardDeckClosed.isNotEmpty
? Padding(
padding: const EdgeInsets.all(4.0),
child: TransformedCard(
playingCard: cardDeckClosed.last,
),
)
: Opacity(
opacity: 0.4,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: TransformedCard(
playingCard: PlayingCard(
cardSuit: CardSuit.diamonds,
cardType: CardType.five,
),
),
),
),
onTap: () {
setState(() {
if (cardDeckClosed.isEmpty) {
cardDeckClosed.addAll(cardDeckOpened.map((card) {
return card
..opened = false
..faceUp = false;
}));
cardDeckOpened.clear();
} else {
cardDeckOpened.add(
cardDeckClosed.removeLast()
..faceUp = true
..opened = true,
);
}
});
},
),
cardDeckOpened.isNotEmpty
? Padding(
padding: const EdgeInsets.all(4.0),
child: TransformedCard(
playingCard: cardDeckOpened.last,
attachedCards: [
cardDeckOpened.last,
],
columnIndex: 0,
),
)
: Container(
width: 40.0,
),
],
),
);
}
// Build the final decks of cards
Widget _buildFinalDecks() {
return Container(
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(4.0),
child: EmptyCardDeck(
cardSuit: CardSuit.hearts,
cardsAdded: finalHeartsDeck,
onCardAdded: (cards, index) {
finalHeartsDeck.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
},
columnIndex: 8,
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: EmptyCardDeck(
cardSuit: CardSuit.diamonds,
cardsAdded: finalDiamondsDeck,
onCardAdded: (cards, index) {
finalDiamondsDeck.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
},
columnIndex: 9,
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: EmptyCardDeck(
cardSuit: CardSuit.spades,
cardsAdded: finalSpadesDeck,
onCardAdded: (cards, index) {
finalSpadesDeck.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
},
columnIndex: 10,
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: EmptyCardDeck(
cardSuit: CardSuit.clubs,
cardsAdded: finalClubsDeck,
onCardAdded: (cards, index) {
finalClubsDeck.addAll(cards);
int length = _getListFromIndex(index).length;
_getListFromIndex(index)
.removeRange(length - cards.length, length);
_refreshList(index);
},
columnIndex: 11,
),
),
],
),
);
}
// Initialise a new game
void _initialiseGame() {
cardColumn1 = [];
cardColumn2 = [];
cardColumn3 = [];
cardColumn4 = [];
cardColumn5 = [];
cardColumn6 = [];
cardColumn7 = [];
// Stores the card deck
cardDeckClosed = [];
cardDeckOpened = [];
// Stores the card in the upper boxes
finalHeartsDeck = [];
finalDiamondsDeck = [];
finalSpadesDeck = [];
finalClubsDeck = [];
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,
));
});
});
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);
}
}
cardDeckClosed = allCards;
cardDeckOpened.add(
cardDeckClosed.removeLast()
..opened = true
..faceUp = true,
);
setState(() {});
}
void _refreshList(int index) {
if (finalDiamondsDeck.length +
finalHeartsDeck.length +
finalClubsDeck.length +
finalSpadesDeck.length ==
52) {
_handleWin();
}
setState(() {
if (_getListFromIndex(index).length != 0) {
_getListFromIndex(index)[_getListFromIndex(index).length - 1]
..opened = true
..faceUp = true;
}
});
}
// Handle a win condition
void _handleWin() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Congratulations!"),
content: Text("You Win!"),
actions: <Widget>[
FlatButton(
onPressed: () {
_initialiseGame();
Navigator.pop(context);
},
child: Text("Play again"),
),
],
);
},
);
}
List<PlayingCard> _getListFromIndex(int index) {
switch (index) {
case 0:
return cardDeckOpened;
case 1:
return cardColumn1;
case 2:
return cardColumn2;
case 3:
return cardColumn3;
case 4:
return cardColumn4;
case 5:
return cardColumn5;
case 6:
return cardColumn6;
case 7:
return cardColumn7;
case 8:
return finalHeartsDeck;
case 9:
return finalDiamondsDeck;
case 10:
return finalSpadesDeck;
case 11:
return finalClubsDeck;
default:
return null;
}
}
}
- playing_card.dart
import 'package:flutter/material.dart';
enum CardSuit {
spades,
hearts,
diamonds,
clubs,
}
enum CardType {
one,
two,
three,
four,
five,
six,
seven,
eight,
nine,
ten,
jack,
queen,
king
}
enum CardColor {
red,
black,
}
// Simple playing card model
class PlayingCard {
CardSuit cardSuit;
CardType cardType;
bool faceUp;
bool opened;
PlayingCard({
@required this.cardSuit,
@required this.cardType,
this.faceUp = false,
this.opened = false,
});
CardColor get cardColor {
if(cardSuit == CardSuit.hearts || cardSuit == CardSuit.diamonds) {
return CardColor.red;
} else {
return CardColor.black;
}
}
}
- transformed_card.dart
import 'package:flutter/material.dart';
import 'package:solitaire_flutter/card_column.dart';
import 'package:solitaire_flutter/playing_card.dart';
// TransformedCard makes the card draggable and translates it according to
// position in the stack.
class TransformedCard extends StatefulWidget {
final PlayingCard playingCard;
final double transformDistance;
final int transformIndex;
final int columnIndex;
final List<PlayingCard> attachedCards;
TransformedCard({
@required this.playingCard,
this.transformDistance = 15.0,
this.transformIndex = 0,
this.columnIndex,
this.attachedCards,
});
@override
_TransformedCardState createState() => _TransformedCardState();
}
class _TransformedCardState extends State<TransformedCard> {
@override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.identity()
..translate(
0.0,
widget.transformIndex * widget.transformDistance,
0.0,
),
child: _buildCard(),
);
}
Widget _buildCard() {
return !widget.playingCard.faceUp
? Container(
height: 60.0,
width: 40.0,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8.0),
),
)
: Draggable<Map>(
child: _buildFaceUpCard(),
feedback: CardColumn(
cards: widget.attachedCards,
columnIndex: 1,
onCardsAdded: (card, position) {},
),
childWhenDragging: _buildFaceUpCard(),
data: {
"cards": widget.attachedCards,
"fromIndex": widget.columnIndex,
},
);
}
Widget _buildFaceUpCard() {
return Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: Colors.white,
border: Border.all(color: Colors.black),
),
height: 60.0,
width: 40,
child: Stack(
children: <Widget>[
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Center(
child: Text(
_cardTypeToString(),
style: TextStyle(
fontSize: 16.0,
),
),
),
Container(
height: 20.0,
child: _suitToImage(),
)
],
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: Align(
alignment: Alignment.topRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
_cardTypeToString(),
style: TextStyle(
fontSize: 10.0,
),
),
Container(
height: 10.0,
child: _suitToImage(),
)
],
),
),
),
],
),
),
);
}
String _cardTypeToString() {
switch (widget.playingCard.cardType) {
case CardType.one:
return "1";
case CardType.two:
return "2";
case CardType.three:
return "3";
case CardType.four:
return "4";
case CardType.five:
return "5";
case CardType.six:
return "6";
case CardType.seven:
return "7";
case CardType.eight:
return "8";
case CardType.nine:
return "9";
case CardType.ten:
return "10";
case CardType.jack:
return "J";
case CardType.queen:
return "Q";
case CardType.king:
return "K";
default:
return "";
}
}
Image _suitToImage() {
switch (widget.playingCard.cardSuit) {
case CardSuit.hearts:
return Image.asset('images/hearts.png');
case CardSuit.diamonds:
return Image.asset('images/diamonds.png');
case CardSuit.clubs:
return Image.asset('images/clubs.png');
case CardSuit.spades:
return Image.asset('images/spades.png');
default:
return null;
}
}
}
GitHub
Source Code: Stunning Solitaire clone made in Flutter.