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
Published

by Daniel Nitsikopoulos

in posts

Tagged

© 2010 - 2024 Daniel Nitsikopoulos. All rights reserved.

🕸💍  →