Paired

After the last section of work on the kata, I had a mild sense of achievement. That's now been slightly enhanced.

The kicker

As mentioned last time, although all the tests passed, I had a feeling that didn't mean I was done with identifying winning hands containing pairs. I added the following two tests:

  • Both hands having a different pair, but the lower pair hand had a higher kicker (e.g. an Ace). Regardless, the higher pair should win.
  • Both hands having the same pair (fives) so the winner should be the one with the highest kicker.

Both these tests failed.

Naïve

The culprit is the rather naïve implementation of the JointHandResolver. All this did was to find the highest card from all the identically-ranked hands and then decide the winner being the hand(s) containing that highest card.

This was one of those implementations where it was simple enough to get the test to pass, knowing that I'd have to revisit it at some point. So here we arrive at that point.

Thinking about how this should work, we want to first consider the 'ranking' cards; that is the ones making up the pair, three-of-a-kind, etc. The highest-valued pair (or three-of-a-kind, etc) should determine the winner. If there's more than one winner after considering these cards, then we move onto the remaining cards: aka the kickers. These should be sorted highest-to-lowest in a simple knockout. So in the case of a pair, there will be three kickers. For each in turn if they are not the same face-value, the hand with lowest card is eliminated. This should repeat until either we have one hand remaining - I.e. the winner - or there are no more cards, in which case all remaining hands will be the winner.

Simplification?

This seems pretty complicated. Let's see if it can be simplified a little.

If the five best cards making up the hands are ordered by the 'ranked' cards, followed by the kickers (ordered high-to-low), can the JointHandResolver simply look at the first card of each hand and remove any hand where it's card is not the highest. Then, repeat this for the next card until we have one winner, or all cards have been compared.

Um, sounds simple. Does it work for a pair: consider these three hands (the first set shows the hole cards followed by the community cards; the cards in parentheses are re-ordered as described previously: ranked cards followed by kickers):

5d 7c 2c 3s Qd Th 5s (5d 5s Qd Th 7c)
5c Jc 2c 3s Qd Th 5s (5c 5s Qd Jc Th)
2d Ah 2c 3s Qd Th 5s (2d 2c Ah Qd Th)

Taking each card in turn does work for these hands. The last hand is eliminated during the first pass as 2 is less than 5. Then the first hand is eliminated at the fourth pass (card) due to the Jack beating the ten. Looks good for pairs.

It's worth thinking ahead slightly: will it work for all rankings?

  • Two pairs: there's kind of three segments to this; 1) the highest pair 2) the lower pair 3) the highest kicker. Assuming the ranked cards are in this order, the JointHandResolver implementation described should be OK.
  • Three-of-a-kind: yes.
  • Straight: As the flush, sort in face-value order.
  • Flush: Just sort in face-value order, no problem.
  • Full-house: Similar to two pairs, except the three-of-a-kind should always be the first set of cards (something to consider when sorting these in the identifier implementation).
  • Four-of-a-kind: yes.
  • Straight flush: yes.

The Code

It seems this should work for all the hands, so into the code to fix the initial set of tests which were added to make the JointHandResolver fail.

The main resolve method now looks like this:

public Collection<RankedHand> resolveHighestHands(Collection<RankedHand> identicallyRankedHands) {
  Collection<RankedHand> winners = new ArrayList<RankedHand>(identicallyRankedHands);
  int numberOfCards = Math.min(5, winners.iterator().next().allCards().size());
  for (int i = 0; i < numberOfCards; i++) {
    if (winners.size() == 1) break; // we have the winner
    removeWhereCardIsNotTheWinner(winners, i);
  }
  return winners;
}

In my opinion, that works quite well at describing our intention. The next method to take a quick look at is removeWhereCardIsNotTheWinner:

private void removeWhereCardIsNotTheWinner(Collection<RankedHand> hands, int cardIndex) {
  Map<Integer, Collection<RankedHand>> cardValues = cardValuesFor(hands, cardIndex);
  int highestCard = Collections.max(cardValues.keySet());
  removeWhereCardValueIsNot(hands, cardIndex, highestCard);
}

Again, relatively simple. Find the highest card from the hands and remove a hand if it's not the highest card. The remove method looks like this:

private void removeWhereCardValueIsNot(Collection<RankedHand> hands, int cardIndex, int highestCard) {
  hands.removeAll(Iterables.where(hands, new CardsFaceValueIsNot(cardIndex, highestCard)));
}

As this is already quite long, I'm not going to list the helper methods/classes (such as cardValuesFor - which simply does a groupBy on the cards at the given index). Just follow the Git link for the complete listing.

The Wrap

This completes the tests for pairs, for which I heave a sigh of relief. Until I find the next hand which fails of course! There is one relatively big outstanding issue which is around hole cards / community cards. As noted in the issues, the current implementation doesn't model this at all - there is simply a map of players to a list of cards. This has been annoying me for a while and now seems to be a good time to tackle it.

After that, it should be a "simple" matter of implementing identifier classes for all the remaining hands. The end seems a bit closer all of a sudden.


Comment Guidelines
See the FAQ for details on the full rules and guidelines. No Spam. Write clearly and thoughtfully - no bad language.