This is following up on my previous attempt, which was admittedly done fast and not-so-well. This code attempts to allow comparing two poker hands to determine a winner, not only evaluating a given set of cards. (this was actually edited - see original take 2 here).
I'm abstracting the dirt away into an interface... and yet it doesn't look very clean to me, but maybe it's just because I don't use Tuple<T1,T2>
very often. T1 is a bool
indicating whether the hand is matched, T2 contains the winning cards.
public interface IPokerHandEvaluator
{
Tuple<bool, IEnumerable<PlayingCard>> IsPair(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsTwoPair(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsThreeOfKind(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsFourOfKind(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsFlush(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsFullHouse(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsStraight(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsStraightFlush(IEnumerable<PlayingCard> cards);
Tuple<bool, IEnumerable<PlayingCard>> IsRoyalFlush(IEnumerable<PlayingCard> cards);
}
This is my current implementation - it correctly evaluates all 5-card hands, and I've tested it correctly evaluates a Straight Flush (ace low) with a 7-card hand as well. So it works, but it doesn't make a good marketing for Tuple<T1,T2>
:
public class PokerHandEvaluator : IPokerHandEvaluator
{
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> GroupByNominalValue(IEnumerable<PlayingCard> cards)
{
return cards.GroupBy(card => card.NominalValue);
}
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> Pairs(IEnumerable<PlayingCard> cards)
{
return GroupByNominalValue(cards).Where(group => group.Count() == 2);
}
private IEnumerable<IGrouping<CardNominalValue, PlayingCard>> Triplets(IEnumerable<PlayingCard> cards)
{
return GroupByNominalValue(cards).Where(group => group.Count() == 3);
}
private IEnumerable<IGrouping<CardSuit, PlayingCard>> Suits(IEnumerable<PlayingCard> cards)
{
return cards.GroupBy(card => card.Suit);
}
public Tuple<bool,IEnumerable<PlayingCard>> IsPair(IEnumerable<PlayingCard> cards)
{
var pairs = Pairs(cards);
var result = pairs.Count() == 1 && !Triplets(cards).Any();
var winningCards = result ? pairs.SelectMany(group => group.Select(card => card)) : null;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsTwoPair(IEnumerable<PlayingCard> cards)
{
var pairs = Pairs(cards);
var result = pairs.Count() == 2;
var winningCards = result ? pairs.SelectMany(group => group.Select(card => card)) : null;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsThreeOfKind(IEnumerable<PlayingCard> cards)
{
var triplets = Triplets(cards);
var result = triplets.Count() == 1 && !Pairs(cards).Any();
var winningCards = result ? triplets.SelectMany(group => group.Select(card => card)) : null;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsFourOfKind(IEnumerable<PlayingCard> cards)
{
var values = GroupByNominalValue(cards);
var result = values.Any(group => group.Count() >= 4);
var winningCards = result ? values.Where(group => group.Count() >= 4).SelectMany(group => group.Select(card => card)) : null;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsFlush(IEnumerable<PlayingCard> cards)
{
var winningSuit = Suits(cards).Where(suit => suit.Count() >= 5);
var result = winningSuit.Any();
var winningCards = result ? winningSuit.SelectMany(group => group.Select(card => card)) : null;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsFullHouse(IEnumerable<PlayingCard> cards)
{
var pairs = Pairs(cards);
var triplets = Triplets(cards);
var result = pairs.Any() && triplets.Any();
if (!result) return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
var winningPair = pairs.OrderByDescending(group => group.Key).FirstOrDefault().Select(card => card);
var winningTriplet = triplets.OrderByDescending(group => group.Key).FirstOrDefault().Select(card => card);
var winningCards = winningPair.Concat(winningTriplet);
return new Tuple<bool, IEnumerable<PlayingCard>>(true, winningCards);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsStraight(IEnumerable<PlayingCard> cards)
{
var distinctValues = GroupByNominalValue(cards);
var isStraightNoAce = IsStraightNoAce(distinctValues);
if (isStraightNoAce.Item1)
{
return new Tuple<bool, IEnumerable<PlayingCard>>(true, isStraightNoAce.Item2);
}
var isStraightWithAce = IsStraightWithAce(distinctValues);
if (isStraightWithAce.Item1.Item1)
{
return new Tuple<bool, IEnumerable<PlayingCard>>(true, isStraightWithAce.Item1.Item2);
}
return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
}
private enum AceKind
{
AceLow,
AceHigh
}
private Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?> IsStraightWithAce(IEnumerable<IGrouping<CardNominalValue, PlayingCard>> distinctValues)
{
// if has no ace, don't bother
if (!distinctValues.Any(group => group.Key == CardNominalValue.Ace)) return new Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?>(new Tuple<bool, IEnumerable<PlayingCard>>(false, null), null);
var ace = distinctValues.Where(group => group.Key == CardNominalValue.Ace).Select(group => group.First()).First();
var aceRemoved = distinctValues.Where(group => group.Key != CardNominalValue.Ace);
var ascending = aceRemoved.OrderBy(value => value.Key).Take(4);
if (ascending.Max(group => group.Key) == CardNominalValue.Five)
{
var winningCards = ascending.Select(group => group.First()).Concat(new List<PlayingCard> {ace});
var result = new Tuple<bool, IEnumerable<PlayingCard>>(true, winningCards);
return new Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?>(result, AceKind.AceLow);
}
var descending = aceRemoved.OrderByDescending(value => value.Key).Take(5);
if (descending.Min(group => group.Key) == CardNominalValue.Ten)
{
var winningCards = descending.Select(group => group.First());
var result = new Tuple<bool, IEnumerable<PlayingCard>>(true, winningCards);
return new Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?>(result, AceKind.AceHigh);
}
return new Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?>(new Tuple<bool, IEnumerable<PlayingCard>>(false, null), null);
}
private Tuple<Tuple<bool, IEnumerable<PlayingCard>>, AceKind?> IsStraightWithAce(IEnumerable<PlayingCard> cards)
{
return IsStraightWithAce(GroupByNominalValue(cards));
}
private Tuple<bool, IEnumerable<PlayingCard>> IsStraightNoAce(IEnumerable<IGrouping<CardNominalValue, PlayingCard>> distinctValues)
{
// if has ace, don't bother
if (distinctValues.Any(group => group.Key == CardNominalValue.Ace)) return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
var sortedValues = distinctValues.OrderBy(group => (int)group.Key);
var possibleLows = sortedValues.Take(5 - sortedValues.Count() + 1);
var skippedCards = 0;
foreach (var possibleLow in possibleLows)
{
var theCards = sortedValues.Skip(skippedCards).Take(5).Select(group => group.First());
var isStraight = theCards.Max(card => card.NominalValue) == possibleLow.Key + 4;
if (isStraight)
{
return new Tuple<bool, IEnumerable<PlayingCard>>(true, theCards);
}
skippedCards++;
}
return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsStraightFlush(IEnumerable<PlayingCard> cards)
{
var isFlush = IsFlush(cards);
if (!isFlush.Item1) return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
var isStraight = IsStraight(cards);
if (!isStraight.Item1) return new Tuple<bool, IEnumerable<PlayingCard>>(false, null);
return new Tuple<bool, IEnumerable<PlayingCard>>(true, isStraight.Item2);
}
public Tuple<bool, IEnumerable<PlayingCard>> IsRoyalFlush(IEnumerable<PlayingCard> cards)
{
var straightWithAceResult = IsStraightWithAce(cards);
var result = IsFlush(cards).Item1 && straightWithAceResult.Item1.Item1 && straightWithAceResult.Item2 == AceKind.AceHigh;
return new Tuple<bool, IEnumerable<PlayingCard>>(result, straightWithAceResult.Item1.Item2);
}
}
So the PokerGame
class is now much more focused at knowing what the rules are and in which order they're evaluated; the dictionary of rules/hands has turned into a private field, there's no need to expose it at all - given the new PokerHand EvaluateHand()
method:
public class PokerGame
{
private readonly IPokerHandEvaluator _evaluator;
private readonly Dictionary<PokerHand, Func<IEnumerable<PlayingCard>, Tuple<bool, IEnumerable<PlayingCard>>>> _hands;
public PokerGame(IPokerHandEvaluator evaluator)
{
_evaluator = evaluator;
_hands = new Dictionary<PokerHand, Func<IEnumerable<PlayingCard>, Tuple<bool, IEnumerable<PlayingCard>>>>
{
{ PokerHand.HighCard, cards => new Tuple<bool, IEnumerable<PlayingCard>>(true, null) },
{ PokerHand.Pair, _evaluator.IsPair },
{ PokerHand.TwoPair, _evaluator.IsTwoPair },
{ PokerHand.ThreeOfKind, _evaluator.IsThreeOfKind },
{ PokerHand.Straight, _evaluator.IsStraight },
{ PokerHand.Flush, _evaluator.IsFlush },
{ PokerHand.FullHouse, _evaluator.IsFullHouse },
{ PokerHand.FourOfKind, _evaluator.IsFourOfKind },
{ PokerHand.StraightFlush, _evaluator.IsStraightFlush },
{ PokerHand.RoyalFlush, _evaluator.IsRoyalFlush }
};
}
public PokerHandEvaluationResult EvaluateHand(IEnumerable<PlayingCard> cards)
{
var winningHand = _hands.OrderByDescending(hand => hand.Key)
.FirstOrDefault(hand => hand.Value.Invoke(cards).Item1).Key;
var redundantCall = _hands[winningHand].Invoke(cards);
var winningCards = redundantCall.Item2;
var otherCards = cards.Where(card => !winningCards.Contains(card));
return new PokerHandEvaluationResult(winningHand, winningCards, otherCards);
}
}
For this to work I had to add PokerHand.HighCard
:
public enum PokerHand
{
HighCard,
Pair,
TwoPair,
ThreeOfKind,
Straight,
Flush,
FullHouse,
FourOfKind,
StraightFlush,
RoyalFlush
}
The CardSuit
enum was reordered, in prevision for when I want to modify this code to compare two hands - the suit would be a tiebreaker:
public enum CardSuit
{
Clubs,
Diamonds,
Hearts,
Spades
}
I also added a PokerHandEvaluationResult
class:
public class PokerHandEvaluationResult : IComparable<PokerHandEvaluationResult>
{
public PokerHand Result { get; private set; }
public IEnumerable<PlayingCard> ResultCards { get; private set; }
public IEnumerable<PlayingCard> HighCards { get; private set; }
public PokerHandEvaluationResult(PokerHand result, IEnumerable<PlayingCard> resultCards, IEnumerable<PlayingCard> highCards)
{
Result = result;
ResultCards = resultCards;
HighCards = highCards;
}
public int CompareTo(PokerHandEvaluationResult other)
{
int result;
if (Result == other.Result)
{
result = ResultCards == null ? 0 : ResultCards.Max().CompareTo(other.ResultCards.Max());
if (result == 0) result = HighCards.Max().CompareTo(other.HighCards.Max());
}
else
{
result = Result.CompareTo(other.Result);
}
return result;
}
}
So I'm not sure whether this code is getting better, or if it's taking a direction that will make me wish we had implemented a Black Jack game this week. I think Tuple
feels like a hack, that the code is begging for something but I can't put my finger on it. Could it be the enum
s shooting myself in the foot?