Project Euler — Problem 54 Solution

Problem

In the card game pok­er, a hand con­sists of five cards and are ranked, from low­est to high­est, in the fol­low­ing way:

  • High Card: High­est val­ue card.
  • One Pair: Two cards of the same val­ue.
  • Two Pairs: Two dif­fer­ent pairs.
  • Three of a Kind: Three cards of the same val­ue.
  • Straight: All cards are con­sec­u­tive val­ues.
  • Flush: All cards of the same suit.
  • Full House: Three of a kind and a pair.
  • Four of a Kind: Four cards of the same val­ue.
  • Straight Flush: All cards are con­sec­u­tive val­ues of same suit.
  • Roy­al Flush: Ten, Jack, Queen, King, Ace, in same suit.

The cards are val­ued in the order:

2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King, Ace.

If two play­ers have the same ranked hands then the rank made up of the high­est val­ue wins; for exam­ple, a pair of eights beats a pair of fives (see exam­ple 1 below). But if two ranks tie, for exam­ple, both play­ers have a pair of queens, then high­est cards in each hand are com­pared (see exam­ple 4 below); if the high­est cards tie then the next high­est cards are com­pared, and so on.

Con­sid­er the fol­low­ing five hands dealt to two play­ers:

image

The file, poker.txt, con­tains one-thou­sand ran­dom hands dealt to two play­ers. Each line of the file con­tains ten cards (sep­a­rat­ed by a sin­gle space): the first five are Play­er 1’s cards and the last five are Play­er 2’s cards. You can assume that all hands are valid (no invalid char­ac­ters or repeat­ed cards), each player’s hand is in no spe­cif­ic order, and in each hand there is a clear win­ner.

How many hands does Play­er 1 win?

Solution

open System.IO

// define the suits
type Suit = | Heart | Club | Spade | Diamond

// define the card values
type CardValue =
    | ValueCard of int | Jack | Queen | King | Ace
    // define the + unary operator on the CardValue type
    static member (~+) (value:CardValue) =
        match value with
        | ValueCard n -> if n < 10 then ValueCard (n+1) else Jack
        | Jack -> Queen | Queen -> King | King -> Ace | Ace -> ValueCard(2)

// define the Card type
type Card = { Value:CardValue; Suit:Suit }

// define the ranks
type Rank =
    | HighCard of CardValue
    | OnePair of CardValue
    | TwoPairs of CardValue * CardValue
    | Three of CardValue
    | Straight of CardValue // highest value card
    | Flush
    | FullHouse of CardValue * CardValue
    | Four of CardValue
    | StraightFlush of CardValue // highest value card & suit
    | RoyalFlush

// define function to check if cards are the same suit
let isSameSuit (cards:Card list) = cards |> List.forall (fun c -> c.Suit = cards.Head.Suit)

// define function to get the highest card
let getHighestCard(cards:Card list) = cards |> List.maxBy (fun card -> card.Value)

// define function to check if cards are a straight set
let isStraight (cards:Card list) =
    let flag = cards |> List.sort |> Seq.windowed 2
    |> Seq.forall (fun [|card1; card2|] -> card2.Value = +card1.Value)

    (flag, getHighestCard cards)

// define function to get the duplicated cards
let getDupes (cards:Card list) =
    cards
    |> Seq.groupBy (fun card -> card.Value)
    |> Seq.map (fun (k, seq) -> (k, Seq.length seq))
    |> Seq.filter (fun (k, len) -> len > 1)
    |> Seq.toList

// evaluate the given hand of cards to return the rank
let evaluateHand (cards:Card list) =
    let (isStraight, highCard) = isStraight cards
    let sameSuit = isSameSuit cards
    
    if sameSuit && isStraight && highCard.Value = Ace then RoyalFlush
    else if sameSuit && isStraight then StraightFlush(highCard.Value)
    else if sameSuit then Flush
    else if isStraight then Straight(highCard.Value)
    else
        let dupes = getDupes cards
        if dupes.Length = 0 then HighCard(highCard.Value)
        else if dupes.Length = 1 then
            match dupes.[0] with
            | (cardValue,2) -> OnePair(cardValue)
            | (cardValue,3) -> Three(cardValue)
            | (cardValue,4) -> Four(cardValue)
        else
            match dupes with
            | [(cardValue',2);(cardValue'',2)] -> TwoPairs(cardValue', cardValue'')
            | [(cardValue',3);(cardValue'',2)] -> FullHouse(cardValue', cardValue'')
            | [(cardValue',2);(cardValue'',3)] -> FullHouse(cardValue'', cardValue')

// define function to check if player 1 wins
let isP1Winner (p1:Card list) (p2:Card list) =
    let p1Rank, p2Rank = evaluateHand p1, evaluateHand p2

    if p1Rank > p2Rank then true
    else if p1Rank = p2Rank then
        let rec compareHighCard (p1':Card list) (p2':Card list) =
            let p1'HighCard, p2'HighCard = getHighestCard p1', getHighestCard p2'
            if p1'HighCard.Value > p2'HighCard.Value then true
            else if p1'HighCard.Value = p2'HighCard.Value then
                let p1'' = p1' |> List.filter (fun c -> c.Value < p1'HighCard.Value)
                let p2'' = p2' |> List.filter (fun c -> c.Value < p2'HighCard.Value)
                compareHighCard p1'' p2''
            else false

        compareHighCard p1 p2
    else false

// define function to parse, say, "3D" into a 3 of Diamond
let parseCard &#91;|(value:char);(suit:char)|&#93; =
    let cardValue =
        match value with
        | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' -> ValueCard(int(value.ToString()))
        | 'T' -> ValueCard(10) | 'J' -> Jack | 'Q' -> Queen | 'K' -> King | 'A' -> Ace
    let suit =
        match suit with
        | 'S' -> Spade | 'H' -> Heart | 'D' -> Diamond | 'C' -> Club
    { Card.Value=cardValue; Suit=suit }

// get the array of all the dealt hands from the poker text file
let hands =
    File.ReadAllLines(@"C:\TEMP\poker.txt")
    |> Array.map (fun str -> 
        str.Split(' ') 
        |> Array.map (fun str -> parseCard (str.ToCharArray())))

// separate the hands into two different arrays, one for player 1 and the other player 2
let p1Hands = hands |> Array.map (fun cards -> cards |> Seq.take 5 |> Seq.toList)
let p2Hands = hands |> Array.map (fun cards -> cards |> Seq.skip 5 |> Seq.toList)

let answer =
    Array.map2 (fun p1 p2 -> isP1Winner p1 p2) p1Hands p2Hands
    |> Array.filter (fun b -> b)
    |> Array.length