There are many clever business people that contrive especially creative ways to make money with software. Unfortunately, not all of this software is very interesting to write. Some software and some days lack that frivolous sense of play that writing code is for me. To regain my happy thoughts, believe it or not, I often write reams of code just for fun. And, sometimes this frivolity is more fun and more interesting than work to the point of distraction. This essential play reinvigorates desire and like honing any skill can educate and help improve the art form.
Recently I stumbled across the cards.dll and had a lot of fun whipping together a reasonably good BlackJack game. In this article you get a chance to play with me and in the process learn a bit about using device contexts, the cards.dll API, and we will include a brief discussion on deriving good object-oriented abstractions. (Because the BlackJack game is already done we will frame our discussion in preparation for building the game, War.)
Working with Device Contexts and Graphics
DC, Device Context, hDC, canvas, and the Graphics class all generically or specifically refer to the same thing: the screen real estate that images are drawn on to create what you see on your computer's monitor. The hDC—handle to a device context—is the way the Windows API thinks of your screen's drawing surface, and the Graphics class is the object-oriented encapsulation of the hDC. If we are working at the Windows API level then we need an hDC; if we are working at the Visual Basic .NET level then we will be working with an instance of the Graphics class.
A real easy way to get an instance of the Graphics object representing a specific form's drawing surface is from the PaintEventArgs.Graphics property, and an easy way to get an hDC (handle to a device context) is from an instance of the Graphics' object's GetHdc method.
The Graphics object is considered stateless. For example, if you cache a Graphics object and the size of the drawing surface changes your cached instance will be unaware of this change. Due to this stateless nature, instances of the Graphics object and its underlying handle, hDC, should not be cached in .NET. One instance of a Graphics object should be used in the context of one method call, and its subordinate calls, but not beyond that. You will see this technique used in the code examples in the remainder of this article.
Painting Playing Cards
The cards.dll supports games that ship with Windows, like Solitaire. If you know how to use the API in cards.dll then you can draw those great looking cards that you see in the games that ship with Windows. The three basic methods we will need are cdtInit, cdtDrawExt, and cdtTerm. We also need a width and height variable that are set by the cards.dll initialization method, cdtInit. Listing 1 shows a functional definition of these API methods.
Listing 1: Declarations we need to tap into the cards.dll API library that ships with Windows.
Private width As Integer = 0 Private height As Integer = 0 Declare Function cdtInit Lib "cards.dll" (ByRef width As Integer, _ ByRef height As Integer) As Boolean Declare Function cdtDrawExt Lib "cards.dll" (ByVal hdc As IntPtr, _ ByVal x As Integer, ByVal y As Integer, ByVal dx As Integer, _ ByVal dy As Integer, ByVal card As Integer, _ ByVal type As Integer, ByVal color As Long) As Boolean Declare Sub cdtTerm Lib "cards.dll" ()
The basic rhythm to the cards.dll library is to call cdtInit at the start of your card game, call cdtDrawExt each time you need to draw a card, and call cdtTerm when you are finished with the cards.dll library.
The most interesting method is the cdtDrawExt method. The first parameter represents the drawing surface. The next four parameters represent the dimensions of the playing card. The card parameter contains most of the information needed to draw the card, and the type and color help create the visual effect. It requires special knowledge to paint the face of a card as opposed to the back of a card, and we can capture some of this special knowledge in the two wrapper methods shown in listing 2.
Listing 2: Two methods to paint the face of a specific card or the back of a card.
1: Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _ 2: ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) 3: 4: Dim hdc As IntPtr = g.GetHdc() 5: Try 6: Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + _ 7: CType(Me.FCardSuit, Integer) 8: cdtDrawExt(hdc, x, y, dx, dy, Card, 0, 0) 9: Finally 10: ' If Intellisense doesn't show this method 11: ' unhide advanced members in Tools|Options 12: g.ReleaseHdc(hdc) 13: End Try 14: End Sub 15: 16: Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _ 17: ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) 18: 19: Dim hdc As IntPtr = g.GetHdc() 20: Try 21: ' TODO: Make card style (hardcoded 61) a configurable property 22: cdtDrawExt(hdc, x, y, dx, dy, 61, 1, 0) 23: Finally 24: g.ReleaseHdc(hdc) 25: End Try 26: End Sub
(Line numbers were added for reference only.) PaintGraphicFace takes a Graphics object and the boundary of the card. A card face—Ace of Spades, King of Hearts—needs to be available to the method. (We'll come back to design issues in the next section.) A device context is obtained from the Graphics object, and the hDC represents a resource we need to protect. To that end we will ensure that Graphics.ReleaseHdc is called by using a Try...Finally...End Try resource protection block. The API method cdtDrawExt is called passing the hDC, and the four Cartesian points describing the card boundary. The card value and suit is expressed as a combined value in the card parameter. The type value, if 0 or 2, indicates we want to draw the face of a card, and 1 indicates that the back of a card is desired. The final color parameter is an RGB color. Passing 0 will result in the card being drawn with a default card color.
We use the same cards.dll API method to draw the back of a card. If the card value is 53 and 68 and the type is 1 then one of the possible back designs—between 53 and 68—is painted on the drawing surface. (In listing 2 the style of the cards is hard coded to 61.)
The value for the card suits are clubs equal 0, diamonds equal 1, hearts equal 2, and spades equal 3. The cards themselves are numbered from in order from lowest Ace equaling 0 to highest, King equaling 12. Thus if we want to draw the Ace of Spades then we need to call cdtInit and our PaintGraphicFace method indicating that we want to draw the ace of Spades. Listing 3 shows the code, and figure 1 illustrates the result.
Listing 3: Some sample code to quickly test the cards.dll API.
Public Class Form1 Inherits System.Windows.Forms.Form [ Windows Form Designer generated code ] Private w As Integer = 0 Private h As Integer = 0 Dim FCardFace As Integer Dim FSuit As Integer Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load cdtInit(w, h) FCardFace = 0 FSuit = 3 End Sub Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint PaintGraphicFace(e.Graphics, 10, 10, 75, 100) End Sub Declare Function cdtInit Lib "cards.dll" (ByRef width As Integer, _ ByRef height As Integer) As Boolean Declare Function cdtDrawExt Lib "cards.dll" (ByVal hdc As IntPtr, _ ByVal x As Integer, ByVal y As Integer, ByVal dx As Integer, _ ByVal dy As Integer, ByVal card As Integer, _ ByVal suit As Integer, ByVal color As Long) As Boolean Declare Sub cdtTerm Lib "cards.dll" () Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) Dim hdc As IntPtr = g.GetHdc() Try Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + FSuit cdtDrawExt(hdc, x, y, dx, dy, Card, 0, 0) Finally ' If Intellisense doesn't show this method ' unhide advanced members in Tools|Options g.ReleaseHdc(hdc) End Try End Sub Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) Dim hdc As IntPtr = g.GetHdc() Try ' TODO: Make card style (hardcoded 61) a configurable property cdtDrawExt(hdc, x, y, dx, dy, 61, 0, 0) Finally g.ReleaseHdc(hdc) End Try End Sub Private Sub Form1_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing cdtTerm() End Sub End Class
Figure 1: The expected Ace of Spades.
The code in listing 3 is what we can euphemistically refer to as hacky code. However, when piecing together some new code very quickly I will often employ a very hacky approach just to get something up and running. For example, this code permits me to very quickly experiment with the card-face drawing algorithm to make sure it produces the desired result.
Now that we have some functional code we can dramatically improve upon the code we have by Refactoring it into useful abstractions.
Good Abstractions are an Abstruse Art
The caveat divid et impera—divide and conquer—is our guiding principle. A big part of what divide and conquer means relative to programming is that if we divide a problem into the correct abstractions then we can conquer the problem. In fact, taking this concept a bit further, if we divide a problem into good abstractions the problem is significantly easier to conquer.
Finding good abstractions is difficult to do however. Yet, by practicing this abstruse art in a well-understood problem domain—for example, the notion of playing cards—we can become more practiced at finding good abstractions early. Applying the notion of divide and conquer to our card drawing tools, we can quickly resolve on some reasonably good abstractions.
There are only four suits available to us. Certainly we can use integers to express card suits but an enumeration is more expressive. Another similar abstraction is an enumeration representing the possible face values of the cards Ace to King. Listing 4 demonstrates our enumerations.
Listing 4: Using enumerations makes the notion of suit and face-value constrained to a specific set of named values and more expressive to the human reader.
Imports System Public Enum Face Ace Two Three Four Five Six Seven Eight Nine Ten Jack Queen King End Enum Public Enum Suit Diamond Club Heart Spade End Enum
Now when we talk about the value of a card we can do so in the domain of the problem: cards have a suite and a face value.
Clearly in the domain of playing cards is the notion of a card. A single card class would be a good place to add a constructor, initializing the face value and suit, and a good place to add our paint methods. Listing 5 demonstrates the new Card class with the aforementioned features.
Listing 5: The Card class.
Imports System Imports System.Drawing Public Class Card Private FCardFace As Face Private FCardSuit As Suit #Region "External methods and related fields" Private Shared initialized As Boolean = False Private Shared width As Integer = 0 Private Shared height As Integer = 0 Private Declare Function cdtInit Lib "cards.dll" ( _ ByRef width As Integer, ByRef height As Integer) As Boolean Private Declare Function cdtDrawExt Lib "cards.dll" ( _ ByVal hdc As IntPtr, ByVal x As Integer, ByVal y As Integer, _ ByVal dx As Integer, ByVal dy As Integer, ByVal card As Integer, _ ByVal suit As Integer, ByVal color As Long) As Boolean Private Declare Sub cdtTerm Lib "cards.dll" () #End Region Public Shared Sub Init() If (initialized) Then Return initialized = True cdtInit(width, height) End Sub Public Shared Sub Deinit() If (Not initialized) Then Return initialized = False cdtTerm() End Sub Public Sub New(ByVal cardSuit As Suit, ByVal cardFace As Face) Init() FCardSuit = cardSuit FCardFace = cardFace End Sub Public Property CardSuit() As Suit Get Return FCardSuit End Get Set(ByVal Value As Suit) FCardSuit = Value End Set End Property Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer) Dim hdc As IntPtr = g.GetHdc() Try Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + FCardSuit cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, Card, 0, 0) Finally g.ReleaseHdc(hdc) End Try End Sub Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer) Dim hdc As IntPtr = g.GetHdc() Try cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, 61, 0, 0) Finally g.ReleaseHdc(hdc) End Try End Sub End Class
From the listing you can see that we made the API methods private. This eliminates consumers of card from calling them directly. The shared Init and Deinit method make a valiant effort to ensure cdtInit is called just once, but in this implementation the Deinit method will need to be called by the consumer. (We could implement IDisposable, have the constructor track how many cards were created, and the Dispose method decrement the counter, calling Deinit when the counter is 0.) Finally, we wrap the cards.dll API methods in wrapper methods to ensure that the device context resource is managed correctly every time. (Notice that we eliminated the width and height arguments (dx and dy respectively) of the two paint methods. The width and height are fixed by the cdtInit method, so we might as well use this information.) The result is that our form is radically simplified and we can reuse Card, Suit, and Face in any Windows solution we'd like. Here is the revised form code (see listing 6).
Listing 6: A Form using the new Card class.
Public Class Main Inherits System.Windows.Forms.Form [ Windows Form Designer generated code ] Private Ace As Card = New Card(Suit.Spade, Face.Ten) Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs) Ace.PaintGraphicFace(e.Graphics, 10, 10, 75, 100) End Sub Private Sub Main_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Card.Init() End Sub Private Sub Main_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing Card.Deinit() End Sub End Class
After creating the Card class all we need to do is create an instance of a card and paint it in the OnPaint event handler.
Thus far we have discovered the easy abstractions. Suit, Face, and Card are pretty easy to find. The hard part is finding as many of the abstractions as we can relative to our problem domain. For example, it is reasonable that we might want a collection of cards, referred to as a deck. However, a pinochle deck has no cards less than 9 but many other games need all of the cards. Furthermore some games like BlackJack might use multiple decks and cards like the Ace may have more than one value depending on context: Ace can be electively used to represent the value of 1 or 11.
Now we have moved into the realm of moderate complexity. What if we want one set of classes to represent all games? What about rules? How do we codify rules to permit changing the rules' object depending on the game selected? What if we want to support Internet play, console play, or multi-player games? Our challenges become significantly greater.
The objective is to figure out what your real goals are and to code to support those goals. If it is possible to support known objectives and permit future growth—for instance, supporting multiple games and one player while leaving room for multi-player in the future—then you are likely to exceed your customer's expectations.
War, What is it Good For?
For fun try to expand on our work here to create a card game that you enjoy playing. Here is a brief description and some hints that will help you code the relatively easy game of War.
War is a simple game where two players are each given half the cards of a single deck. Each player draws a single card and the cards are compared. The highest value card wins the hand and both cards. The player that ends up with all of the cards wins. In addition, a War occurs when each player draws the same face value card. Additional cards are drawn to resolve the War. The winner of a war-hand wins all of the cards involved, potentially speeding up the game's outcome.
In the problem domain we will need some of the following items: we will need to represent a deck of cards, a way to shuffle the deck, a means of representing two players, drawing the cards, and deciding the outcome of each draw. Finally, we will need to decide the outcome of a game.
Here are some hints: often nouns in the problem domain are good candidates for classes in the solution domain (e.g. Deck and Player). Next, verbs in the problem domain are often behaviors in the solution domain; that is methods in your classes. Picking the right classes and matching the right behaviors to those classes is a big part of the battle. For example, it might be reasonable to define a class named Rules and the Rules class might reasonably contain a method that compares two instances of Card and decides if the Card's represent a definitive winner, loser, or a war. A broader solution might define an interface named IRules and a class named Hand. A Hand represents n-number of cards and IRules accepts two hands and returns value indicating if one is greater, less, or equal to the other. Such an interface opens the door to writing a solution that encompasses many card games.
Finding some code to write that inspires you to write for fun is a great way to learn and amuse yourself in a non-toxic low cost way. Writing and rewriting such a solution many times over a period of months or years will likely help you hone your craft to a keen edge. You will likely be surprised at how different your solutions are between the first time you write them and the last.
With some practice you will be able to very quickly find good abstractions and implement solutions with a confident adroitness. If you come up with a good implementation of Texas hold 'em let me know. It looks like a lot of fun.Paul Kimmel is the VB Today columnist for Codeguru.com and Developer.com and has written several books on object oriented programming, including the recently released Visual Basic .NET Power Coding from Addison-Wesley and the upcoming Excel VBA 2003: Programmer's Reference from Wiley. He is the chief architect for Software Conceptions and is available to help design and build your next application.
Meet Paul Kimmel at DevDays in Detroit, March 2004.
# # #