Your all-expenses-paid trip turns out to be a one-way, five-minute ride in an airship. (At least it's a cool airship!) It drops you off at the edge of a vast desert and descends back to Island Island.
"Did you bring the parts?"
You turn around to see an Elf completely covered in white clothing, wearing goggles, and riding a large camel.
"Did you bring the parts?" she asks again, louder this time. You aren't sure what parts she's looking for; you're here to figure out why the sand stopped.
"The parts! For the sand, yes! Come with me; I will show you." She beckons you onto the camel.
After riding a bit across the sands of Desert Island, you can see what look like very large rocks covering half of the horizon. The Elf explains that the rocks are all along the part of Desert Island that is directly above Island Island, making it hard to even get there. Normally, they use big machines to move the rocks and filter the sand, but the machines have broken down because Desert Island recently stopped receiving the parts they need to fix the machines.
You've already assumed it'll be your job to figure out why the parts stopped when she asks if you can help. You agree automatically.
Because the journey will take a few days, she offers to teach you the game of Camel Cards. Camel Cards is sort of similar to poker except it's designed to be easier to play while riding a camel.
In Camel Cards, you get a list of hands, and your goal is to order them based on the strength of each hand. A hand consists of five cards labeled one of A
, K
, Q
, J
, T
, 9
, 8
, 7
, 6
, 5
, 4
, 3
, or 2
. The relative strength of each card follows this order, where A
is the highest and 2
is the lowest.
Every hand is exactly one type. From strongest to weakest, they are:
- 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
Hands are primarily ordered based on type; for example, every full house is stronger than any three of a kind.
If two hands have the same type, a second ordering rule takes effect. Start by comparing the first card in each hand. If these cards are different, the hand with the stronger first card is considered stronger. If the first card in each hand have the same label, however, then move on to considering the second card in each hand. If they differ, the hand with the higher second card wins; otherwise, continue with the third card in each hand, then the fourth, then the fifth.
So, 33332
and 2AAAA
are both four of a kind hands, but 33332
is stronger because its first card is stronger. Similarly, 77888
and 77788
are both a full house, but 77888
is stronger because its third card is stronger (and both hands have the same first and second card).
To play Camel Cards, you are given a list of hands and their corresponding bid (your puzzle input). For example:
1234532T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
This example shows five hands; each hand is followed by its bid amount. Each hand wins an amount equal to its bid multiplied by its rank, where the weakest hand gets rank 1, the second-weakest hand gets rank 2, and so on up to the strongest hand. Because there are five hands in this example, the strongest hand will have rank 5 and its bid will be multiplied by 5.
So, the first step is to put the hands in order of strength:
32T3K
is the only one pair and the other hands are all a stronger type, so it gets rank 1.KK677
andKTJJT
are both two pair. Their first cards both have the same label, but the second card ofKK677
is stronger (K
vsT
), soKTJJT
gets rank 2 andKK677
gets rank 3.T55J5
andQQQJA
are both three of a kind.QQQJA
has a stronger first card, so it gets rank 5 andT55J5
gets rank 4.
Now, you can determine the total winnings of this set of hands by adding up the result of multiplying each hand's bid with its rank (765
_ 1 + 220
_ 2 + 28
_ 3 + 684
_ 4 + 483
* 5). So the total winnings in this example are 6440
.
Find the rank of every hand in your set.What are the total winnings?
Builds a hand for each line and calculates the maximum number of points for each hand by checking hand types. After that, it's just sorting the hands by points/cards, then a little bit of math to calculate the final total.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146from dataclasses import dataclass
from pathlib import Path
import pytest
CARD_ORDER = ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"]
def card_position(card: str) -> int:
return CARD_ORDER.index(card)
@dataclass
class Hand:
cards: list[str]
card_count: dict[str, int]
bid: int
@property
def is_five_of_kind(self) -> bool:
return len(set(self.cards)) == 1
@property
def is_four_of_kind(self) -> bool:
return any(count == 4 for count in self.card_count.values())
@property
def is_full_house(self) -> bool:
found_three = False
found_two = False
for count in self.card_count.values():
if count == 3:
found_three = True
if count == 2:
found_two = True
return found_three and found_two
@property
def is_three_of_kind(self) -> bool:
found_three = False
found_two = False
for count in self.card_count.values():
if count == 3:
found_three = True
if count == 2:
found_two = True
return found_three and not found_two
@property
def is_two_pair(self) -> bool:
found_pair = 0
for count in self.card_count.values():
if count == 2:
found_pair += 1
return found_pair == 2
@property
def is_one_pair(self) -> bool:
found_pair = 0
for count in self.card_count.values():
if count == 2:
found_pair += 1
return found_pair == 1
@property
def is_high_card(self) -> bool:
return len(set(self.cards)) == 5
@property
def points(self) -> int:
if self.is_five_of_kind:
return 1
if self.is_four_of_kind:
return 2
if self.is_full_house:
return 3
if self.is_three_of_kind:
return 4
if self.is_two_pair:
return 5
if self.is_one_pair:
return 6
if self.is_high_card:
return 7
raise AssertionError
def __gt__(self, other: "Hand"):
if self.points == other.points:
for self_card, other_card in zip(self.cards, other.cards, strict=False):
if self_card != other_card:
return card_position(self_card) > card_position(other_card)
return self.points > other.points
def runner(document: list[str]) -> int:
hands: list[Hand] = []
for line in document:
hand_str, bid_str = line.split(" ")
hands.append(
Hand(
cards=list(hand_str),
card_count={
card: list(hand_str).count(card) for card in list(hand_str)
},
bid=int(bid_str),
),
)
hands.sort(reverse=True)
return sum([hand.bid * (index + 1) for index, hand in enumerate(hands)])
@pytest.mark.parametrize(
"filename,output",
[
("example-1.txt", 6440),
("example-2.txt", 251545216),
],
)
def test_runner(filename: str, output: int) -> None:
with open(Path(__file__).with_name(filename)) as file:
result = runner(file.read().splitlines())
assert result == output
Answer: 251,545,216
To make things a little more interesting, the Elf introduces one additional rule. Now, J
cards are jokers - wildcards that can act like whatever card would make the hand the strongest type possible.
To balance this, J
cards are now the weakest individual cards, weaker even than 2
. The other cards stay in the same order: A
, K
, Q
, T
, 9
, 8
, 7
, 6
, 5
, 4
, 3
, 2
, J
.
J
cards can pretend to be whatever card is best for the purpose of determining hand type; for example, QJJQ2
is now considered four of a kind. However, for the purpose of breaking ties between two hands of the same type, J
is always treated as J
, not the card it's pretending to be: JKKK2
is weaker than QQQQ2
because J
is weaker than Q
.
Now, the above example goes very differently:
1234532T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
32T3K
is still the only one pair; it doesn't contain any jokers, so its strength doesn't increase.KK677
is now the only two pair, making it the second-weakest hand.T55J5
,KTJJT
, andQQQJA
are now all four of a kind!T55J5
gets rank 3,QQQJA
gets rank 4, andKTJJT
gets rank 5.
With the new joker rule, the total winnings in this example are 5905
.
Using the new joker rule, find the rank of every hand in your set.What are the new total winnings?
Builds off of Part 1, but has to treat J
cards as wildcards, and slightly rearranges the card strength ordering.
To treat J
cards as wildcards, let's look at the is_four_of_kind
property.
12345678910111213141516171819def is_four_of_kind(self) -> bool:
j_count = self.card_count.get("J", 0)
temp_cards = {
card: count for card, count in self.card_count.items() if card != "J"
}
card_counts = list(temp_cards.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
for _card, count in card_counts:
if count == 4:
return True
if count + j_count == 4:
return True
return False
Sorting the hand doesn't matter for is_four_of_kind
, but it does matter for other hands.
Full solution below
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
import pytest
CARD_ORDER = ["A", "K", "Q", "T", "9", "8", "7", "6", "5", "4", "3", "2", "J"]
def card_position(card: str) -> int:
return CARD_ORDER.index(card)
@dataclass
class Hand:
cards: list[str]
card_count: dict[str, int]
bid: int
@property
def is_five_of_kind(self) -> bool:
if "J" in self.card_count and len(set(self.cards)) == 2:
return True
return len(set(self.cards)) == 1
@property
def is_four_of_kind(self) -> bool:
j_count = self.card_count.get("J", 0)
temp_cards = {
card: count for card, count in self.card_count.items() if card != "J"
}
card_counts = list(temp_cards.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
for _card, count in card_counts:
if count == 4:
return True
if count + j_count == 4:
return True
return False
@property
def is_full_house(self) -> bool:
j_count = self.card_count.get("J", 0)
temp_cards = {
card: count for card, count in self.card_count.items() if card != "J"
}
card_counts = list(temp_cards.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
found_three = False
found_two = False
for _card, count in card_counts:
if count == 3:
found_three = True
elif count + j_count == 3:
j_count = 0
found_three = True
elif count == 2:
found_two = True
elif count + j_count == 2:
j_count = 0
found_two = True
return found_three and found_two
@property
def is_three_of_kind(self) -> bool:
j_count = self.card_count.get("J", 0)
temp_cards = {
card: count for card, count in self.card_count.items() if card != "J"
}
card_counts = list(temp_cards.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
found_three = False
found_two = False
for _card, count in card_counts:
if count == 3:
found_three = True
elif count + j_count == 3:
j_count = 0
found_three = True
elif count == 2:
found_two = True
elif count + j_count == 2:
j_count = 0
found_two = True
return found_three and not found_two
@property
def is_two_pair(self) -> bool:
found_pair = 0
card_counts = list(self.card_count.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
for _card, count in card_counts:
if count == 2:
found_pair += 1
return found_pair == 2
@property
def is_one_pair(self) -> bool:
j_count = self.card_count.get("J", 0)
temp_cards = {
card: count for card, count in self.card_count.items() if card != "J"
}
card_counts = list(temp_cards.items())
card_counts.sort(key=lambda x: x[1], reverse=True)
found_pair = 0
for _card, count in card_counts:
if count == 2:
found_pair += 1
elif count + j_count == 2:
j_count = 0
found_pair += 1
return found_pair == 1
@property
def is_high_card(self) -> bool:
return "J" not in self.card_count and len(set(self.cards)) == 5
@cached_property
def points(self) -> int:
if self.is_five_of_kind:
return 1
if self.is_four_of_kind:
return 2
if self.is_full_house:
return 3
if self.is_three_of_kind:
return 4
if self.is_two_pair:
return 5
if self.is_one_pair:
return 6
if self.is_high_card:
return 7
raise AssertionError
def __gt__(self, other: "Hand") -> bool:
if self.points == other.points:
for self_card, other_card in zip(self.cards, other.cards, strict=False):
if self_card != other_card:
return card_position(self_card) > card_position(other_card)
return self.points > other.points
def runner(document: list[str]) -> int:
hands: list[Hand] = []
for line in document:
hand_str, bid_str = line.split(" ")
hand = Hand(
cards=list(hand_str),
card_count={card: list(hand_str).count(card) for card in list(hand_str)},
bid=int(bid_str),
)
hands.append(hand)
hands.sort(reverse=True)
return sum([hand.bid * (index + 1) for index, hand in enumerate(hands)])
@pytest.mark.parametrize(
"cards,points",
[
("JJJJJ", 1),
("JJJJK", 1),
("JJJKK", 1),
("JJKKK", 1),
("JKKKK", 1),
("A2KJJ", 4),
("ATJKT", 4),
],
)
def test_hand_part_2(cards: str, points: int) -> None:
hand = Hand(
cards=list(cards),
card_count={card: list(cards).count(card) for card in list(cards)},
bid=0,
)
assert hand.points == points
@pytest.mark.parametrize(
"filename,output",
[
("example-1.txt", 5905),
("example-2.txt", 250384185),
],
)
def test_runner(filename: str, output: int) -> None:
with open(Path(__file__).with_name(filename)) as file:
result = runner(file.read().splitlines())
assert result == output
Answer: 250,384,185
Day | Part 1 Time | Part 1 Rank | Part 2 Time | Part 2 Rank |
---|---|---|---|---|
7 | 00:21:33 | 1,156 | 01:42:55 | 7,255 |
Part 1 was fairly straightforward, it was an interesting challenge in data modelling that I quite enjoyed ๐.
I got tripped up on edge cases for Part 2, so much so that I had to result to making another small test suite of individual hands to ensure I was counting points correctly ๐. The last few edge cases were A2KJJ
and ATJKT
.
For A2KJJ
, I was double-counting the J
cards, which results in a higher score than I should have gotten. For ATJKT
, I was pairing the J
card with the A
card, getting two pair instead of three of a kind.
The first was solved by removing the J
cards before checking each hand. The second was solved by ordering the remaining cards by the card count, descending.
Kinda silly mistakes, and I struggled with them (and other edge cases) for so long because I didn't create a smaller test suite until well over an hour in.
I was pretty happy that I got to use dataclasses though, those are always fun!