- Introduction
- CardGames.Core
- CardGames.Core.French
- CardGames.Poker
- Benchmarks
- License
- Versioning
- Feedback and Contributing
After writing simulations and tools for card games (poker in particular) in the last years, I have decided to take a step back, sort through the code bases, clean up here and there, and distill out small, reusable packages that hopefully can be useful for the open source community.
I assume this project will be slow, because of several reasons. First of all, it is a pure leisure project. Secondly, because I value design. I try to model the entities and their apis as closely as possible to the real world concepts they represent (albeit adding some convenience apis if they are very useful). I value code quality and readability a lot as well. I'll spend lots of time rewriting algorithmically simple things if I feel I can express them even cleaner, or more performantly.
Available at Nuget: EluciusFTW.CardGames.Core
There are thousands of different card games, with many different cards and collections of cards, with different rules and purposes.
In this package, we tried to follow a domain-driven approach to card games in general1. It contains the basic entities needed for such a game: Cards (duh!), Card decks (which are the finite collections of all possible cards of given type) and Dealers.
1 Actually, up to abuse of language, any game of chance that contains a finite set of possibilities can be modelled using this package, e.g., we can interpret a die as a deck of six cards called: 1,2,3,4,5,6. We can implement a DiceDealer
who 'shuffles the deck' (i.e. returns the dealt/rolled card/value back to the deck immediately) after dealing a card (rolling the die).
The most elemental part of a card game is the card. There is actually nothing universal that describes a card, except for it being detemined by it's content. So in all later entities the card will be represented by a generic type TCard
, with the constraint that it is a class.
NOTE: Until recently,
TCard
was generically constrained to be a struct. However, I have decided to switch over to class, as in all my use-cases, the gain from passing-by-reference as default (in terms of performance) was higher than the benefit of creation on the stack, as they are often passed around into other methods, collections, etc. Another reason is that the memory allocation needed to create an instance depends heavily on implementation, and might be big, depending on your card deck and game (think: Cards in a deck builder game with lot's of properties).
The finite collection of all different cards, in a bunch, is called a deck.
The package provides a generic interface for a deck which holds cards of the generic type TCard
// Interface definition for a generic deck
public interface IDeck<TCard> where TCard : class
{
int NumberOfCardsLeft();
TCard GetFromRemaining(int index);
TCard GetSpecific(TCard specificCard);
void Reset();
};
The dealer is the entity handling the deck. Instead of only providing an interface like for the deck, the library provides a generic implementation of a dealer, which can be specified (i.e., derived from) in order to add more specific functionality:
public class Dealer<TCard> where TCard : class
{
public Dealer(IDeck<TCard> deck) {...}
public Dealer(IDeck<TCard> deck, IRandomNumberGenerator numberGenerator) {...}
public TCard DealCard() {...}
public IReadOnlyCollection<TCard> DealCards(int amount) {...}
public void Shuffle() {...}
}
The dealer can deal one or many cards at once, and shuffle the deck. In order to do that, he needs a deck (duh!). In order to shuffle, he needs some source of randomness. We provide an interface you can implement,
public interface IRandomNumberGenerator
{
public int Next(int upperBound);
}
and a standand implementation (which is just a wrapper holding an instance of System.Random
).
Available at Nuget: EluciusFTW.CardGames.Core.French
This library is an implementation of the core library for the arguably the most well-known playing card: the french-suited playing card.
A french-suited playing card is characterized by two properties: The suit (Diamonds, Hearts, Clubs and Spades) and the symbol (Deuce, Three ... King, Ace), both of which are represented as enums in the library. The value of the card is a numeric value betwen 2 and 14 in bijective relation to it's symbol:
// Using the Suit and Symbol enum
var card = new Card(Suit.Hearts, Symbol.Deuce);
// Using the Value instead of the Symbol
var card = new Card(Suit.Hearts, 8);
There are conventional string representations for these cards, which we support via extensions (resp. implementing the ToString()
method):
// String extension expecting the format {symbol char}{suit char}
var card = "Jc".ToCard();
var serializedCard = card.ToString() // equals "Jc" again.
// String extension expecting one or more cards separated by a space
var cards = "2h 5d Qs".ToCards();
var serializedCards = cards.ToStringRepresentation(); // equals "2h 5d Qs" again.
Since most card games involve players having more than one card, handling collections of cards is needed. We provide several extensions on IEnumerable<Card>
for convenience:
// Get cards by descending value
IReadOnlyCollection<Card> ByDescendingValue(this IEnumerable<Card> cards)
// Get values in several flavors
IReadOnlyCollection<int> Values(this IEnumerable<Card> cards)
IReadOnlyCollection<int> DescendingValues(this IEnumerable<Card> cards)
IReadOnlyCollection<int> DistinctDescendingValues(this IEnumerable<Card> cards)
// Get all distinct suits
IReadOnlyCollection<Suit> Suits(this IEnumerable<Card> cards)
// Check if given values are all in the cards
bool ContainsValue(this IEnumerable<Card> cards, int value)
bool ContainsValues(this IEnumerable<Card> cards, IEnumerable<int> valuesToContain)
// Detemines highest value-duplicates
int ValueOfBiggestPair(this IEnumerable<Card> cards)
int ValueOfBiggestTrips(this IEnumerable<Card> cards)
int ValueOfBiggestQuads(this IEnumerable<Card> cards)
Of course, as we have provided the french-suited card, we also provide some decks containing these cards in this library.
First of all, there is a base class which provides a few more useful methods already using the fact that a french card has a symbol
(resp. value
) and a suit
. The only thing an implementing class must provide is the collection of all cards in the deck:
public abstract class FrenchDeck : IDeck<Cards.French.Card>
{
protected abstract IReadOnlyCollection<Card> Cards();
public IReadOnlyCollection<Card> CardsLeft() {...}
public IReadOnlyCollection<Card> CardsLeftOfValue(int value) {...}
public IReadOnlyCollection<Card> CardsLeftOfSuit(Suit suit) {...}
public IReadOnlyCollection<Card> CardsLeftWith(Func<Card, bool> predicate) {...}
}
There are two implementations in the package:
FullFrenchDeck
: The standard 52-card deck consisting of Deuce-to-Ace of all four suits.ShortFrenchDeck
: A 36-card deck consisting of Six-to-Ace of all four suits (like is used in Short-deck poker).
One could wonder why a dealer cares whyt kind of deck he deals, i.e., why a specific dealer implementation for a given deck makes sense.
In our domain view, the dealer is the owner of the deck, and responsible for dealing the cards. He hence has knowledge about the deck (a dealer can peek if he wants!), and using this knowledge, combined with a specific deck let's us add convenience methods on the dealer.
The FrenchDeckDealer
we provide in this package (including two factory methods for the decks we have defined earlier), can peek into the deck and try to narrow down the cards from which he deals the next, randomly:
// provides a dealer with full deck (or use .WithShortDeck() for a short deck)
var dealer = FrenchDeckDealer.WithfullDeck();
// deals a random card of given value, suit or symbol.
// Succeeds if there are still some in the deck, else fails.
_ = dealer.TryDealCardOfValue(7, out var card);
_ = dealer.TryDealCardOfSymbol(Symbol.King, out var card);
_ = dealer.TryDealCardOfSuit(Suit.Spades, out var card);
This is very useful and increases performance in simulation scenarios where certain specific situations have to be recreated over and over.
This library utilizes the French cards and proceeds to model Poker variants. It is not yet published as a package as it is not yet mature enough.
The library contains domain models for hands in these poker disciplines:
- 5-card draw
- Holdem
- Omaha
- Stud
The Holdem and Omaha hands derive from a more generic hand model called CommunityCardsHand
, which can be used to model any kind of community card hand (any number of community cards, any number of hole cards, any requirement how many of them have to be used for a hand, and how many must at least be used. So in these parameters, a Holdem hand is (3-5, 2, 0, 2) and a Omaha hand (3-5, 4, 2, 2)). So using this as a basis, it is easy to implement, e.g., 5-card PLO and other lesser known variants.
Hands all implement IComparable
, and the operators "<, >" are implemented by default. This is accompished by using two properties of the base class of any hand:
Strength (of type long
) and Type (e.g. HandType.Flush
). The calculations of the strength and type are directly performed when constructing the hand, and they are designed in such a fashion that the ordering of the types can be provided as well (because, e.g., in short deck, a flush beats a full-house). The classical orderign as well as the ordering for short-deck poker are provided in the class HandTypeStrength
.
The library contains models for Holdem (full and short deck) and Stud simulations (currently still in the CardGames.Playground
project, but they will soon move to the CardGames.Poker
project), and other simulations can easily be built in similar fashion. Both Simulations are configurable with a fluent builder pattern. Here's an example of a Holdem simulation configuration:
// any number of players can be added
// each players hole cards can be specified by providing zero, one or two cards
// optionally, a flop/turn/river can be provided
// finally the simulation is executed by calling SimulateWithFullDeck resp. SimulateWithShortDeck
private HoldemSimulationResult RunHoldemSimulation(int nrOfHAnds)
=> new HoldemSimulation()
.WithPlayer("John", "Js Jd".ToCards())
.WithPlayer("Jeremy", "8s 6d".ToCards())
.WithPlayer("Jarvis", "Ad".ToCards())
.WithFlop("8d 8h 4d".ToCards())
.SimulateWithFullDeck(nrOfHAnds);
The Stud simulation works similarly. However, since a player has different kinds of cards, one provides any number of StudPlayers
to the simulation, which have a builder of their own. Here's what that looks like in an example:
// again, any number of players can be specified
// and each players hole and board cards can be specified individually
private StudSimulationResult RunStudSimulation(int nrOfHAnds)
=> new SevenCardStudSimulation()
.WithPlayer(
new StudPlayer("John")
.WithHoleCards("Js Jd".ToCards())
.WithBoardCards("Qc".ToCards()))
.WithPlayer(
new StudPlayer("Jeremy")
.WithHoleCards("3s 4s".ToCards())
.WithBoardCards("7s".ToCards()))
.WithPlayer(
new StudPlayer("Jarvis")
.WithBoardCards("Tc".ToCards()))
.Simulate(nrOfHAnds);
If you want to play around with these simulations, there is a console program in CardGames.Playground.Runner
where you can run any simulation. It also contains some benchmarks (using BenchmarkDotNet), which you can run. In fact, they have been instrumental in finding the right balance between design and performance, big shoutout to them!
The simulation result classes contain a complete collection of all run hands, as well as some predefined queries and aggregations, which can easily be extendend and customized due to the fact that the full collection of hands is available.
Here is a simple printout of the above Stud simulation:
Here is a simple printout of the above Holdem simulation:
This repository also contians two benchmark projects: one for the core packages, one for the poker simulations. These are only meant to be utilities during development in order to test the implementations for their performance, resp. to prevent introduction of performance regressions. Once stable enough, baseline benchmarks might be included in the documentation and in the workflows.
We have switched from manual semantic versioning to using NerdBank.GitVersioning and follow the version scheme: <major>.<minor>.<git-depth>
for out releases. All packages in this repository will have synchronized version numbers.
Note: In particular, the third number in the version does not have the same meaning as the patches in SemVer. Increments in that number may contain breaking changes, in contrast to patch versions in SemVer.
All feedback welcome! All contributions are welcome!