Advent of Code 2023 - Day 7: Camel Cards

:: programming, puzzle, racket

1
2
#lang iracket/lang #:require racket
(require "../advent.rkt")

Day 7 involves simulating a card game. Our input looks like:

...
8J833 494
6AJT8 318
AA4QQ 125
62KK6 876
7A7QK 241
...

The left side is our card hand. The right side is our bid amount. As always, we begin with parsing. For our purposes of illustration, I’ll skip the first 20 hands to get to a more interesting one with a J:

1
2
3
(define lines (take (drop (parse-aoc 7 strings #:print-sample #f) 20) 5))

lines

’((“8J833” “494”) (“6AJT8” “318”) (“AA4QQ” “125”) (“62KK6” “876”) (“7A7QK” “241”))

We’ll map a parse function over this list of pairs. Let’s build that function from the bottom up. First we’ll need a function to translate the card symbols to a numeric value. This will need to be parameterized to handle Part 1 and Part 2 differently. For Part 1, a J has a value of 11, between a T and a Q. For Part 2, a J is a wildcard with a value of 1, the lowest card:

1
2
3
4
(define (translate part2? card)
  (index-of (string->list (if part2? "_J23456789T_QKA" "__23456789TJQKA")) card))

(translate #f #\J)

11

Let’ parse the first hand of 8J833

1
2
3
(define hand (map (curry translate #f) (string->list (car (car lines)))))

hand ; J is worth 11

’(8 11 8 3 3)

Or for Part 2:

1
2
3
(define hand2 (map (curry translate #t) (string->list (car (car lines)))))

hand2 ; J is worth 1

’(8 1 8 3 3)

Next, we’ll need to define our two part functions, because the parse function will delegate the work of ranking the hand to them. Our first step will be to group the cards and sort the groups by length, so we can determine the type of hand:

1
2
3
4
(define groups (~> (group-by identity hand)
                   (map length _)
                   (sort _ >)))
groups

’(2 2 1)

For Part 1, this shows we have two pair e.g. 2 identical cards, 2 identical cards and 1 other card. The final step for Part 1 is to match that pattern:

1
2
3
4
5
6
7
8
(match groups
  [ '(5)         7 ]   ; Five of a kind
  [ '(4 1)       6 ]   ; Four of a kind
  [ '(3 2)       5 ]   ; Full house
  [ '(3 1 1)     4 ]   ; Three of a kind
  [ '(2 2 1)     3 ]   ; Two pair
  [ '(2 1 1 1)   2 ]   ; One pair
  [ '(1 1 1 1 1) 1 ])

3

Here’s the part1 function in its entirety:

1
2
3
4
5
6
7
8
9
(define (part1 cards)
  (match (sort (map length (group-by identity cards)) >)
         [ '(5)         7 ]   ; Five of a kind
         [ '(4 1)       6 ]   ; Four of a kind
         [ '(3 2)       5 ]   ; Full house
         [ '(3 1 1)     4 ]   ; Three of a kind
         [ '(2 2 1)     3 ]   ; Two pair
         [ '(2 1 1 1)   2 ]   ; One pair
         [ '(1 1 1 1 1) 1 ])) ; High card

For Part 2, determining the type of hand involves a little more work. Since the J cards are wildcards, they can be any card, and we need to use them to obtain the best hand. To do this, we’ll simply generate all possible hands where the J cards become any of the other cards, then we’ll call Part 1 to determine the type of hand, sort those results and take the best one:

1
2
3
4
5
6
7
(define (part2 cards)
  (~> '(2 3 4 5 6 7 8 9 10 12 13 14)
      (map (compose1 part1 (curry list-replace cards 1)) _)
      (sort _ >)
      car))

(part2 hand2)

5

Notice that for Part 2, we now have a full house - the J became an 8, so we have three 8 and two 3. Now we can show the parse-input function in its entirety:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(define (parse-input part)
  (define (translate part2? card)
    (index-of (string->list (if part2? "_J23456789T_QKA" "__23456789TJQKA")) card))

  (define (parse-round part pair)
    (let ([ lst (map (curry translate (eq? part part2)) (string->list (car pair))) ])
      (cons (match-let ([(list a b c d e f) (cons (part lst) lst)])
              (+ (* 537824 a) (* 38416 b) (* 2744 c) (* 196 d) (* 14 e) f))
            (string->number (cadr pair)))))

  (map (curry parse-round part) (parse-aoc 7 strings #:print-sample #f)))

Lines 7 through 9 convert the hand to a single “strength” number by prepending the hand type (e.g. 5 for a Full House) to the bid, and then converting the list of 6 numbers (type card1 card2 ... card5) and converting that list to a base 14 number. We’ll combine that number with the bid to get a pair (strength . bid). With all of that in place, the only thing left is the solve function which will:

  1. sort those pairs in order of strength, ascending
  2. extract the bid
  3. add a rank, starting with 1 for the lowest
  4. multiply the rank and bid
  5. sum all the values
1
2
3
4
5
6
7
8
(define (solve rounds)
  (~> (sort rounds < #:key car)            ; sort by strength ascending
      (map cdr _)                          ; grab bid
      (enumerate _ 1)                      ; add rank
      (map (parallel-combine * car cdr) _) ; multiply rank * bid
      list-sum))                           ; sum all

(solve (parse-input part1))

246409899

1
(solve (parse-input part2))

244848487