Cards Tutorial
Code:
0. Why Cards?
1. Using TomCards
1.1 Drawing a Card
1.2 Drawing the Deck
2. Creating a Deck
2.1 Shuffling a deck
2.2 Creating a Deck Class
3. Lets make Blackjack
3.1 Storing the Displayed Cards
3.2 Dealing Cards
4. Deal Animation
4.1 Creating the Timer
4.2 Drawing the Card
0. Why Cards?
If you read on the internet about successful Independant Game Developers a lot
are creating card and puzzle games. My goal is to make it possible for us to
easily create card games that in the future will allow us to become successful
Indy developers.
1. Using TomCards
A long time ago I downloaded a Card component called TomCards. I have not been
able to find any updates on this component. As its an easy place to start I
thought I'd use this component as a basis for creating card games.
Download TomCards from
http://www.cairnsgames.co.za/cardtut/tomcards.zip
1.1 Drawing a Card
The easiest way to draw a card is to place a Card component on your form. Just
set the various properties to display the card you want.
The problem with placing the card as a component on the form is that you need
to place all the cards you need up front. Often this is rather impracticle. So
lets look at how to dynamically create a card.
[pascal]
// For this example you need a Form with a single button on it. In the example
// the form is Form1 and the button btnMakeCard.
// Make sure TomCard is in your forms uses clause
procedure TForm1.btnMakeCardClick(Sender: TObject);
Var
Card : TCards;
begin
Card := TCards.Create(Self);
Card.Parent := Self;
Card.Left := 100;
Card.Top := 100;
Card.Suit := csHearts;
Card.Value := 9;
end;
[/pascal]
If you run this program and press the Make Card button a 9 of hearts will be
drawn on the screen.
1.2 Drawing the deck
To draw a deck of card we typically only need to display a single card that is
placed face down. TomCards component has a property called State that defines
how a card should be drawn. By default a card is drawn Face up (ctFront) by
setting this property to ctBack the card back is drawn. The default card back
is a plain dreary hatch pattern. To spice it up the CardBackStyle property
should be changed. The valid values are from 1 to 13.
[pascal]
// For this example add an additional button to the form. Call the button
// MakeDeck.
procedure TForm1.btnMakeDeckClick(Sender: TObject);
Var
Card : TCards;
begin
Card := TCards.Create(Self);
Card.Parent := Self;
Card.Left := 100;
Card.Top := 200;
Card.State := ctBack;
Card.CardBackStyle := 7;
end;
[/pascal]
My favorite CardBackStyle is definitly number 7.
TomCards does not declare constants for the various CardBackStyles so it is
mostly trial and error to find what you like.
Of course in any card game we create we would want to allow the person playing
the game to change the card back to whatever pattern they like.
The tutorial to this point is dowloadable as Cards1 from
http://www.cairnsgames.co.za/cardtut/cards1.zip
2. Creating a Deck
Nearly all card games have a completely shuffled deck of cards to start with.
For any game we create we are going to have to store the order of the cards and
show them to the player in the order.
There are many ways of creating a data-structure to store the deck. For me the
easiest way is to create an array.
[pascal]
// First we declare a record to store the card relevant data in
Type
TCardDetail = Record
CardSuit : TCardSuit;
CardValue : Integer;
End;
// Now declare the deck to store the cards
TDeck = Array of TCardDetail;
[/pascal]
Notice that I have not created an upper and lower limit for the Deck. This is to
allow for different size decks, e.g. Decks that include or exclude Jokers.
For now I'm just going to populate the deck with a standard set of cards. For
this example I have extended the previous example. I have created a new public
variable for the form:
[pascal]
public
{ Public declarations }
Deck : TDeck;
Card : TCards;
CardNo : Integer; // Store the current card in the deck
[/pascal]
Then in the forms OnCreate event handler:
[pascal]
procedure TForm1.FormCreate(Sender: TObject);
Var
I : Integer;
begin
// Dynamic arrays need their length set before they can be accessed
SetLength(Deck, 52);
For I := 0 to 51 do
Begin
Case I div 13 of // div by 13 gives a 0-3 result
0: Deck[I].CardSuit := csClubs;
1: Deck[I].CardSuit := csDiamonds;
2: Deck[I].CardSuit := csHearts;
3: Deck[I].CardSuit := csSpades;
End;
Deck[I].CardValue := (I mod 13) + 1; // populates the card value
End;
end;
[/pascal]
Just to display the various cards I changed the Make Card button to just
display the next card each time.
[pascal]
procedure TForm1.btnMakeCardClick(Sender: TObject);
begin
// First check if the Card display has been created or not
If Not Assigned(Card) then
Begin
Card := TCards.Create(Self);
Card.Parent := Self;
Card.Left := 100;
Card.Top := 100;
End;
Card.Suit := Deck[CardNo].CardSuit;
Card.Value := Deck[CardNo].CardValue;
CardNo := CardNo + 1;
end;
[/pascal]
When running this program you will see that as you press the Make Card button
the card steps through the deck in card order.
2.1 Suffling a deck
Most card games use a randomly sorted deck of cards. So at some point we should
randomize the order of the cards in the deck. If you search the internet you
will see that there are multiple shuffling algorithms. As we dont have that many
cards in our deck we can use a simple card swapping routine to shuffle the
deck.
For each card in the deck randomly select another card and switch them. This
garantees that every card is moved at least once in a shuffle. By repeating the
shuffle multiple times we can ensure that the deck is properly randomized.
[pascal]
procedure TForm1.ShuffleDeck(var Deck: TDeck; Times: Integer);
Var
J,I : Integer;
R : Integer;
begin
For J := 1 to Times do
For I := Low(Deck) to High(Deck) do
Begin
R := -1;
Repeat
R := Trunc(Random(High(Deck)+1)) + Low(Deck);
Until R <> I;
SwapCard(Deck[I],Deck[R]);
End;
end;
[/pascal]
I've created a little function called SwapCard that exchanges all the fields
within two TCardDetail variables:
[pascal]
// Swapcard is created so that additional fields can be added to the TCardDetail
// record type without having to modify this function.
procedure TForm1.SwapCard(var Card1, Card2: TCardDetail);
Var
TempCard : TCardDetail;
begin
Move(Card1,TempCard,SizeOf(TCardDetail));
Move(Card2,Card1,SizeOf(TCardDetail));
Move(TempCard,Card2,SizeOf(TCardDetail));
end;
[/pascal]
2.2 Creating a Deck Class
Wouldn't it be nice if we could create a class that manages all the function for
a deck.
The class would need to manage the Deck including populating the cards,
shuffling and returning cards as you need them.
[pascal]
// An example deck class might look something like this
Type
TDeck = Class
private
Cards : Array of TCardDetail;
CardNo : Integer;
Procedure SwapCards(var Card1,Card2 : TCardDetail);
published
Property NumberOfCards : Integer;
Procedure Shuffle(Times : Integer);
Function NextCard : TCardDetail;
End;
[/pascal]
I'm not going to go into the implementation of the class as the TDeck type we
have already declared will be sufficient for most of what we need. I'll possibly
do an implementation of it later.
The tutorial to this point is dowloadable as Cards2.
http://www.cairnsgames.co.za/cardtut/cards2.zip
3. Lets make Blackjack
Ok so now we know how to show and manipulate cards. Lets actually try and make a
simple game using what we have learnt.
Blackjack also known as 21 is a relativly simple game. The goal is to try and
get a score as close to 21 as possible but going over 21 means that you are bust
and out of the game, if betting is involved being bust immediatly loses your
stake. An Ace is worth 1 or 11 points and all face cards (Jack, Queen, King) are
worth 10 point.
For this example I am going to use the structure that we built before. I'm going
to build a small part of the game showing how easy it is to build the
functionality of a game.
3.1 Storing the Displayed Cards
As discussed before it would be possible to place a number of card components
on the form. But as it would not be possible to know how many cards the player
has got this solution isn't really viable. My choice would be to use a TList to
store a ]http://www.cairnsgames.co.za/cardtut/cards3form.gif[/img]
3.2 Dealing Cards
When the DeckOfCards is clicked we want to create the next dealt card for the
player. These cards are displayed next to one another at the bottom of the form.
Rememeber also that we need to increment the current CardNo being dealt so that
the same card does not get dealt time and again.
[pascal]
procedure TForm1.DeckOfCardsClick(Sender: TObject);
Begin
Card := TCards.Create(Self);
Card.Parent := Self;
Card.Left := 150+(100*PlayerCards.Count);
Card.Top := 300;
Card.Suit := Deck[CardNo].CardSuit;
Card.Value := Deck[CardNo].CardValue;
CardNo := CardNo + 1;
PlayerCards.Add(Card);
Score := Score + CardScore[Card.Value];
end;
[/pascal]
As can be seen in the example I'm incrementing a Score property with the values
in CardScore. I've created a constant that represents the various values of
each card.
[pascal]
Const
CardScore : Array[1..13] of Integer = (11,2,3,4,5,6,7,8,9,10,10,10,10);
[/pascal]
The Array index represents the various values stored in the Value property of a
TCards component. Currently I've forced all Aces to be worth 11 points.
Properties are something that I don't see used nearly enough. Properties allow
all sorts of 'automated' functionality. In the following example I'm using a
property to basically control the whole state of the game.
[pascal]
private
FScore: Integer;
procedure SetScore(const Value: Integer);
public
Property Score : Integer read FScore write SetScore;
....
procedure TForm1.SetScore(const Value: Integer);
begin
FScore := Value;
lScore.Caption := IntToStr(Score);
If FScore > 21 then
lBust.Visible := True;
If FScore = 0 then
lBust.Visible := False;
end;
[/pascal]
On the lBust label I have created an event that restarts the game. As you can
only click on the label when it is visible this means that you can only click
the label after you are bust - ie the game is over.
The tutorial to this point is dowloadable as Cards3.
http://www.cairnsgames.co.za/cardtut/cards3.zip
4. Deal Animation
Lots of card games show the card flying off the deck and ending at the location
the card needs to be. This sort of animation is also used when moving cards from
one stack to another.
4.1 Creating the Timer
The easiest way to animate a card is to implement a timer and a timing control
class. The Timing control class stores information about the position of the
card while it is being flown to the new location.
[pascal]
// TFlyCard stores the information for a moving card. This can be used whenever
// you want to automate a cards motion.
// I'd also suggest an event for when the card reaches its destination - this
// would allow you to fly a face down card and turn it face up on reaching its
// destination.
TFlyCard = Class
DestLeft, DestTop : Integer;
Speed : Integer;
Card : TCards;
End;
[/pascal]
Basic trigonometry is needed to calculate how the card needs to move in each
timer event. By using the theorem of pythagorus we can word out how far a card
needs to move in each direction to move straight to its destination.
[pascal]
procedure TForm1.Timer1Timer(Sender: TObject);
Var
Fly : TFlyCard;
DX,DY,DirX,DirY : Integer;
DH : Single;
I : Integer;
begin
If FlyingCards.Count = 0 then
Exit;
For I := FlyingCards.Count - 1 downto 0 do
Begin
Fly := TFlyCard(FlyingCards[I]);
DX := ABS(Fly.Card.Left-Fly.DestLeft);
DY := ABS(Fly.Card.Top-Fly.DestTop);
// Calculate the distance of the hypotenuse
DH := SQRT(DX*DX+DY*DY);
If DH < 1 then DH := 1;
// Move the card
If Fly.Card.Top-Fly.DestTop > 0 then
DirY := Trunc((DY/DH) * -Fly.Speed)
else
DirY := Trunc((DY/DH) * Fly.Speed);
If Fly.Card.Left-Fly.DestLeft > 0 then
DirX := Trunc((DX/DH) * -Fly.Speed)
Else
DirX := Trunc((DX/DH) * Fly.Speed);
If (ABS(DirX) >= ABS(DX)) and
(ABS(DirY) >= ABS(DY)) then
Begin
// If reached destination then ensure that Timing Control Class is
// removed.
Fly.Card.Top := Fly.DestTop;
Fly.Card.Left := Fly.DestLeft;
FlyingCards.Delete(I);
End
Else
Begin
Fly.Card.Top := Fly.Card.Top + DirY;
Fly.Card.Left := Fly.Card.Left + DirX;
End;
End;
end;
[/pascal]
Obviously a FlyCard can be manually insert into the FlyingCards list but I
prefer doing it in a generic function.
[pascal]
procedure TForm1.MoveCardTo(Card: TCards; Speed, DLeft, DTop: Integer);
Var
Fly : TFlyCard;
begin
Fly := TFlyCard.Create;
Fly.Card := Card;
Fly.DestLeft := DLeft;
Fly.DestTop := DTop;
Fly.Speed := Speed;
FlyingCards.Add(Fly);
end;
[/pascal]
My best results come when setting a timer interval of 1 and a speed of 15.
By using animations like this moving the cards to the various areas gives the
game a bit of class.
4.2 Drawing the Card
The real beauty of the Delphi VCL is often you don't have to worry about doing
certain actions. As the Parent for the Card has been set as the cards position
changes it is automatically draw in the new location.
The tutorial to this point is dowloadable as Cards4.
http://www.cairnsgames.co.za/cardtut/cards4.zip
Bookmarks