3. Cards¶
In this chapter we will begin implementing the most visible component in the
game – the Card component, which corresponds to a single real-life card.
There will be 52 Card
objects in the game.
Each card has a rank (from 1 to 13, where 1 is an Ace, and 13 is a King) and a suit (from 0 to 3: hearts ♥, diamonds ♦, clubs ♣, and spades ♠). Also, each card will have a boolean flag faceUp, which controls whether the card is currently facing up or down. This property is important both for rendering, and for certain aspects of the gameplay logic.
The rank and the suit are simple properties of a card, they aren’t components,
so we need to make a decision on how to represent them. There are several
possibilities: either as a simple int
, or as an enum
, or as objects. The
choice will depend on what operations we need to perform with them. For the
rank, we will need to be able to tell whether one rank is one higher/lower than
another rank. Also, we need to produce the text label and a sprite corresponding
to the given rank. For suits, we need to know whether two suits are of different
colors, and also produce a text label and a sprite. Given these requirements,
I decided to represent both Rank
and Suit
as classes.
Suit¶
Create file suit.dart
and declare an @immutable class Suit
there, with no
parent. The @immutable
annotation here is just a hint for us that the objects
of this class should not be modified after creation.
Next, we define the factory constructor for the class: Suit.fromInt(i)
. We
use a factory constructor here in order to enforce the singleton pattern for
the class: instead of creating a new object every time, we are returning one
of the pre-built objects that we store in the _singletons
list:
factory Suit.fromInt(int index) {
assert(index >= 0 && index <= 3);
return _singletons[index];
}
After that, there is a private constructor Suit._()
. This constructor
initializes the main properties of each Suit
object: the numeric value, the
string label, and the sprite object which we will later use to draw the suit
symbol on the canvas. The sprite object is initialized using the
klondikeSprite()
function that we created in the previous chapter:
Suit._(this.value, this.label, double x, double y, double w, double h)
: sprite = klondikeSprite(x, y, w, h);
final int value;
final String label;
final Sprite sprite;
Then comes the static list of all Suit
objects in the game. Note that we
define it as static variable so it is evaluated lazily (as if it was marked
with the late
keyword) meaning that it will be only initialized the first
time it is needed. This is important: as we can see above, the constructor
tries to retrieve an image from the global cache, so it can only be invoked
after the image is loaded into the cache.
static final List<Suit> _singletons = [
Suit._(0, '♥', 1176, 17, 172, 183),
Suit._(1, '♦', 973, 14, 177, 182),
Suit._(2, '♣', 974, 226, 184, 172),
Suit._(3, '♠', 1178, 220, 176, 182),
];
The last four numbers in the constructor are the coordinates of the sprite
image within the sprite sheet klondike-sprites.png
. If you’re wondering how I
obtained these numbers, the answer is that I used a free online service
spritecow.com – it’s a handy tool for locating sprites within a sprite sheet.
Lastly, I have simple getters to determine the “color” of a suit. This will be needed later when we need to enforce the rule that cards can only be placed into columns by alternating colors.
/// Hearts and Diamonds are red, while Clubs and Spades are black.
bool get isRed => value <= 1;
bool get isBlack => value >= 2;
Rank¶
The Rank
class is very similar to Suit
. The main difference is that Rank
contains two sprites instead of one, separately for ranks of “red” and “black”
colors. The full code for the Rank
class is as follows:
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flutter/foundation.dart';
@immutable
class Rank {
factory Rank.fromInt(int value) {
assert(value >= 1 && value <= 13);
return _singletons[value - 1];
}
Rank._(
this.value,
this.label,
double x1,
double y1,
double x2,
double y2,
double w,
double h,
) : redSprite = klondikeSprite(x1, y1, w, h),
blackSprite = klondikeSprite(x2, y2, w, h);
final int value;
final String label;
final Sprite redSprite;
final Sprite blackSprite;
static final List<Rank> _singletons = [
Rank._(1, 'A', 335, 164, 789, 161, 120, 129),
Rank._(2, '2', 20, 19, 15, 322, 83, 125),
Rank._(3, '3', 122, 19, 117, 322, 80, 127),
Rank._(4, '4', 213, 12, 208, 315, 93, 132),
Rank._(5, '5', 314, 21, 309, 324, 85, 125),
Rank._(6, '6', 419, 17, 414, 320, 84, 129),
Rank._(7, '7', 509, 21, 505, 324, 92, 128),
Rank._(8, '8', 612, 19, 607, 322, 78, 127),
Rank._(9, '9', 709, 19, 704, 322, 84, 130),
Rank._(10, '10', 810, 20, 805, 322, 137, 127),
Rank._(11, 'J', 15, 170, 469, 167, 56, 126),
Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),
Rank._(13, 'K', 243, 170, 696, 167, 92, 123),
];
}
Card component¶
Now that we have the Rank
and the Suit
classes, we can finally start
implementing the Card component. Create file components/card.dart
and
declare the Card
class extending from the PositionComponent
:
class Card extends PositionComponent {}
The constructor of the class will take integer rank and suit, and make the
card initially facing down. Also, we initialize the size of the component to
be equal to the cardSize
constant defined in the KlondikeGame
class:
Card(int intRank, int intSuit)
: rank = Rank.fromInt(intRank),
suit = Suit.fromInt(intSuit),
_faceUp = false,
super(size: KlondikeGame.cardSize);
final Rank rank;
final Suit suit;
bool _faceUp;
The _faceUp
property is private (indicated by the underscore) and non-final,
meaning that it can change during the lifetime of a card. We should create some
public accessors and mutators for this variable:
bool get isFaceUp => _faceUp;
bool get isFaceDown => !_faceUp;
void flip() => _faceUp = !_faceUp;
Lastly, let’s add a simple toString()
implementation, which may turn out to
be useful when we need to debug the game:
@override
String toString() => rank.label + suit.label; // e.g. "Q♠" or "10♦"
Before we proceed with implementing the rendering, we need to add some cards
into the game. Head over to the KlondikeGame
class and add the following at
the bottom of the onLoad
method:
final random = Random();
for (var i = 0; i < 7; i++) {
for (var j = 0; j < 4; j++) {
final card = Card(random.nextInt(13) + 1, random.nextInt(4))
..position = Vector2(100 + i * 1150, 100 + j * 1500)
..addToParent(world);
if (random.nextDouble() < 0.9) { // flip face up with 90% probability
card.flip();
}
}
}
This snippet is a temporary code – we will remove it in the next chapter – but for now it lays down 28 random cards on the table, most of them facing up.
Rendering¶
In order to be able to see a card, we need to implement its render()
method.
Since the card has two distinct states – face up or down – we will
implement rendering for these two states separately. Add the following methods
into the Card
class:
@override
void render(Canvas canvas) {
if (_faceUp) {
_renderFront(canvas);
} else {
_renderBack(canvas);
}
}
void _renderFront(Canvas canvas) {}
void _renderBack(Canvas canvas) {}
renderBack()¶
Since rendering the back of a card is simpler, we will do it first.
The render()
method of a PositionComponent
operates in a local coordinate
system, which means we don’t need to worry about where the card is located on
the screen. This local coordinate system has the origin at the top-left corner
of the component, and extends to the right by width
and down by height
pixels.
There is a lot of artistic freedom in how to draw the back of a card, but my implementation contains a solid background, a border, a flame logo in the middle, and another decorative border:
void _renderBack(Canvas canvas) {
canvas.drawRRect(cardRRect, backBackgroundPaint);
canvas.drawRRect(cardRRect, backBorderPaint1);
canvas.drawRRect(backRRectInner, backBorderPaint2);
flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);
}
The most interesting part here is the rendering of a sprite: we want to
render it in the middle (size/2
), and we use Anchor.center
to tell the
engine that we want the center of the sprite to be at that point.
Various properties used in the _renderBack()
method are defined as follows:
static final Paint backBackgroundPaint = Paint()
..color = const Color(0xff380c02);
static final Paint backBorderPaint1 = Paint()
..color = const Color(0xffdbaf58)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint backBorderPaint2 = Paint()
..color = const Color(0x5CEF971B)
..style = PaintingStyle.stroke
..strokeWidth = 35;
static final RRect cardRRect = RRect.fromRectAndRadius(
KlondikeGame.cardSize.toRect(),
const Radius.circular(KlondikeGame.cardRadius),
);
static final RRect backRRectInner = cardRRect.deflate(40);
static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);
I declared these properties as static because they will all be the same across all 52 card objects, so we might as well save some resources by having them initialized only once.
renderFront()¶
When rendering the face of a card, we will follow the standard card design: the rank and the suit in two opposite corners, plus the number of pips equal to the rank value. The court cards (jack, queen, king) will have special images in the center.
As before, we begin by declaring some constants that will be used for rendering. The background of a card will be black, whereas the border will be different depending on whether the card is of a “red” suit or “black”:
static final Paint frontBackgroundPaint = Paint()
..color = const Color(0xff000000);
static final Paint redBorderPaint = Paint()
..color = const Color(0xffece8a3)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint blackBorderPaint = Paint()
..color = const Color(0xff7ab2e8)
..style = PaintingStyle.stroke
..strokeWidth = 10;
Next, we also need the images for the court cards:
static final Sprite redJack = klondikeSprite(81, 565, 562, 488);
static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);
static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);
Note that I’m calling these sprites redJack
, redQueen
, and redKing
. This
is because, after some trial, I found that the images that I have don’t look
very well on black-suit cards. So what I decided to do is to take these images
and tint them with a blueish hue. Tinting of a sprite can be achieved by
using a paint with colorFilter
set to the specified color and the srcATop
blending mode:
static final blueFilter = Paint()
..colorFilter = const ColorFilter.mode(
Color(0x880d8bff),
BlendMode.srcATop,
);
static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)
..paint = blueFilter;
static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)
..paint = blueFilter;
static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)
..paint = blueFilter;
Now we can start coding the render method itself. First, draw the background and the card border:
void _renderFront(Canvas canvas) {
canvas.drawRRect(cardRRect, frontBackgroundPaint);
canvas.drawRRect(
cardRRect,
suit.isRed ? redBorderPaint : blackBorderPaint,
);
}
In order to draw the rest of the card, I need one more helper method. This
method will draw the provided sprite on the canvas at the specified place (the
location is relative to the dimensions of the card). The sprite can be
optionally scaled. In addition, if flag rotate=true
is passed, the sprite
will be drawn as if it was rotated 180º around the center of the card:
void _drawSprite(
Canvas canvas,
Sprite sprite,
double relativeX,
double relativeY, {
double scale = 1,
bool rotate = false,
}) {
if (rotate) {
canvas.save();
canvas.translate(size.x / 2, size.y / 2);
canvas.rotate(pi);
canvas.translate(-size.x / 2, -size.y / 2);
}
sprite.render(
canvas,
position: Vector2(relativeX * size.x, relativeY * size.y),
anchor: Anchor.center,
size: sprite.srcSize.scaled(scale),
);
if (rotate) {
canvas.restore();
}
}
Let’s draw the rank and the suit symbols in the corners of the card. Add the
following to the _renderFront()
method:
final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;
final suitSprite = suit.sprite;
_drawSprite(canvas, rankSprite, 0.1, 0.08);
_drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);
The middle of the card is rendered in the same manner: we will create a big switch statement on the card’s rank, and draw pips accordingly. The code below may seem long, but it is actually quite repetitive and consists only of drawing various sprites in different places on the card’s face:
switch (rank.value) {
case 1:
_drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);
case 2:
_drawSprite(canvas, suitSprite, 0.5, 0.25);
_drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);
case 3:
_drawSprite(canvas, suitSprite, 0.5, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
_drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);
case 4:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
case 5:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
case 6:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
case 7:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
case 8:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);
case 9:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
case 10:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
case 11:
_drawSprite(canvas, suit.isRed? redJack : blackJack, 0.5, 0.5);
case 12:
_drawSprite(canvas, suit.isRed? redQueen : blackQueen, 0.5, 0.5);
case 13:
_drawSprite(canvas, suit.isRed? redKing : blackKing, 0.5, 0.5);
}
And this is it with the rendering of the Card
component. If you run the code
now, you would see four rows of cards neatly spread on the table. Refreshing
the page will lay down a new set of cards. Remember that we have laid these
cards in this way only temporarily, in order to be able to check that rendering
works properly.
In the next chapter we will discuss how to implement interactions with the cards, that is, how to make them draggable and tappable.
1import 'dart:math';
2import 'dart:ui';
3
4import 'package:flame/components.dart';
5import '../klondike_game.dart';
6import '../rank.dart';
7import '../suit.dart';
8
9class Card extends PositionComponent {
10 Card(int intRank, int intSuit)
11 : rank = Rank.fromInt(intRank),
12 suit = Suit.fromInt(intSuit),
13 _faceUp = false,
14 super(size: KlondikeGame.cardSize);
15
16 final Rank rank;
17 final Suit suit;
18 bool _faceUp;
19
20 bool get isFaceUp => _faceUp;
21 void flip() => _faceUp = !_faceUp;
22
23 @override
24 String toString() => rank.label + suit.label; // e.g. "Q♠" or "10♦"
25
26 @override
27 void render(Canvas canvas) {
28 if (_faceUp) {
29 _renderFront(canvas);
30 } else {
31 _renderBack(canvas);
32 }
33 }
34
35 static final Paint backBackgroundPaint = Paint()
36 ..color = const Color(0xff380c02);
37 static final Paint backBorderPaint1 = Paint()
38 ..color = const Color(0xffdbaf58)
39 ..style = PaintingStyle.stroke
40 ..strokeWidth = 10;
41 static final Paint backBorderPaint2 = Paint()
42 ..color = const Color(0x5CEF971B)
43 ..style = PaintingStyle.stroke
44 ..strokeWidth = 35;
45 static final RRect cardRRect = RRect.fromRectAndRadius(
46 KlondikeGame.cardSize.toRect(),
47 const Radius.circular(KlondikeGame.cardRadius),
48 );
49 static final RRect backRRectInner = cardRRect.deflate(40);
50 static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);
51
52 void _renderBack(Canvas canvas) {
53 canvas.drawRRect(cardRRect, backBackgroundPaint);
54 canvas.drawRRect(cardRRect, backBorderPaint1);
55 canvas.drawRRect(backRRectInner, backBorderPaint2);
56 flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);
57 }
58
59 static final Paint frontBackgroundPaint = Paint()
60 ..color = const Color(0xff000000);
61 static final Paint redBorderPaint = Paint()
62 ..color = const Color(0xffece8a3)
63 ..style = PaintingStyle.stroke
64 ..strokeWidth = 10;
65 static final Paint blackBorderPaint = Paint()
66 ..color = const Color(0xff7ab2e8)
67 ..style = PaintingStyle.stroke
68 ..strokeWidth = 10;
69 static final blueFilter = Paint()
70 ..colorFilter = const ColorFilter.mode(
71 Color(0x880d8bff),
72 BlendMode.srcATop,
73 );
74 static final Sprite redJack = klondikeSprite(81, 565, 562, 488);
75 static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);
76 static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);
77 static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)
78 ..paint = blueFilter;
79 static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)
80 ..paint = blueFilter;
81 static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)
82 ..paint = blueFilter;
83
84 void _renderFront(Canvas canvas) {
85 canvas.drawRRect(cardRRect, frontBackgroundPaint);
86 canvas.drawRRect(
87 cardRRect,
88 suit.isRed ? redBorderPaint : blackBorderPaint,
89 );
90
91 final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;
92 final suitSprite = suit.sprite;
93 _drawSprite(canvas, rankSprite, 0.1, 0.08);
94 _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);
95 _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);
96 _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);
97 switch (rank.value) {
98 case 1:
99 _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);
100 case 2:
101 _drawSprite(canvas, suitSprite, 0.5, 0.25);
102 _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);
103 case 3:
104 _drawSprite(canvas, suitSprite, 0.5, 0.2);
105 _drawSprite(canvas, suitSprite, 0.5, 0.5);
106 _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);
107 case 4:
108 _drawSprite(canvas, suitSprite, 0.3, 0.25);
109 _drawSprite(canvas, suitSprite, 0.7, 0.25);
110 _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
111 _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
112 case 5:
113 _drawSprite(canvas, suitSprite, 0.3, 0.25);
114 _drawSprite(canvas, suitSprite, 0.7, 0.25);
115 _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
116 _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
117 _drawSprite(canvas, suitSprite, 0.5, 0.5);
118 case 6:
119 _drawSprite(canvas, suitSprite, 0.3, 0.25);
120 _drawSprite(canvas, suitSprite, 0.7, 0.25);
121 _drawSprite(canvas, suitSprite, 0.3, 0.5);
122 _drawSprite(canvas, suitSprite, 0.7, 0.5);
123 _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
124 _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
125 case 7:
126 _drawSprite(canvas, suitSprite, 0.3, 0.2);
127 _drawSprite(canvas, suitSprite, 0.7, 0.2);
128 _drawSprite(canvas, suitSprite, 0.5, 0.35);
129 _drawSprite(canvas, suitSprite, 0.3, 0.5);
130 _drawSprite(canvas, suitSprite, 0.7, 0.5);
131 _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
132 _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
133 case 8:
134 _drawSprite(canvas, suitSprite, 0.3, 0.2);
135 _drawSprite(canvas, suitSprite, 0.7, 0.2);
136 _drawSprite(canvas, suitSprite, 0.5, 0.35);
137 _drawSprite(canvas, suitSprite, 0.3, 0.5);
138 _drawSprite(canvas, suitSprite, 0.7, 0.5);
139 _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
140 _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
141 _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);
142 case 9:
143 _drawSprite(canvas, suitSprite, 0.3, 0.2);
144 _drawSprite(canvas, suitSprite, 0.7, 0.2);
145 _drawSprite(canvas, suitSprite, 0.5, 0.3);
146 _drawSprite(canvas, suitSprite, 0.3, 0.4);
147 _drawSprite(canvas, suitSprite, 0.7, 0.4);
148 _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
149 _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
150 _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
151 _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
152 case 10:
153 _drawSprite(canvas, suitSprite, 0.3, 0.2);
154 _drawSprite(canvas, suitSprite, 0.7, 0.2);
155 _drawSprite(canvas, suitSprite, 0.5, 0.3);
156 _drawSprite(canvas, suitSprite, 0.3, 0.4);
157 _drawSprite(canvas, suitSprite, 0.7, 0.4);
158 _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
159 _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
160 _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);
161 _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
162 _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
163 case 11:
164 _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);
165 case 12:
166 _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);
167 case 13:
168 _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);
169 }
170 }
171
172 void _drawSprite(
173 Canvas canvas,
174 Sprite sprite,
175 double relativeX,
176 double relativeY, {
177 double scale = 1,
178 bool rotate = false,
179 }) {
180 if (rotate) {
181 canvas.save();
182 canvas.translate(size.x / 2, size.y / 2);
183 canvas.rotate(pi);
184 canvas.translate(-size.x / 2, -size.y / 2);
185 }
186 sprite.render(
187 canvas,
188 position: Vector2(relativeX * size.x, relativeY * size.y),
189 anchor: Anchor.center,
190 size: sprite.srcSize.scaled(scale),
191 );
192 if (rotate) {
193 canvas.restore();
194 }
195 }
196}
1import 'package:flame/components.dart';
2
3class Foundation extends PositionComponent {
4 @override
5 bool get debugMode => true;
6}
1import 'package:flame/components.dart';
2
3class Pile extends PositionComponent {
4 @override
5 bool get debugMode => true;
6}
1import 'package:flame/components.dart';
2
3class Stock extends PositionComponent {
4 @override
5 bool get debugMode => true;
6}
1import 'package:flame/components.dart';
2
3class Waste extends PositionComponent {
4 @override
5 bool get debugMode => true;
6}
1import 'dart:math';
2
3import 'package:flame/components.dart';
4import 'package:flame/flame.dart';
5import 'package:flame/game.dart';
6
7import 'components/card.dart';
8import 'components/foundation.dart';
9import 'components/pile.dart';
10import 'components/stock.dart';
11import 'components/waste.dart';
12
13class KlondikeGame extends FlameGame {
14 static const double cardGap = 175.0;
15 static const double cardWidth = 1000.0;
16 static const double cardHeight = 1400.0;
17 static const double cardRadius = 100.0;
18 static final Vector2 cardSize = Vector2(cardWidth, cardHeight);
19
20 @override
21 Future<void> onLoad() async {
22 await Flame.images.load('klondike-sprites.png');
23
24 final stock = Stock()
25 ..size = cardSize
26 ..position = Vector2(cardGap, cardGap);
27 final waste = Waste()
28 ..size = cardSize
29 ..position = Vector2(cardWidth + 2 * cardGap, cardGap);
30 final foundations = List.generate(
31 4,
32 (i) => Foundation()
33 ..size = cardSize
34 ..position =
35 Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
36 );
37 final piles = List.generate(
38 7,
39 (i) => Pile()
40 ..size = cardSize
41 ..position = Vector2(
42 cardGap + i * (cardWidth + cardGap),
43 cardHeight + 2 * cardGap,
44 ),
45 );
46
47 world.add(stock);
48 world.add(waste);
49 world.addAll(foundations);
50 world.addAll(piles);
51
52 camera.viewfinder.visibleGameSize =
53 Vector2(cardWidth * 7 + cardGap * 8, 4 * cardHeight + 3 * cardGap);
54 camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);
55 camera.viewfinder.anchor = Anchor.topCenter;
56
57 final random = Random();
58 for (var i = 0; i < 7; i++) {
59 for (var j = 0; j < 4; j++) {
60 final card = Card(random.nextInt(13) + 1, random.nextInt(4))
61 ..position = Vector2(100 + i * 1150, 100 + j * 1500)
62 ..addToParent(world);
63 // flip the card face-up with 90% probability
64 if (random.nextDouble() < 0.9) {
65 card.flip();
66 }
67 }
68 }
69 }
70}
71
72Sprite klondikeSprite(double x, double y, double width, double height) {
73 return Sprite(
74 Flame.images.fromCache('klondike-sprites.png'),
75 srcPosition: Vector2(x, y),
76 srcSize: Vector2(width, height),
77 );
78}
1import 'package:flame/game.dart';
2import 'package:flutter/widgets.dart';
3
4import 'klondike_game.dart';
5
6void main() {
7 final game = KlondikeGame();
8 runApp(GameWidget(game: game));
9}
1import 'package:flame/components.dart';
2import 'package:flutter/foundation.dart';
3import 'klondike_game.dart';
4
5@immutable
6class Rank {
7 factory Rank.fromInt(int value) {
8 assert(
9 value >= 1 && value <= 13,
10 'value is outside of the bounds of what a rank can be',
11 );
12 return _singletons[value - 1];
13 }
14
15 Rank._(
16 this.value,
17 this.label,
18 double x1,
19 double y1,
20 double x2,
21 double y2,
22 double w,
23 double h,
24 ) : redSprite = klondikeSprite(x1, y1, w, h),
25 blackSprite = klondikeSprite(x2, y2, w, h);
26
27 final int value;
28 final String label;
29 final Sprite redSprite;
30 final Sprite blackSprite;
31
32 static final List<Rank> _singletons = [
33 Rank._(1, 'A', 335, 164, 789, 161, 120, 129),
34 Rank._(2, '2', 20, 19, 15, 322, 83, 125),
35 Rank._(3, '3', 122, 19, 117, 322, 80, 127),
36 Rank._(4, '4', 213, 12, 208, 315, 93, 132),
37 Rank._(5, '5', 314, 21, 309, 324, 85, 125),
38 Rank._(6, '6', 419, 17, 414, 320, 84, 129),
39 Rank._(7, '7', 509, 21, 505, 324, 92, 128),
40 Rank._(8, '8', 612, 19, 607, 322, 78, 127),
41 Rank._(9, '9', 709, 19, 704, 322, 84, 130),
42 Rank._(10, '10', 810, 20, 805, 322, 137, 127),
43 Rank._(11, 'J', 15, 170, 469, 167, 56, 126),
44 Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),
45 Rank._(13, 'K', 243, 170, 696, 167, 92, 123),
46 ];
47}
1import 'package:flame/sprite.dart';
2import 'package:flutter/foundation.dart';
3import 'klondike_game.dart';
4
5@immutable
6class Suit {
7 factory Suit.fromInt(int index) {
8 assert(
9 index >= 0 && index <= 3,
10 'index is outside of the bounds of what a suit can be',
11 );
12 return _singletons[index];
13 }
14
15 Suit._(this.value, this.label, double x, double y, double w, double h)
16 : sprite = klondikeSprite(x, y, w, h);
17
18 final int value;
19 final String label;
20 final Sprite sprite;
21
22 static final List<Suit> _singletons = [
23 Suit._(0, '♥', 1176, 17, 172, 183),
24 Suit._(1, '♦', 973, 14, 177, 182),
25 Suit._(2, '♣', 974, 226, 184, 172),
26 Suit._(3, '♠', 1178, 220, 176, 182),
27 ];
28
29 /// Hearts and Diamonds are red, while Clubs and Spades are black.
30 bool get isRed => value <= 1;
31 bool get isBlack => value >= 2;
32}