A Poker evaluator, but troubling Ord instance

I am trying to implement a poker hand evaluator, and it has gone good so far! As of right now, it’s possible to identify complete hands (5 cards), and kickers where needed. Main business is shown below (but here’s a link to the source):

data Card = Card {rank :: Rank, suit :: Suit}

-- A hand with it's highest rank(s), and kickers where needed
data Hand
  = High Rank [Rank]
  | Pair Rank [Rank]
  | TwoPair Rank Rank (Maybe Rank)
  | ThreeKind Rank [Rank]
  | Straight Rank
  | Flush Rank
  | FullHouse Rank Rank
  | FourKind Rank
  | StraightFlush Rank
  deriving (Show)

identifyHands :: [Card] -> [Hand]
identifyHands cs =
  straightFlushes cs
    <++ catMaybes [fourKind cs]
    <++ fullHouses cs
    <++ flushes cs
    <++ straights cs
    <++ threeKinds cs
    <++ twoPairs cs
    <++ pairs cs
    <++ [high cs]
  where
    [] <++ xs = xs
    xs <++ _ = xs

Now I need to compare the identified hands, but the Ord instance will be very, very long and repetitive as I need to compare “all” combinations. Looking at my Hand data type, it doesn’t offer any Enum instances, so it’s hard. Do you have any suggestion as to how I can make modifications so that it would be easier to compare hands?

Could a deriving Ord instance do most of the work? Especially if each constructor of the Hand type had a custom type as its payload. Then you could write the logic to compare like Hands (the Ord instance of those custom types) and the deriving Ord instance for Hand handles ranking the types of Hands.

3 Likes

I tried simply deriving Ord, and I was super surprised to see it actually working exactly as it should. I mean, look at this:

ghci> FullHouse Three Two > FullHouse Two  Three
True
ghci> FullHouse Three Two < FullHouse Two  Three
False

It even figured out that a full house with higher three kinds beats one with lower! And:

ghci> High King [Ace, Jack] > High King [Ace, Two]
True
ghci> High King [Ace, Jack] > High King [Ace, King]
False

This is really, really cool. I didn’t know exactly what deriving Ord would do, since the implementation is hidden, so I didn’t think of it as an option. What did your intuition say that deriving Ord would do?

Thank you!

1 Like

LYAH has a good explanation (if you scroll down). That’s where I learned it. There’s also a specification here.

EDIT: Oh I guess I should summarize. It first compares constructors. Earlier-declared ctors are “lower.” On a tie, it compares the product fields left-to-right using their Ord instances.

4 Likes

Yeah, that makes sense! This will be super useful to remember. Huge thanks!

3 Likes

If it comes to a kicker in a three-of-a-kind vs. three-of-a-kind, get your revolver ready because there is a cheat at the table! Same for FullHouse.

On the other hand Flush and StraightFlush is missing an important bit, as if the rank is the same, Suits are compared.

2 Likes

Oh really good catch with the three kinds! Regarding full house, I actually made it so the first Rank is the three kind, the second rank is the pair (which worked out nicely with Ord!)

I’m not sure what you mean with the last thing, comparing suits? Same rank flushes are just ties (or “chops”), aren’t they?

1 Like

Correct, I remembered wrong!

The Haskell Language report specifies how Ord instances must be derived in section 11.1.

1 Like

Interestingly I am also writting a Poker game in Haskell. I did the same as you: deriving Ord. Then I wrote some test and surprise!! It turns out that there are some nuances which made (in my case) the derived instance not good.

  • The TwoPair is probably wrong. TwoPair Three Seven > TwoPair Two Eight up to the derived instance, but in the actual game is the other way.
  • Be carefull with the Straight as one starting in Ace is smaller than one starting in Two despite of Ace being higher ranked than Two
  • Notice the Flush needs to compare all cards, not just the highest ranked card, but from your representation it seems you are looking only the highest card.
2 Likes

This is where custom Ord for the product component makes sense. Encapsulating these hands in their own records makes adding this logic easy, but you can keep the sun type derived Ord.

1 Like

Oh right, the TwoPair. So is it possible to complement the Ord instance in such a way to take this into account?

This sounds like complementing the instance! Could you elaborate? Are you talking like a data “DoubleRank”, for which we define an Ord instance?

Yes - for the TwoPair case, you can use a custom type that compares the pair properly using rank sorting instead of using the order in the data type. Then you don’t have to worry about the order you construct the TwoPair case.

1 Like

What do you mean with “tank sorting”?
Edit: oh, rank.

1 Like

Depends on the game, right? It’s perfectly plausible in Texas Hold 'Em, or other variants with shared cards.

1 Like

I’m a bit puzzled as it seems to me that three-of-a-kind can only come down to a kicker in a variant with shared cards (or wild cards, or …) and full house can never come down to a kicker (there isn’t one)!

1 Like

Oh you’re right, there’s trips!

Looking at my Hand data type, it doesn’t offer any Enum instances, so it’s hard.

I don’t know the rules of poker and whether the derived instances do what you want. And perhaps an Ord instance suffices for you and you do not need an Enum instance.
But I want to mention that you can also derive Enum in a marginally more complex way than Ord.

The generic-data package can help you to derive Enum for both Hand and Card if their fields (Rank and Suit) are finite (Bounded). This is explained in the documentation of the FiniteEnum option. You can expand the Details section in the documentation to see an example.

There is also a FiniteEnumeration newtype, which can be used with DerivingVia. Again the documentation of FiniteEnumeration gives you an example how to use it, if you uncollapse it.

For your code this looks like

{-# language DerivingVia #-}
{-# language DeriveGeneric #-}

import Generic.Data

data Rank = Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Ace
  deriving (Enum, Bounded, Show, Eq, Ord)

data Suit = Diamonds | Spades | Clubs | Hearts
  deriving (Enum, Bounded, Show, Eq, Ord)

data Card = Card {rank :: Rank, suit :: Suit}
  deriving (Generic, Show, Eq, Ord)
  deriving (Enum, Bounded) via (FiniteEnumeration Card )

You can reorder the fields to change the order of the derived enumeration. It should match the order of the derivable Ord instance.

This also works for Hand if you replace [] and Maybe with finitely enumerable variants, i.e. a type Foo with

instance Bounded a => Bounded (Foo a)
instance Enum a => Enum (Foo a)

The rest already works:

data Hand
--   = High Rank (FiniteList Rank)
--   | Pair Rank (FiniteList Rank)
--   | TwoPair Rank Rank (Maybe Rank)
--   | ThreeKind Rank [Rank]
  = Straight Rank
  | Flush Rank
  | FullHouse Rank Rank
  | FourKind Rank
  | StraightFlush Rank
  deriving (Generic, Show, Eq, Ord)
  deriving (Enum, Bounded) via (FiniteEnumeration Hand)
3 Likes