Advent of Code 2023 — Day 7
Day 7 saw us playing cards, on camels — camel cards!
Camel cards are like poker. But simpler, so you can play it while on a camel.
The hand-rules are similar to Poker, but with a few changes — which is ace, because I have no idea what the poker rules are anymore.
Five of a kind, where all five cards have the same label:
AAAAA
Four of a kind, where four cards have the same label and one card has a different label:
AA8AA
Full house, where three cards have the same label, and the remaining two cards share a different label:
23332
Three of a kind, where three cards have the same label, and the remaining two cards are each different from any other card in the hand:
TTT98
Two pair, where two cards share one label, two other cards share a second label, and the remaining card has a third label:
23432
One pair, where two cards share one label, and the other three cards have a different label from the pair and each other:
A23A4
High card, where all cards' labels are distinct:
23456
The hands are also ranked by suit, in descending order where an ace is high.
Part 1
In part 1, we have the following input:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
Where the right hand column is the bid amount for the hand.
Our job is to sort each hand, and then multiply each hand's bid with its rank.
So if we take the order above as an example — we'd have 765 * 1
+ 684 * 2
+ ...
I started off this puzzle by assigning a suit / card number a letter (easier to sort alphabetically than numerically in this case):
ORDER = {"A" => "a", "K" => "b", "Q" => "c", "J" => "d", "T" => "e", "9" => "f", "8" => "g", "7" => "h", "6" => "i", "5" => "j", "4" => "k", "3" => "l", "2" => "m"}.freeze
Then turned the input in to an array of arrays that looked like [[hand, bid], ...]
, sorted the list and then went to work adding up each bid:
rounds = input.map { |l| l.split(" ") }
sorted_rounds = rounds
.sort {|a,b| [hand_type(a[0]), hand_value(b[0])] <=> [hand_type(b[0]), hand_value(a[0])] }
sorted_rounds
.map
.with_index(1) {|r, idx| r[1].to_i * idx }
.sum
To find the type of hand I was looking at, I used the below 2 methods.
First, I turned the hand in to a hash with the number of cards of each type. So for an input of K3558
I would have {K => 1, 3 => 1, 5 => 2, 8 => 1}
. Then, I take the values and sort them — in to something like [2,1,1,1]
.
This is then passed in to the score_hand
method which returns a rank based on the hand.
def hand_type(hand)
cards = hand.gsub(/(.)\0*/).to_a
sorted_cards = cards.tally.values.sort {|a,b| b <=> a}
score_hand(sorted_cards)
end
def score_hand(cards)
return 6 if cards == [5]
return 5 if cards == [4,1]
return 4 if cards == [3,2]
return 3 if cards == [3,1,1]
return 2 if cards == [2,2,1]
return 1 if cards == [2,1,1,1]
return 0 if cards == [1,1,1,1,1]
end
Lastly, we need to sort cards that have the same type based on the order of the cards in hand.
For this, sorting alphabetically was was simpler than zipping each array and comparing card values.
For this, I took a look at the ORDER
constant and turned a hand like K3558
to bljjg
, so when compared with another hand that has the same type, say K3448
(bliig
), the first hand wins.
def hand_value(hand)
hand.gsub(/(.)\0*/).map{ |v| ORDER[v] }.join("")
end
Part 2
In part 2, Jacks are out, Jokers are in.
Jokers are the lowest scoring card, but they can be used as a wildcard to improve a hand to one of a better quality. So for example, the hand 33JJ8
goes from being two of a kind [33, JJ]
to four of a kind [3333]
!
Because Jokers are now worthless, I have a new order for them:
IMPROVED_ORDER = {"A" => "a", "K" => "b", "Q" => "c", "T" => "d", "9" => "e", "8" => "f", "7" => "g", "6" => "h", "5" => "i", "4" => "j", "3" => "k", "2" => "l", "J" => "m"}.freeze
The code for part 2 and part 1 is pretty much the same, except I try and improve each hand when sorting:
rounds = input
.map { |l| l.split(" ") }
sorted_rounds = rounds
.sort {|a,b| [improve_hand(a[0]), improved_hand_value(b[0])] <=> [improve_hand(b[0]), improved_hand_value(a[0])] }
sorted_rounds
.map
.with_index(1) {|r, idx| r[1].to_i * idx }
.sum
I try and improve each hand with the method below. I do the same tallying of each hand.
So 33JJ8
becomes {3 => 2, J => 2, 8 => 1}
.
But then I take any J
cards and add them to whichever card type has the most already.
So now, the tally looks like {3 => 4, 8 => 1}
— which makes this a 4 of a kind.
Thankfully Jokers only improve hand quality, not overall score, so we didn't need to consider hands like KQKQJ
and QKQKJ
and apply the Joker to the King in order to get a better overall score.
def improve_hand(hand)
cards = hand.gsub(/(.)\0*/).to_a
return hand_type(hand) unless cards.any? { |c| c == "J" }
return hand_type(hand) if cards.all? { |c| c == "J" }
card_types = cards.tally
improve_by = card_types["J"]
type_to_improve = type_to_improve(card_types)
card_types[type_to_improve] += improve_by if improve_by && type_to_improve
card_types.delete("J")
sorted_cards = card_types.values.sort {|a,b| b <=> a}
score_hand(sorted_cards)
end
def type_to_improve(cards)
return nil if cards.empty?
no_jack = cards.reject{|k,v| k == "J" }
return nil if no_jack.empty?
no_jack.max_by{|_,v| v}[0]
end
⭐️ Success!
Day 7: https://github.com/dNitza/advent-of-code/blob/main/2023/07/lib/puzzle.rb
Performance
day 07
├─ part 1 — 0.073877s
╰─ part 2 — 0.101986s
in posts
Tagged