So, we have Cards with Suits and Ranks, we have a Deck, and we can draw random cards from it! Now it’s time to get some cards into the hands of the players!
Hands
Remember the rule we focused on last time:
A poker hand consists of 5 cards dealt from the deck.
We didn’t do anything with a poker hand yet, so let’s go and create it. We start off simple with our first failing test:
1 2 3 4 5 6 |
class HandTests : XCTestCase { func testEmptyHand() { let hand: Hand = Hand(cards: []) XCTAssert(hand.numberOfCards == 0) } } |
Next, the implementation to make our test pass, which is easy enough. We give the Hand a set of cards, and the Hand number should be 0:
1 2 3 4 5 6 7 |
public struct Hand { public var numberOfCards: Int { return 0 } init(cards: [Card]) { } } |
The test passes, but we haven’t really done anything yet, so lets continue with another test.
What test could we add to make the implementation more sensible? One that tests one card instead of none, why not.
1 2 3 4 |
func testOneCardInHand() { let hand: Hand = Hand(cards: [Card(suit: .Club, rank: .Ace)]) XCTAssert(hand.numberOfCards == 1) } |
We have to change our code a bit:
1 2 3 4 5 6 7 8 9 10 11 |
public struct Hand { private let cards: [Card] public var numberOfCards: Int { return cards.count } init(cards: [Card]) { self.cards = cards } } |
Nothing special here. We add an array, we store the parameter from the constructor into the property and in the calculated property numberOfCards we return cards.count. Test passes 🙂
So how are we going to determine which hand is better? We need some kind of evaluation. Lets look at the rules again to see what they say.
Poker hands are ranked by the following partial order from lowest to highest: High Card, Pair, Two Pair, Three of a Kind, Straight, Flush, Full House, Four of a Kind, Straight Flush, (and Royal Flush).
Alright. Lets start with the first one, the High Card.
High Card
Hands which do not fit any higher category are ranked by the value of their highest card. If the highest cards have the same value, the hands are ranked by the next highest, and so on.
Lets first write a test that evaluates our hand to a high card. When is this the case? Well, when a hand only has one card, it is a high card by definition. Lets write a test to reflect this.
1 2 3 4 5 |
func testSingleCardHandStrengthIsHighCard() { let hand = Hand(cards: [Card(suit: .Club, rank: .Ace)]) XCTAssert(hand.evaluate() == .HighCard) } |
Looks easy enough, but I made some presumptions. I’d thought to make an enum for the hand strengths we have, one of them being .HighCard. We need an evaluate function that returns that .HighCard.
Heres how the enum looks:
1 2 3 |
public enum HandStrength: Int { case EmptyHand, HighCard, Pair, TwoPair, ThreeOfAKind, Straight, Flush, FullHouse, FourOfAKind, StraightFlush } |
Note that we back the HandStrength with an Int. This way we can use comparison, which means that HighCard < Pair and StraightFlush > Straight. Now lets create the evaluation function, which is super straightforward for now:
1 2 3 4 5 |
... public func evaluate() -> HandStrength { return .HighCard } ... |
Easy does it!
But what if we compare two hands, both of which are high cards? Our evaluate function will just return us that the hand is of strength high card, so if we compare the two, they would be equal. However, it doesn’t differentiate between a high card ten and high card ace, while according to the rules the ace would win right?
So we need something to compare hands with based on more than the hand strength. Lets write a test to capture this.
1 2 3 4 5 6 |
func testThatHighCardAceBeatsHighCardTen() { let handA: Hand = Hand(cards: [Card(suit: .Club, rank: .Ace)]) let handB: Hand = Hand(cards: [Card(suit: .Club, rank: .Ten)]) XCTAssert(handA > handB) } |
Now it gets interesting! What is going on here? How are we going to make this test pass? The compiler gives us a hint already. It says “Binary operator ‘>’ cannot be applied to two ‘Hand’ operands”. How can we solve this? We need some way to make two hands comparable. Whats great about Swift is that there is a protocol for this. It is called Comparable.
Lets look at how to implement it. We are first going to create an extension on Hand and make it conform to Comparable:
1 |
extension Hand: Comparable {} |
XCode again will give us a hand here; it gives an error and a possible fix, so lets go with that. It provides us two stubs for functions. Looks familiar doesn’t it?
1 2 |
public static func ==(lhs: Hand, rhs: Hand) -> Bool { } public static func <(lhs: Hand, rhs: Hand) -> Bool { } |
Comparable actually inherits from the protocol Equatable, and so the ‘==’ actually is required by Equatable instead. Let’s take a step back and define a test for this first, commenting out our testThatHighCardAceBeatsHighCardTen test for a moment and removing the extension adopting Comparable for a bit.
Here is our new test. Two hands are the same if they hold cards that have the same value:
1 2 3 4 5 6 |
func testThatHighCardsWithEqualRanksAreEqual() { let handA: Hand = Hand(cards: [Card(suit: .Club, rank: .Ace)]) let handB: Hand = Hand(cards: [Card(suit: .Heart, rank: .Ace)]) XCTAssert(handA == handB) } |
Now lets adopt Equatable for hand to pass our test:
1 2 3 4 5 |
extension Hand: Equatable { public static func ==(lhs: Hand, rhs: Hand) -> Bool { return lhs.cards[0].rank == rhs.cards[0].rank } } |
This makes our test pass, but as soon as our hand holds more cards we are going to be in trouble. Lets add another test to capture this:
1 2 3 4 5 6 |
func testThatHandsWithTwoCardsWithEqualRanksAreEqual() { let handA: Hand = Hand(cards: [Card(suit: .Club, rank: .Ace), Card(suit: .Diamond, rank: .Ten)]) let handB: Hand = Hand(cards: [Card(suit: .Heart, rank: .Ace), Card(suit: .Spade, rank: .Ten)]) XCTAssert(handA == handB) } |
And so we have to loop over the cards in the hands and compare each one as such, where we use a handy function in swift called zip, which creates an array of tuple pairs for each entry of handA and B together:
1 2 3 4 5 6 7 8 9 |
public static func ==(lhs: Hand, rhs: Hand) -> Bool { for (lhsCard, rhsCard) in zip(lhs.cards, rhs.cards) { if lhsCard.rank != rhsCard.rank { return false } } return true } |
But we aren’t there yet. If we change our test a little, we can see why.
1 2 3 4 5 6 |
func testThatHandsWithTwoCardsInArbitraryOrderWithEqualRanksAreEqual() { let handA: Hand = Hand(cards: [Card(suit: .Club, rank: .Ace), Card(suit: .Diamond, rank: .Ten)]) let handB: Hand = Hand(cards: [Card(suit: .Spade, rank: .Ten), Card(suit: .Heart, rank: .Ace)]) XCTAssert(handA == handB) } |
In order to fix this, we simply need to order our cards before comparing them. We define a sort function which sorts our cards high to low, and use that to sort both lhs.cards and rhs.cards using sorted(by:):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static func ==(lhs: Hand, rhs: Hand) -> Bool { let sortByRank: (Card, Card) -> Bool = { cardA, cardB in return cardA.rank.rawValue > cardB.rank.rawValue } let lhsCards = lhs.cards.sorted(by: sortByRank) let rhsCards = rhs.cards.sorted(by: sortByRank) for (lhsCard, rhsCard) in zip(lhs.cards, rhs.cards) { if lhsCard.rank != rhsCard.rank { return false } } return true } |
Our test passes! We can actually simplify this a bit further, so lets refactor a bit. Using what we’ve just learned, we can define Comparable for Card.
1 2 3 4 5 |
extension Card: Comparable { public static func <(lhs: Card, rhs: Card) -> Bool { return lhs.rank.rawValue < rhs.rank.rawValue } } |
We can now replace our sort function in the Hand extension for Equatable as such, because operators are actually defined as a function!
1 2 3 4 5 6 7 8 9 10 11 12 |
public static func ==(lhs: Hand, rhs: Hand) -> Bool { let lhsCards = lhs.cards.sorted(by: >) let rhsCards = rhs.cards.sorted(by: >) for (lhsCard, rhsCard) in zip(lhs.cards, rhs.cards) { if lhsCard.rank != rhsCard.rank { return false } } return true } |
Awesome. How does this work? We’ve defined functions for == and < , so Swift is now able to infer the other cases as well; >=, < =, > and !=. Lets check if our tests still run.
Green!
Remember we still have one commented test. Lets uncomment it. You probably already figured now how to make this test pass; we have to adopt Comparable on Hand again. It’s slightly different from the Equatable function; we’re only interested in cases where the ranks are different, hence we check if two cards have the same rank, and if so, we continue.
Note that we actually have to compare rank (lhsCard.rank == rhsCard.rank), as our == function on Card also compares suit, and would give false where the ranks are the same but the suits are not. If they’re not the same, then we return lhsCard < rhsCard. If all cards are the same, then < is obviously false:
1 2 3 4 5 6 7 8 9 10 11 12 |
extension Hand: Comparable { public static func <(lhs: Hand, rhs: Hand) -> Bool { let lhsCards = lhs.cards.sorted(by: >) let rhsCards = rhs.cards.sorted(by: >) for (lhsCard, rhsCard) in zip(lhsCards, rhsCards) { if lhsCard.rank == rhsCard.rank { continue } return lhsCard < rhsCard } return false } } |
And our test passes! Great!
We’ve covered a lot again, making use of zip and sorted(by:) and implementing Comparable to make things easier for us. Next time we’re going a bit further to see if we can detect any other hand strengths as well, like Pairs, Straights and Flushes.