I have seen many posts regarding a standard 52-card deck of playing cards. I recently was composing an answer to one such post when I realized my answer should be its own post since (A) the answer diverged greatly from the OP and (B) my classes addressed my own objectives and not the OP's.
I have no specific questions other than the implied at CR: please review and critique the code. My particular interest is in the API, but I also welcome comments about the Console UI example. I am using C# and .NET 5.
Here are my objectives:
The base objects of Rank
, Suit
, Card
, and Deck
will exist in a re-useable API which can be used by any UI application, be it Console-based, WPF, WinForms, Unity, etc.
I want to generate a standard 52-deck of cards.
I also want the versatility to generate many other types of decks from that same API. Maybe I want a deck that includes 1 or more Jokers. Maybe I want a non-standard deck composed of 2 suits of 10 cards each. Maybe I want a casino style super deck composed of 6 standard decks.
I want to completely avoid the magic numbers of 13, 4, and 52. I want to go as far as avoiding them as constants.
While many use only an
enum
for rank or suit, I want a little more meta data associated with each rank and suit. For example, the color is the suit is nice to know in an API.
About the API
My Rank
struct does have a superficial FaceValue
property but that should not be confused with the value or points of a given card under the rules of a particular game. Consider the treatment of various cards for various games:
- In Poker, an Ace may be a 1 or a 14.
- In Blackjack, an Ace may be a 1 or 11.
- In Freecell, an Ace is a 1.
- In Hearts, an Ace has a value of 14, but the Ace of Hearts is equal to 1 point.
- In Crazy Eights, only the facevalue of the Ace matters.
A Deck
is not a Game. For this post, a Game is not defined. A Deck
is oblivous to the notion that in some games one suit may trump another suit, or that a given rank may be of greater value or points that another rank. Those concerns would be handled by a given game object and its own set of rules and are (rightfully) ignored here.
There are a couple of methods in Deck
that I chose to be Fluent. One in particular is Shuffle
, which began as a void
but required 2 lines such as:
deck.Shuffle();
deck.WriteLines();
With a Fluent design, I can do that in one line with:
deck.Shuffle().WriteLines();
Rank Structure
public enum RankName : byte
{
Ace = 1,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Joker = byte.MaxValue
}
public struct Rank : IEquatable<Rank>
{
private Rank(RankName name, string symbol)
{
this.Name = name;
this.Symbol = symbol;
}
public RankName Name { get; }
public string Symbol { get; }
// Do not confuse the FaceValue with a card's true Value or rank in a Game.
// A Game object woud define its own rules and assign values accordingly.
// Examples:
// Poker - Ace's Value may be a 14 or a 1.
// Blackjack - Ace's Value may be 10 or 11.
// FreeCell - Ace's Value is a 1.
// A deck of playing cards is oblivous to values assigned by a Game,
// but a deck can know about the FaceValue of each card.
public byte FaceValue => (byte)Name;
public bool Equals(Rank other) => Name.Equals(other.Name);
public override string ToString() => Symbol;
public static Rank Ace => new Rank(RankName.Ace, "A");
public static Rank Two => new Rank(RankName.Two, "2");
public static Rank Three => new Rank(RankName.Three, "3");
public static Rank Four => new Rank(RankName.Four, "4");
public static Rank Five => new Rank(RankName.Five, "5");
public static Rank Six => new Rank(RankName.Six, "6");
public static Rank Seven => new Rank(RankName.Seven, "7");
public static Rank Eight => new Rank(RankName.Eight, "8");
public static Rank Nine => new Rank(RankName.Nine, "9");
public static Rank Ten => new Rank(RankName.Ten, "10"); // Alert: 2-character symbol
public static Rank Jack => new Rank(RankName.Jack, "J");
public static Rank Queen => new Rank(RankName.Queen, "Q");
public static Rank King => new Rank(RankName.King, "K");
// Other possible Joker symbols:
// "WC" for Wildcard
// "<Ŵ>" which is a Wild way of using W for Wildcard
public static Rank Joker => new Rank(RankName.Joker, "¡J!"); // VERY special symbol also > 1 character
public static IList<Rank> StandardRanks => new Rank[] { Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King };
}
Suit Structure
public enum SuitColor : byte { None, Black, Red }
public enum SuitName : byte { None, Clubs, Diamonds, Hearts, Spades }
public struct Suit : IEquatable<Suit>
{
private Suit(SuitName name, string symbol, SuitColor color)
{
this.Name = name;
this.Symbol = symbol;
this.Color = color;
}
public SuitName Name { get; }
public SuitColor Color { get; }
public string Symbol { get; }
public bool Equals(Suit other) => Name.Equals(other.Name);
public override string ToString() => Symbol;
public static Suit Clubs => new Suit(SuitName.Clubs, "♣", SuitColor.Black);
public static Suit Diamonds => new Suit(SuitName.Diamonds, "♦", SuitColor.Red);
public static Suit Hearts => new Suit(SuitName.Hearts, "♥", SuitColor.Red);
public static Suit Spades => new Suit(SuitName.Spades, "♠", SuitColor.Black);
public static Suit None => new Suit(SuitName.None, "", SuitColor.None);
public static IList<Suit> StandardSuits => new Suit[] { Hearts, Clubs, Diamonds, Spades };
public static IList<Suit> RedSuits => new Suit[] { Diamonds, Hearts };
public static IList<Suit> BlackSuits => new Suit[] { Clubs, Spades };
}
Card Structure
public struct Card : IEquatable<Card>
{
public Card(Rank rank, Suit suit)
{
this.Rank = rank;
this.Suit = suit;
}
public Rank Rank { get; }
public Suit Suit { get; }
public bool Equals(Card other) => Rank.Equals(other.Rank) && Suit.Equals(other.Suit);
public override string ToString() => $"{Rank}{Suit}";
}
Deck Class
public class Deck
{
public Deck(IEnumerable<Suit> suits, IEnumerable<Rank> ranks, int numberOfDecks = 1, int numberOfJokers = 0)
{
Suits = suits.ToArray();
Ranks = ranks.ToArray();
DeckCount = numberOfDecks;
JokerCount = numberOfJokers;
_cards = new Card[(Suits.Count * Ranks.Count * numberOfDecks) + numberOfJokers];
ResetToNewDeck();
}
private Card[] _cards { get; }
public IReadOnlyList<Card> Cards => _cards;
public IReadOnlyList<Suit> Suits { get; }
public IReadOnlyList<Rank> Ranks { get; }
public int DeckCount { get; }
public int JokerCount { get; }
private Random _random = new Random();
public static Deck CreateStandardDeck() => new Deck(Suit.StandardSuits, Rank.StandardRanks, numberOfDecks: 1, numberOfJokers: 0);
public Deck ResetToNewDeck()
{
int i = 0;
for (var d = 1; d <= DeckCount; d++)
foreach (var suit in Suits)
foreach (var rank in Ranks)
{
_cards[i++] = new Card(rank, suit);
}
for (var j = 0; j < JokerCount; j++)
{
_cards[i++] = new Card(Rank.Joker, Suit.None);
}
return this; // trying to be a little Fluent
}
public Deck Shuffle()
{
// Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
for (var i = 0; i < _cards.Length - 1; i++)
{
var j = _random.Next(i, _cards.Length);
if (i != j)
{
var temp = _cards[i];
_cards[i] = _cards[j];
_cards[j] = temp;
}
}
return this; // trying to be a little Fluent
}
}
A sample Console UI
I wanted a little razzle dazzle when displaying the cards, so I choose to colorize them with a white background and red or black text. I made an concession regarding the Joker to make it stand-out more and not be confused for a "J" or Jack.
ConsoleExtensions class
// These methods do not extend the Console object, but rather use the Console as the primary UI.
public static class ConsoleExtensions
{
// Tuple of ConsoleColor pair is for Foreground and Background respectively.
private static IDictionary<SuitColor, (ConsoleColor, ConsoleColor)> ColorMap =>
new Dictionary<SuitColor, (ConsoleColor, ConsoleColor)>()
{
{ SuitColor.Black, (ConsoleColor.Black, ConsoleColor.White) },
{ SuitColor.Red, (ConsoleColor.DarkRed, ConsoleColor.White) },
{ SuitColor.None, (ConsoleColor.White, ConsoleColor.DarkYellow) }
};
private const string DefaultCardSeparator = " ";
private const string DefaultLinePrefix = " ";
public static void Write(this Card card)
{
var (foreColor, backColor) = ColorMap[card.Suit.Color];
Console.ForegroundColor = foreColor;
Console.BackgroundColor = backColor;
Console.Write(card);
// Why ResetColor? For other methods that write many cards and use a separator
// between each card. That separator should use the default Console colors
// so that is cannot be visually mistaken as part of a card.
Console.ResetColor();
}
private static HashSet<string> NewLineSet => new HashSet<string>() { "\n\r", "\r\n", "\n" };
public static void WriteLine(this IEnumerable<Card> cards, string separator = DefaultCardSeparator, string linePrefix = DefaultLinePrefix)
{
separator ??= "";
linePrefix ??= "";
var isNewLine = NewLineSet.Contains(separator);
var repeatPrefix = true;
foreach (var card in cards)
{
if (repeatPrefix)
{
Console.Write(linePrefix);
repeatPrefix = isNewLine;
}
card.Write();
Console.Write(separator);
}
if (!isNewLine)
{
Console.Write(Environment.NewLine);
}
}
public static void WriteLine(this Deck deck, string separator = DefaultCardSeparator, string linePrefix = DefaultLinePrefix) => deck.Cards.WriteLine(separator, linePrefix);
public static void WriteLines(this Deck deck, int maxPerRow = 0, string separator = DefaultCardSeparator, string linePrefix = DefaultLinePrefix)
{
if (maxPerRow <= 0)
{
maxPerRow = deck.Ranks.Count;
}
var rows = (int)Math.Ceiling(deck.Cards.Count / (double)maxPerRow);
for (var i = 0; i < rows; i++)
{
deck.Cards.Skip(i * maxPerRow).Take(maxPerRow).WriteLine(separator, linePrefix);
}
}
}
The Main Program
class Program
{
static void Main(string[] args)
{
var deck = Deck.CreateStandardDeck();
Console.WriteLine("Fresh new standard deck:");
deck.WriteLines();
Console.WriteLine($"{Environment.NewLine}Shuffled deck:");
deck.Shuffle().WriteLines();
Console.WriteLine($"{Environment.NewLine}New deck with 2 Jokers:");
deck = new Deck(Suit.StandardSuits, Rank.StandardRanks, numberOfDecks: 1, numberOfJokers: 2);
deck.WriteLines();
Console.WriteLine($"{Environment.NewLine}Shuffled deck with 2 Jokers:");
deck.Shuffle().WriteLines();
Console.WriteLine($"{Environment.NewLine}Fresh deck with each card on it's own line.");
// Example (1)
deck.ResetToNewDeck().WriteLine(Environment.NewLine);
// Example(2)
//deck.ResetToNewDeck().WriteLines(maxPerRow: 1, separator: null);
// To create a special deck of 2 suits and 10 ranks, use:
Console.WriteLine($"{Environment.NewLine}Special deck of 2 suits and 10 ranks");
deck = new Deck(new[] { Suit.Spades, Suit.Hearts },
new[] { Rank.Ace, Rank.Two, Rank.Three, Rank.Four, Rank.Five, Rank.Six, Rank.Seven, Rank.Eight, Rank.Nine, Rank.Ten });
deck.WriteLines();
Console.WriteLine($"{Environment.NewLine}Press ENTER to close.");
Console.ReadLine();
}
}
Sample Output Images
How to create a Casino Super Deck
// Fun fact: most Blackjack games in Las Vegas use 6 to 8 decks!
// https://en.wikipedia.org/wiki/Blackjack
// To create one big deck composed of 6 standard decks, use:
deck = new Deck(Suit.StandardSuits, Rank.StandardRanks, numberOfDecks: 6);