![]() |
Canfield (C# Project)
Overview
For my first project in this language, I decided to program a simple canfield game. Canfield is a solitaire game, the goal of which is to move all 52 cards to the foundations, building up in suit from the first rank played (wrapping around from King to Ace as needed). Four tableaux are built down in alternating colors. Gaps in the tableaux are filled by the top card of the ten-card reserve, and then by cards from the waste. Waste cards are flipped three at a time, with no limit on the number of deals.
Because I was primarily interested in working with the C# logic such as objects and arrays, I kept the user interface simple. Card movement is done entirely through command buttons; drag-and-drop doesn't work. Also, there are no help or undo functions.
![]() |
| Fig. 1: Initial state |
Running the program
This is an online-only implementation, with the following prerequisites.
If these components are already installed, you can run the application now (apparently requires Internet Explorer). Otherwise, set it up and run it (does not require Internet Explorer).
If you want, you can download the project files. These are not needed to run the program.
User interface
In order to help direct the possible user actions, especially in light of the lack of a help file or verbose, clear error messages, I took advantage of command buttons' visible and enabled properties. Here is a sample game where no card has been picked up, and another where a pile of cards is currently "in hand."
![]() |
| Fig. 2: No card in hand |
![]() |
| Fig. 3: Pile in hand |
Major concepts explored and learned
Custom reusable object classes
Before creating the Canfield program, I created a set of classes with the intent to make them flexible enough for re-use. These were tweaked as I programmed, and ended up as:
Playing cards consist of two properties, and are thus the most straightforward of these objects:
class PlayingCard
{
private PlayingCardSuit suit = new PlayingCardSuit();
public PlayingCardSuit Suit
{
get
{
return suit;
}
set
{
suit = value;
}
}
... code here ...
}
Suits and ranks offer three properties: Index, Name, and Initial. This allows the programmer interacting with these classes to choose which they prefer; the canfield program uses all three in different parts of the program. In reality, each class only stores a single value for each parameter--index--and then has conversion arrays for the names and initials. For instance, here are the global variables for the rank class; only index is changeable.
private const int MAXRANKS = 13;
private static string[] initials = { "I", "A", "2", "3", "4", "5", "6", "7",
"8", "9", "X", "J", "Q", "K" };
private static string[] names = { "Invalid", "Ace", "Two", "Three", "Four",
"Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King" };
private int index = 0;
Suits offer two additional read-only properties: Color (i.e., red or black) and Mark (the suit character, which is easier for a user to read than the initial). These are made read-only by leaving out the "set" section:
public string Color // Read-only: Red or black
{
get
{
switch (index)
{
case 1: // Hearts
case 2: // Diamonds
return "Red";
case 3: // Clubs
case 4: // Spades
return "Black";
default:
return "Invalid";
}
}
}
As a side note, I discovered that break;, which is usually required at the end of a case block, is neither required nor allowed if the last line of the block is return or throw.
A poker deck consists of 52 to 54 playing cards (up to two jokers can be added). The current methods are GetCardByIndex (get a specific playing card based on its index), NewDeck (create a new deck, sorted by suit and rank), and ShuffleDeck. The recommended shuffle is a full shuffle, but a fan shuffle is included as well. I included the fan shuffle because I was interested in simulating a real world shuffle technique, to see how many shuffles it generally will take until a new deck appears to be reasonably shuffled.
Overloaded methods
The ShuffleDeck method is overloaded, i.e., there are two versions of it with two different argument lists. The effect of this is that the "full" shuffle is the default version.
public void ShuffleDeck()
{
PlayingCard tempCard = new PlayingCard();
int lowIndex = new int();
int highIndex = new int();
Random random = new Random();
for (lowIndex = 0; lowIndex < deckSize; lowIndex++)
{
highIndex = random.Next(0, deckSize - lowIndex + 1);
if (highIndex > 0)
{
tempCard = cardDeck[lowIndex];
cardDeck[lowIndex] = cardDeck[highIndex];
cardDeck[highIndex] = tempCard;
}
}
}
public void ShuffleDeck(string Type)
{
switch (Type.ToUpper())
{
case "FAN":
... code here ...
break;
case "FULL":
ShuffleDeck();
break;
default:
throw new System.ArgumentException("Invalid shuffle type specified.");
}
}
I'm used to doing this (in VB) via optional arguments, which for this sort of purpose strikes me as easier to write and maintain. I can see the added flexibility of overloaded methods overall, though.
Multidimensional Generic Lists
Canfield has four tableau piles, each of which can any number of cards. These are stored as an array of four generic lists. Using lists allow us to have dynamically-sized arrays, as well as making it easier to remove specific elements from the middle of the array (very useful in a card game). This is done by first defining the fixed (four element) array, and then creating each individual list:
List<PlayingCard>[] glTableau = new List[TABLEAUCOUNT]; ... code here ... for (I = 0; I < TABLEAUCOUNT; I++) { glTableau[I] = new List<PlayingCard>(); }
Specific elements can then be referred to by nesting array references, as in glTableau[intTableau][glTableau[intTableau].Count - 1] (where intTableau indicates one of the four tableau array lists and glTableau[intTableau].Count - 1 its last element).
Likewise, specific elements can be added and removed this way:
while (glTableau[intTableauCardInHandSource].Count > 0)
{
glTableau[intTableau].Add(glTableau[intTableauCardInHandSource][0]);
glTableau[intTableauCardInHandSource].RemoveAt(0);
}
I had initially written these with ArrayLists, but switched them to generic Lists on the advice of a commenter. Lists have the advantage of allowing type specification in the declaration statement, eliminating the need during to explicitly state type in the code. For instance, glTableau[intTableau][glTableau[intTableau].Count - 1] was glTableau[intTableau][glTableau[intTableau].Count - 1].
Control arrays
The twelve tableau buttons are grouped in four sets of three. For the ease of programming, it is convenient to refer to this from within an array. To create a two-dimensional control array, I first created the twelve controls I wanted on the form, named cmdTableau00..cmdTableau32. I then declared an array and filled it:
Button[,] cmdTableaux = new Button[TABLEAUCOUNT, TABLEAUCOLUMNCOUNT];
... code here ...
for (I = 0; I < TABLEAUCOUNT; I++)
{
foreach (Control aControl in this.grpTableau.Controls)
{
for (J = 0; J < TABLEAUCOLUMNCOUNT; J++)
{
if (aControl.Name.Equals("cmdTableau" + I.ToString() + J.ToString()))
{
cmdTableaux[I, J] = (Button) aControl;
}
}
}
}
Arrays of objects
One minor puzzle I had to overcome was realizing that, when I initialized a static array of objects, I also had to initialize each element of the array. For instance, in the PokerDeck class,
private PlayingCard[] cardDeck = new PlayingCard[MAXCARDCOUNT + MAXJOKERCOUNT];
creates an array of playing cards, but each element is null. The following code is also needed:
for (index = 0; index < deckSize; index++)
{
cardDeck[index] = new PlayingCard();
}
Or/and evaluation precedence
C#, like C, has sequence points in boolean evaluations; VB in my experience doesn't. With a sequence point, as each expression is evaluated from left to right, if the truth value of entire evaluation can be definitively determined, the evaluation stops. That is, if expression a in if (a || b) {c;} else {d;} is true, expression b is ignored and c is processed; likewise, if expression a in if (a && b) {c;} else {d;} is false, expression b is ignored and d is processed.
This allowed me to simplify the code in the DropCard overload method. Consider this snippet:
if (glTableau[intTableau].Count == 0 ||
ValidPileMatch(pcCardInHand,
glTableau[intTableau][glTableau[intTableau].Count - 1]))
{
glTableau[intTableau].Add(pcCardInHand);
glReserves.RemoveAt(glReserves.Count - 1);
DropCard();
}
If there are no elements in the glTableau[intTableau] array, then attempting to access any of its elements via glTableau[intTableau][glTableau[intTableau].Count - 1] will return an error. However, if there are no elements in the array, then glTableau[intTableau].Count == 0 is true and the ValidPileMatch call is not invoked. I had originally programmed this with a nested-if, being used to VB.
Conclusions
Overall, canfield turned out to be easier than I was expecting it to be to program (about four evenings, working around a rambunctious infant). This was my first VC# project, although I have experience with VB, PHP, and less so with VC++. As someone who has programmed primarily in various flavors of Basic for the last twenty-five years (as well as some reasonable forays over the last decade into PHP), I can appreciate some of the strengths of C# that I encountered in this project.
As for the program itself, I've run through it extensively and believe I've gotten rid of all the bugs, but please do let me know if you find any, have any questions, or if you just happen to find this page, enjoy the program or write-up, and want to say hello.