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 . sortThat 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 _ = TrueFrom 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 * bidPart 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):xsThat 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 enJokerCode
You can get the code from my locally-hosted Git repo, or from Gitlab.