How to: Poker Hand Evaluator in C# implementieren

Pokerhand Evaluator CsharpDas Thema Poker habe ich nun schon in dem ein oder anderen C#-Artikel als Aufhänger genommen, um tiefer in verschiedene Programmierthematiken einzusteigen. So gab es bereits einen Artikel zur Bildanalyse, einen zur Berechnung der Gewinnwahrscheinlichkeit und einen, in dem die beiden eben genannten kombiniert wurden.

Alle drei Artikel haben eines gemeinsam. Sie basieren auf einer Bibliothek, die ermitteln kann, welche Pokerhand in einem direkt Vergleich der Gewinner ist. Solch eine Bibliothek nennt man auch “Hand evaluator”, da sie zwei Pokerhände evaluieren kann. Und genau hier wollen wir heute ansetzen.

In diesem Artikel wollen wir Schritt für Schritt eine eigene C#-Bibliothek zum Vergleich zweier Pokerhände erstellen. Hierzu arbeiten wir uns nach und nach zum Ziel. Beginnen werden wir mit den Datenstrukturen für Spielkarten und Pokerhände. Abschließend geht es an den Abgleich – also die Ermittlung des Gewinners. Die nachfolgende Implementierung ist angelehnt an die Umsetzung von C. Scutaru.

Die Basis – Datenstrukturen für Spielkarte und Pokerhand

Warum nicht “Datenklassen”? Weil wir eben nicht nur Klassen, sondern auch klassische Strukturen nutzen. Für die Abbildung der Spielkarten nehmen wir ein struct, da die Spielkarte nur zwei enums enthält und diese nach der Initialisierung sowieso keine Änderung mehr erfahren.

public enum TypeOfRank : int 
{ 
	Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace 
}

public enum TypeOfSuit : int 
{ 
	Spades, Hearts, Diamonds, Clubs 
}
 
public struct Card
{
	public Card(TypeOfRank rank, TypeOfSuit suit) : this()
 	{ 
 		Rank = rank; 
 		Suit = suit; 
 	}
 
 	public TypeOfRank Rank { get; private set; }
 	public TypeOfSuit Suit { get; private set; }
}

Zuerst definieren wir also die möglichen Kartenwerte (Ranks) und Kartenfarben (Suits) in zwei Aufzählungen (enum), und nutzen diese dann, um unsere Kartenstruktur (struct Card) aufzubauen. Eine Card kann nur beim initialisieren über den Konstruktur mit Farbe und Wert versehen werden. Danach können Farbe und Wert nur noch ausgelesen werden, da der setter mittels “private set” nach außen hin nicht sichtbar ist.

Kommen wir nun zur Pokerhand. Diese implementiert das IComparable-Interface, sodass die Klasse zwingend das eine CompareTo-Methode enthalten muss. Zudem implementieren wir noch eine Sort-Methode.

public class PokerHand : IComparable<PokerHand>
{
 	public Card[] Cards { get; private set; }
 
	public PokerHand(Card c1, Card c2, Card c3, Card c4, Card c5)
 	{ 
 		Cards = new Card[] {c1, c2, c3, c4, c5}; 
 		Sort(); 
 	}
 
	private void Sort()
	{
 		Cards = Cards.OrderBy(c => c.Rank)
  					 .OrderBy(c => Cards.Where(c1 => c1.Rank == c.Rank)
  					 .Count()).ToArray();
 
 		if (Cards[4].Rank == TypeOfRank.Ace 
 		    && Cards[0].Rank == TypeOfRank.Two
  			&& (int)Cards[3].Rank - (int)Cards[0].Rank == 3)
  		{
  			Cards = new Card[] { Cards[4], Cards[0], Cards[1], Cards[2], Cards[3] };
  		}
  		
	}
 
	public int CompareTo(PokerHand other)
	{
 	for (var i = 4; i >= 0; i--)
 	{
  		TypeOfRank rank1 = Cards[i].Rank, 
  				   rank2 = other.Cards[i].Rank;
  		if (rank1 > rank2)
  			return 1;
  		if (rank1 < rank2)
  			return -1;
 	}
 	return 0;
}

Das Herzstück der PokerHand-Klasse ist das Cards-Array. Dieses enthält die Spielerkarten (vom eben erstellen Card-Struct-Typ). Befüllt wird das Cards-Array im Konstruktur woraufhin es mit der Sort-Methode sortiert wird.

Die Sort-Methode ist die erste von zwei Funktionen der PokerHand-Klasse. Sie sortiert die Karten ihrem Wert nach. Durch den zweiten Teil des Lambda-Ausdrucks (“.OrderBy(c = Cards.Where…”) werden Pärchen und Drillinge höher einsortiert. Die abschließende if-Abfrage prüft, ob eine Straße vorliegen kann und schiebt ggf. das Ass an den Anfang der Hand.

Die CompareTo-Methode erlaubt nun ein einfaches Vergleichen zweier Pokerhände, da diese bereits durch die Sort-Methode ihrem Wert nach korrekt sortiert sind. Die genaue Wertreihenfolge kann zum Beispiel auf pokervergleich.net nachgeschlagen werden, wo über Poker, Pokeranbieter und Poker Boni informiert wird.

Implementierung der Pokerhand-Typen

Was fehlt bisher? Na klar, die einzelnen Pokerhand-Typen wie Flush, Full house oder Straight. Da es sich hierbei wieder um gegebene Werte/Kombinationen handelt, greifen wir wieder auf den enum-Typ zurück. Dabei starten wir mit der stärksten und enden mit der schwächsten Hand.

public enum HandType : int 
{ 
	RoyalFlush, StraightFlush, FourOfAKind, FullHouse, Flush, Straight, ThreeOfAKind, TwoPairs, OnePair, HighCard 
}

Da es sich bei dem enum, jedoch nur um eine numerische Auflistung handelt, müssen wir noch definieren, was den jeweiligen Typen eigentlich ausmacht. Hierzu schreiben wir einen Validator – namentlich die IsValid-Funktion.

public bool IsValid(HandType handType)
{	
	switch (handType) 
	{
  		case HandType.RoyalFlush: 
   			return IsValid(HandType.StraightFlush) && Cards[4].Rank == TypeOfRank.Ace;
  		case HandType.StraightFlush:
   			return IsValid(HandType.Flush) && IsValid(HandType.Straight);
  		case HandType.FourOfAKind:
   			return GetGroupByRankCount(4) == 1;
  		case HandType.FullHouse:
   			return IsValid(HandType.ThreeOfAKind) && IsValid(HandType.OnePair);
  		case HandType.Flush:
   			return GetGroupBySuitCount(5) == 1;
  		case HandType.Straight:
   			return (int)Cards[4].Rank - (int)Cards[0].Rank == 4
    				|| Cards[0].Rank == TypeOfRank.Ace;
  		case HandType.ThreeOfAKind:
   			return GetGroupByRankCount(3) == 1;
  		case HandType.TwoPairs:
   			return GetGroupByRankCount(2) == 2;
  		case HandType.OnePair:
   			return GetGroupByRankCount(2) == 1;
  		case HandType.HighCard:
   			return GetGroupByRankCount(1) == 5;
 	}
 	return false;
}
 
private int GetGroupByRankCount(int n)
{ 
	return Cards.GroupBy(c => c.Rank).Count(g => g.Count() == n); 
}
 
private int GetGroupBySuitCount(int n)
{ 
	return Cards.GroupBy(c => c.Suit).Count(g => g.Count() == n); 
}

Mittels einer Switch-Case-Abfrage werden die einzelnen Handtypen durchgegangen und gemäß der Pokerregeln gegen die in der Hand befindlichen Karten (im Cards-Array) geprüft. Stimmt der übergebene Hand-Typ mit den Karten im Cards-Array überein, gibt die IsValid-Methode ein “true” zurück.

Der eigentliche Hand-Evaluator

Der letzte Teil der Implementierung besteht aus der Ausprogrammierung der Evaluate-Methode vom Interface-Typ IList. Die Evaluate-Methode nimmt ein Dictionary vom Typ <string, Pokerhand> an, wobei als String der Name des jeweiligen Handbesitzers angegeben werden sollte. Der Rückgabewert der Funktion entspricht einer Namensliste der Gewinner.

public static IList<string> Evaluate(IDictionary<string, PokerHand> hands)
{
	var len = Enum.GetValues(typeof(HandType)).Length;
	var winners = new List<string>();
	HandType winningType = HandType.HighCard;
 
 	foreach (var name in hands.Keys)
 	{
 		for (var handType = HandType.RoyalFlush;
   			(int)handType < len; handType = handType + 1) { var hand = hands[name]; if (hand.IsValid(handType)) { int compareHands = 0, compareCards = 0; if (winners.Count == 0 || (compareHands = winningType.CompareTo(handType)) > 0
	     			|| compareHands == 0
		      		&& (compareCards = hand.CompareTo(hands[winners[0]])) >= 0)
		    	{
		      		if (compareHands > 0 || compareCards > 0)
		      			winners.Clear();
		      		winners.Add(name);
		      		winningType = handType;
	    		}
		    	break;
		   	}
  		}
 	}  
 	return winners;
}

Innerhalb der Evaluate-Methode wird für jeden Spieler die gegebene Hand gegen alle möglichen Hände mittels der IsValid-Methode geprüft. Die ermittelte Hand wird wiederum gegen die Hand des/der Spieler in der Gewinnerliste (winner) geprüft. Ist die aktuelle Hand besser wird, die Gewinnerliste geleert und der aktuelle Spieler eingesetzt. So bleiben am Ende ein (oder bei gleichen Händen / Split-Pot) mehrere Gewinner, die von der Evaluate-Methode zurückgegeben werden.

Raffi

Seit 2011 blogge ich hier über Programmierung, meine Software, schreibe Tutorials und versuche mein Wissen, so gut es geht, mit meinen Lesern zu teilen.

1 Kommentare

  1. Vielen Dank für das Lehrreiche Tutorial! Ich finde es immer Klasse, wenn komplexe Probleme anhand eines einfachen “real life” Beispieles implementiert werden können. Selbst fehlen einem oft die Ideen dazu, desshalb finde ich dieses Beispiel top ;)

Schreibe einen Kommentar zu Andreas Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.