Project Euler – Problem 54 Solution

Yan Cui

I help clients go faster for less using serverless technologies.

This article is brought to you by

MongoDB 8.0 is here to change the game. Faster reads and inserts, and brand-new vector search to support modern AI-powered apps.

Learn More

Problem

In the card game poker, a hand consists of five cards and are ranked, from lowest to highest, in the following way:

  • High Card: Highest value card.
  • One Pair: Two cards of the same value.
  • Two Pairs: Two different pairs.
  • Three of a Kind: Three cards of the same value.
  • Straight: All cards are consecutive values.
  • 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 value.
  • Straight Flush: All cards are consecutive values of same suit.
  • Royal Flush: Ten, Jack, Queen, King, Ace, in same suit.

The cards are valued in the order:

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

If two players have the same ranked hands then the rank made up of the highest value wins; for example, a pair of eights beats a pair of fives (see example 1 below). But if two ranks tie, for example, both players have a pair of queens, then highest cards in each hand are compared (see example 4 below); if the highest cards tie then the next highest cards are compared, and so on.

Consider the following five hands dealt to two players:

image

The file, poker.txt, contains one-thousand random hands dealt to two players. Each line of the file contains ten cards (separated by a single space): the first five are Player 1’s cards and the last five are Player 2’s cards. You can assume that all hands are valid (no invalid characters or repeated cards), each player’s hand is in no specific order, and in each hand there is a clear winner.

How many hands does Player 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

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
  2. I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
  3. Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.

2 thoughts on “Project Euler – Problem 54 Solution”

  1. Pingback: F# – Use Discriminated Unions instead of class hierarchies | theburningmonk.com

  2. That is a very clean solution. I like the use of the Rank type, much better than my approach. I ended up returning a large number plus the value of the highest card for each ranking.

    For example a straight flush would be 8000 + the value of the highest card.

Leave a Comment

Your email address will not be published. Required fields are marked *