Day 7 was a nice little puzzle, and thankfully much simpler than it could have been! Things would have been much tricker if the possible hands had includes straights and the like. My solution was a little verbose, but that's mainly because there are a lot of cards and types of hand.
Representation and parsing
My first decision was to be explicit about how I represented the data. I defined my own data type for Card
values and hand classifications (HandClass
), both in order from lowest to highest. That meant I could more easily do comparisons and sorting. I also defined data types for a Hand
(cards and the bid) and a ClassifiedHand
(including the HandClass
). Again, these are arranged so that the default sort does the right thing.
data Card = Two | Three | Four | Five | Six | Seven | Eight | Nine |
Ten | Jack | Queen | King | Ace deriving (Eq, Ord, Show)
data HandClass = HighCard | OnePair | TwoPair | ThreeOfAKind | FullHouse |
FourOfAKind | FiveOfAKind deriving (Eq, Ord, Show)
data Hand = Hand [Card] Int deriving (Eq, Ord, Show)
data ClassifiedHand = CHand HandClass [Card] Int deriving (Eq, Ord, Show)
Parsing the input involved matching the text strings against the Card
constructors.
handsP = handP `sepBy` endOfLine
handP = Hand <$> ((many1 cardP) <* space) <*> decimal
cardP = (Two <$ "2") <|> (Three <$ "3") <|> (Four <$ "4") <|>
(Five <$ "5") <|> (Six <$ "6") <|> (Seven <$ "7") <|>
(Eight <$ "8") <|> (Nine <$ "9") <|> (Ten <$ "T") <|>
(Jack <$ "J") <|> (Queen <$ "Q") <|> (King <$ "K") <|>
(Ace <$ "A")
Part 1
The classification of hands in "Camel Cards" is based purely on the size of the largest group (or two) of identical cards, with ties broken in lexicographical order of the cards-as-dealt. I called this "groups in order" representation of a hand its signature.
type SignatureElement = (Int, [Card])
type Signature = [SignatureElement]
That gave me a pipeline operation to sign a set of cards: sort then group them, decorate each group with its size, then sort the decorated groups. Ensure the largest group is first.
sign :: [Card] -> Signature
sign = reverse . sort . fmap (\g -> (length g, g)) . group . sort
That means the sample hands from the problem text are signed like this:
[ [(2,[Three,Three]),(1,[King]),(1,[Ten]),(1,[Two])]
, [(3,[Five,Five,Five]),(1,[Jack]),(1,[Ten])]
, [(2,[King,King]),(2,[Seven,Seven]),(1,[Six])]
, [(2,[Jack,Jack]),(2,[Ten,Ten]),(1,[King])]
, [(3,[Queen,Queen,Queen]),(1,[Ace]),(1,[Jack])]
]
I then defined a bunch of functions to recognise each of the hand classifications, and a classify
function to classify a Hand
. I could have done this with a big case
statement in classify
, but the separate functions are easier to test should it be needed.
classify :: Hand -> ClassifiedHand
classify (Hand cards bid)
| isFiveOfAKind signature = CHand FiveOfAKind cards bid
| isFourOfAKind signature = CHand FourOfAKind cards bid
| isFullHouse signature = CHand FullHouse cards bid
| isThreeOfAKind signature = CHand ThreeOfAKind cards bid
| isTwoPair signature = CHand TwoPair cards bid
| isOnePair signature = CHand OnePair cards bid
| otherwise = CHand HighCard cards bid
where signature = sign cards
isFiveOfAKind, isFourOfAKind, isFullHouse, isThreeOfAKind, isTwoPair,
isOnePair :: Signature -> Bool
isFiveOfAKind ((5, _):_) = True
isFiveOfAKind _ = False
isFourOfAKind ((4, _):_) = True
isFourOfAKind _ = False
isFullHouse ((3, _):(2, _):_) = True
isFullHouse _ = False
isThreeOfAKind ((3, _):_) = True
isThreeOfAKind _ = False
isTwoPair ((2, _):(2, _):_) = True
isTwoPair _ = False
isOnePair ((2, _):_) = True
isOnePair _ = False
-- isHighCard :: Signature -> Bool
-- isHighCard _ = True
From that, finding the score isn't too hard. Classify the hands, sort them, combine with their rank (so the weakest hand has rank 1, counting up), then sum the scores.
part1, part2 :: [Hand] -> Int
part1 hands = sum $ fmap score rankedHands
where sortedHands = sort $ fmap classify hands
rankedHands = zip [1..] sortedHands
score (r, CHand _ _ bid) = r * bid
Part 2
The "J" cards aren't "Jacks", they're "Jokers".
First of all, it's a new type of card that's smaller than the others. I added it to the Card
type...
data Card = Joker | Two | Three ...
and made sure I could replace all the Jacks with Jokers.
enJoker :: Hand -> Hand
enJoker (Hand cards bid) = Hand jCards bid
where jCards = replace Jack Joker cards
replace :: Eq a => a -> a -> [a] -> [a]
replace f t = fmap (\x -> if x == f then t else x)
The tricky part comes from signing a hand. I remove the Jokers, sign the rest of the cards as before, then add the Jokers to the largest group.
sign :: [Card] -> Signature
sign cards = addJokers nonJokerSigned (length jokers, jokers)
where (jokers, nonJokers) = partition (== Joker) cards
nonJokerSigned = reverse $ sort $ fmap (\g -> (length g, g)) $
group $ sort nonJokers
addJokers :: Signature -> SignatureElement -> Signature
addJokers [] js = [js]
addJokers ((n, cs):xs) (jn, js) = (n + jn, cs ++ js):xs
That means the sample hands, with Jokers, have these signatures:
[ [(2,[Three,Three]),(1,[King]),(1,[Ten]),(1,[Two])]
, [(4,[Five,Five,Five,Joker]),(1,[Ten])]
, [(2,[King,King]),(2,[Seven,Seven]),(1,[Six])]
, [(4,[Ten,Ten,Joker,Joker]),(1,[King])]
, [(4,[Queen,Queen,Queen,Joker]),(1,[Ace])]
]
Everything else is the same.
part2 = part1 . fmap enJoker
Code
You can get the code from my locally-hosted Git repo, or from Gitlab.