CardLib

Introducing CardLib, a card-game programming library

(How to use cards.dll, Part 3)

Updated 14th Jul 2005

 
  • Fixed bug in Cardlib's CardRgnMouse.cpp which caused cardgames to hang when a stack was clicked (only occurred when compiled with VC7.0+)

Introduction

The previous two tutorials in this series concentrated on accessing the playing-card bitmap images from cards.dll, and demonstrated how to simply draw a playing-card into a window.

This tutorial presents CardLib, a fully featured card-game library, which provides a framework to manipulate cards and collections of cards.

Cards can be dragged around the screen from one stack to another. Card-stacks are fully customizable. A function-callback system allows the programmer to register callback functions with different events. This system provides complete control over playing card manipulation.

{short description of image}

I decided to write this library using C++. No external libraries such as MFC or ATL are required to use the library. However, there is nothing to stop you from using these frameworks alongside CardLib. CardLib requires the use of a C++ compiler. The project currently only supports Visual C++ 6.0, but other 32bit Windows compilers should also work because there are no Visual C++ extensions used by the project.

I chose C++, because it is the language which lends itself best to this type of library. Originally I had wanted to use C instead of C++, but I encountered many problems designing the library. This was because an inherently object-oriented concept was being designed, and I finally decided that C++ was the way forward. This, in addition to the type-safety C++ provides, has resulted in an easier to use and better designed game library. (Well, I think so, anyway!).

For the purely C folk reading this, please don't be put off. The library is really easy to use, and I havn't used any advanced C++ features such as templates or polymorphism. In fact, most of the functional game code that you write will use very few C++ features. If you can write C programs, you can use this library.

Create a playing table

The first thing you must do in a CardLib program is to link with CardLib.lib, and #include "CardLib.h". Once you have done this, you are free to start using the library. Next, you must create a CardWindow which forms the basis of all subsequent coding. Therefore, you need to declare a class object of type CardWindow somewhere in your program.

CardWindow cardwnd;

If you are writing a bare-bones Win32 application, then this declaration could just be a global variable. For an MFC application, you would define this variable as a class member in a CView class.

By itself, this declaration doesn't really do much. In order to actually create a physical card window, you need to call the CardWindow::Create member function.

BOOL CardWindow::Create(HWND hwndParent, DWORD dwExStyle, DWORD dwStyle, 
                            int x, int y, int width, int height);

With this function, you specify the parent window of the card-window, the window styles, and the card window location. A typical usage of this function might look like this:

cardwnd.Create(hwnd, WS_EX_CLIENTEDGE, WS_CHILD|WS_VISIBLE, 0, 0, 100, 100);

In order to manipulate the physical card window, you simply pass the CardWindow object to any Win32 API call you desire. The CardWindow class automatically converts to a HWND window handle whenever you need one. So, to resize the card window in a WM_SIZE handler, for example, you would write something like this:

case WM_SIZE:
    nWidth  = (LOWORD)lParam;
    nHeight = (HIWORD)lParam;

MoveWindow(cardwnd, 0, 0, nWidth, nHeight, TRUE); break;

At this point you don't need to do anything else. The card window is currently empty though, apart from the default dark-green background. It would be nice if we could create some playing cards somehow, and drag them around.

Create a Playing Card

The basic element of any card-game is a playing card. CardLib provides a built-in C++ class called Card. A card has a number of attributes, listed below:

  • Face-value, or Rank (Ace-King)
  • Suit (Clubs, Diamonds, Hearts and Spades)
  • Face-direction (facing Up or Down)

It is simple to create a playing card - just create a Card variable.

Card card(Ten, Diamonds);
Card card(48); 

The example above shows two methods of intializing a playing card. The first method is obvious - specify the suit followed by the rank. The second method needs explaining though. The single-argument constructor for a Card requires an integer value in the range 0-51, inclusive. This value uniquely indentifies a playing card. You can calculate this value using the following formula, or use the MAKE_CARD function, whichever you prefer.

int MAKE_CARD(int Value, int Suit);

... or ...

int cardval = (Value - 1) * 4 + Suit;

Whilst playing cards are essential to a card game, a Card object provides no method to display itself - a Card is just a basic C++ class. To actually display and interact with a Card object, you must first place the card on a card-stack.

Create a Card Stack

CardLib implements a stack-orientated card system. Rather than creating individual playing cards and positioning them manually on the screen, CardLib gives you the power to manipulate and interact with collections of cards instead. A CardStack is a C++ data structure which provides a "stack" metaphore to one or more playing cards. Cards are accessed from the top of the stack, and cards can be pushed and popped from a stack in whatever way you want.

Note that a CardStack still doesn't provide any way to draw itself on the screen, or any way for the user to interact with it. The sole purpose of a CardStack object is to collect one or more cards together in an ordered manner. The following example shows how to create and manipulate a CardStack.

Card card(8, Spades);
CardStack cardstack;

/* Add, then remove a card */
cardstack.Push(card);
cardstack.Pop();

Once you have created a CardStack and populated it with cards, it is finally time to display it on the screen.

Create a Card Region

A CardRegion object is a place-holder for a CardStack. This is basically a region in a CardWindow which can hold and display a CardStack. Every CardRegion object contains one CardStack. The contents of the CardStack dictate how the CardRegion draws itself. You create a CardRegion using the following CardWindow member function:

CardRegion* CardWindow::CreateRegion (int id, bool fVisible, 
                       int x, int y, int xoffset, int yoffset);

With this function, you specify a unique ID, whether or not the stack is initally visible, the coordinates of the stack, and the direction that cards take when collected on the stack. A typical usage of this function might look like this:

#define MY_CARD_STACK 1
CardRegion *pRegion1;
...
pRegion1 = cardwnd.CreateRegion(MY_CARD_STACK, true, 10, 10, 20, 3);

The CardWindow object performs all necessary allocation for you, and performs all the management required to draw the card-region and interact with it. You don't need to store the CardRegion pointer that is returned - you can retrieve this at any time by using the CardWindow::CardRegionFromId(int Id) member function. However, you can perform alot of customization on the CardRegion itself, so you will probably find yourself storing this pointer for later use.

Once a CardRegion has been created, you can add a CardStack to it, to be displayed and interacted with. Use the CardRegion::SetCardStack method to accomplish this. The following example shows how to create a stack of cards and add it to a CardRegion for display.

CardStack cardstack;
Card card1(Jack, Clubs);
Card card2(Ace, Hearts);

cardstack.Push(card1);
cardstack.Push(card2);

pRegion1->SetCardStack(cardstack);

Note that the CardRegion does not take "ownership" of the CardStack passed to it - a CardRegion keeps its own internal CardStack. Therefore if you modified the CardStack after passing it to CardRegion::SetCardStack, this would have no effect on the region's internal CardStack.

At this stage we have created a fully functional stack of cards which can be dragged around the screen. Creating additional stacks is left as an exercise to the reader. The only thing left to do is to customize the appearance and behaviour of the CardRegion.

There are quite a few CardRegion member functions to choose from. The CardLib Programmer's Reference describes each function available to you.

Create a Button

The CardWindow object supports buttons as well as card-stacks. These buttons are not "real" button controls, rather they are built into the card window, and share the same colour scheme. It is not necessary to create any buttons in your card game, but sometimes they can be useful if you want to give the user some kind of control to click on, say to shuffle a card-stack, or to pickup some cards from one stack and place them in another.

You create a CardButton object using the following CardWindow member function:

CardButton* CardWindow::CreateButton(int id, TCHAR *szText, UINT uStyle, 
         bool fVisible, int x, int y, int width, int height);

Again, it is not necessary to store the CardButton pointer that is returned, but it will be useful if you want to customize the button, for example setting if it is centered or right-aligned.

Provide Drag and Drop Behaviour

Card stack behaviour is the key to implementing a card-game. By default, a card-stack will let you drag and drop any number of cards from and to it. It is unlikely that you will want to allow this type of behaviour, so specifying which cards can be dragged to and from stacks gives you greater control over stack manipulation.

There are only two functions which control stack behaviour. These are:

bool CardRegion::SetDragRule (UINT uDragType, pCanDragProc proc=0)

Card dragging is controlled using the SetDragRule member function. There are four different types of card dragging:

  • CS_DRAG_NONE prevents any cards from being dragging from a stack.
  • CS_DRAG_TOP restricts dragging to the top card only.
  • CS_DRAG_ALL allows any number of cards to be dragged (the default).
  • CS_DRAG_CALLBACK allows you to specify a custom callback function which is executed whenever the user tries to drag cards from a stack. The callback function returns a boolean value to indicate whether or not to allow the drag action to contine. More information on callbacks can be found in the next section.
bool CardRegion::SetDropRule (UINT uDropType, pCanDropProc proc=0)

Card dropping is controlled using the SetDropRule member function. There are three different types of drop behaviour:

  • CS_DROP_NONE prevents any cards from being dropped.
  • CS_DROP_ANY allows any number of cards to be dropped (the default).
  • CS_DROP_CALLBACK allows you to specify a custom callback function which is executated whenever the user tries to drop cards on a stack. The callback function returns a boolean value to indicate whether or not to allow the drop to take place. More information on callbacks can be found in the next section.

SetDragRule and SetDropRule both accept an optional second parameter - a pointer to a callback function. This parameter must only be specified when using the CS_xxxx_CALLBACK rule.

Create an Event Callback

Event callbacks provide complete control over stack manipulation. Here is a list of the different types of callback that a StackObject supports.

Type Return type Description
CanDragProc bool Controls how many cards can be removed from a stack.
CanDropProc bool Controls the type of card that can be dropped on a stack.
ClickProc void Single-click notification of one of the cards in a stack.
DblClickProc void Double-click notification of one of the cards in a stack.
AddProc void Sent after cards have been dropped (added) on a stack
RemoveProc void Sent after cards have been dragged (removed) from a stack.

Drag Callback

A drag callback must use the following prototype.

bool CARDLIBPROC CanDragProc(CardRegion &cardrgn, int iNumDragging);

The CardRegion parameter identifies which card-stack is being manipulated.

The iNumDragging variable specifies how many cards (from the top) are being removed.

The callback must return true to allow the drag, or false to prevent any cards from being removed.

Drop Callback

A drop callback must use the following prototype.

bool CARDLIBPROC CanDropProc(CardRegion &cardrgn, CardStack &cardstack);

The CardRegion parameter identifies which card-stack is having cards dropped on it.

The CardStack object contains the list of cards that are being dropped.

The callback must return true to allow the drop, or false to prevent any cards from being dropped.

Mouse Click Callback

A mouse-click callback is set using the CardRegion::SetClickProc member function.

void CardRegion::SetClickProc (pClickProc proc);

The function you pass to SetClickProc must use the following prototype.

void CARDLIBPROC ClickProc(CardRegion &cardrgn, int iNumClicked);

The CardRegion parameter identifies which card-stack has been clicked on.

The iNumClicked variable specifies which card (from the top) has been clicked on. This variable will range from 0 (the top card) to n-1 (the number of cards in the stack).

The callback does not return a value.

Mouse Double-Click Callback

A mouse double-click callback is set using the CardRegion::SetDblClickProc member function.

void CardRegion::SetClickProc (pClickProc proc);

The callback function uses the exact same function prototype as the single-click callback.

void CARDLIBPROC ClickProc(CardRegion &cardrgn, int iNumClicked);

The CardRegion parameter identifies which card-stack has been double-clicked on.

The iNumClicked variable specifies which card (from the top) has been double-clicked on.

The callback does not return a value.

Add Callback

The card-add callback is set using the CardRegion::SetAddCardProc member function.

void CardRegion::SetAddCardProc(pAddProc proc);

The callback is called after any card(s) have been dropped on a card-stack, and must use the following prototype.

void CARDLIBPROC AddProc(CardRegion &cardrgn, CardStack &cardstack);

The CardRegion parameter identifies which card-stack has been clicked on.

The CardStack parameter specifies the list of cards which have been added.

The callback does not return a value.

Remove Callback

The card-remove callback is set using the CardRegion::SetRemoveCardProc member function.

void CardRegion::SetRemoveCardProc(pRemoveProc proc);

The callback is called after any card(s) have been dragged from a card-stack (and subsequently dropped on another stack). The remove-callback must use the following prototype.

void CARDLIBPROC RemoveProc(CardRegion &cardrgn, int iNumRemoved);

The CardRegion parameter identifies which card-stack has been clicked on.

The iNumRemoved variable specifies how many cards were removed.

The callback does not return a value.

Conclusion

Once again, this is work in progress. It's not that there are any problems with the library, but this is my first real stab at a framework of this sort, so there are undoubtably some areas that could be improved, from a design perspective.

I am confident that the library works well. It took me just over an hour to write a complete Solitiare game using the library, so I think this speaks for itself.

Well, if you have any comments or ideas for improvement, then I'd like to hear them..

James.