[Day 7] Camel Cards // Advent of Code 2023 (Python)
Solving Advent of Code 2023 Day 7 in Python: ranking Camel Cards hands by type and card order with a custom sort key, then treating J as a joker in Part 2.

Camel Cards (via DALL-E 3)
Advent of Code
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!)…adventofcode.com
Part One
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, or2. The relative strength of each card follows this order, whereAis the highest and2is 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:
AAAAAFour of a kind, where four cards have the same label and one card has a different label:
AA8AAFull house, where three cards have the same label, and the remaining two cards share a different label:
23332Three 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:
TTT98Two pair, where two cards share one label, two other cards share a second label, and the remaining card has a third label:
23432One pair, where two cards share one label, and the other three cards have a different label from the pair and each other:
A23A4High card, where all cards’ labels are distinct:
23456Hands 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,
33332and2AAAAare both four of a kind hands, but33332is stronger because its first card is stronger. Similarly,77888and77788are both a full house, but77888is 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:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483This 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:
32T3Kis the only one pair and the other hands are all a stronger type, so it gets rank 1.
KK677andKTJJTare both two pair. Their first cards both have the same label, but the second card ofKK677is stronger (KvsT), soKTJJTgets rank 2 andKK677gets rank 3.
T55J5andQQQJAare both three of a kind.QQQJAhas a stronger first card, so it gets rank 5 andT55J5gets 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 are6440.Find the rank of every hand in your set. What are the total winnings?
Well..
Finally a cards game question! Very apt just after Diwali as we had a lot of game nights with Teen Patti(Indian version of Poker). Anyhow, the rules look pretty simple where we have to rank a hands based on:
FIRST_ORDER = ['Five of a Kind', 'Four of a Kind', 'Full House', 'Three of a Kind', 'Two Pairs', 'One Pair', 'High Card']
# in case of tie, use SECOND_ORDER while going Left -> Right
SECOND_ORDER = ['K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2']
But, first things first, let’s just write a test case, so we can build our logic to fix this one first! So, as per the base case given in question:

Parse Input
Now let’s do some ipython3 magic by parsing the input:


puzzle_input ~> game_pot
Classification and Ranking
Perfect!, now let’s write a way to calculate our first order of hand? Just writing some dirty code to code complete it and then optimise it:

Let’s run this and see if our code is able to identify hand’s first order properly:
# Game Pot
32T3K: 765, One Pair
T55J5: 684, Three of a Kind
KK677: 28, Two Pairs
KTJJT: 220, Two Pairs
QQQJA: 483, Three of a Kind
This looks correct. Now we need to rank it according to our HAND_STRENGTH_ORDER followed by CARD_ORDER:
HAND_STRENGTH_ORDER = [
"Five of a Kind",
"Four of a Kind",
"Full House",
"Three of a Kind",
"Two Pairs",
"One Pair",
"High Card",
]
CARD_ORDER = 'A23456789TJQK'
We can utilise Python’s sorted function along with a custom key function. The key function will use the HAND_STRENGTH_ORDER ↖️ to determine the order of hands in the game_pot we want to sort and then for all the ties, we will sort them internally using CARD_ORDER

The most important logic is highlighted!
Let’s try to run our testcase:
❯ /Users/jmalik/.pyenv/versions/3.10.7/bin/python /Users/jmalik/github/advent-of-code-2023/day_7/camel_cards.py
test_calculate_total_winnings passed successfully!
Looks like that is working correctly, now let’s load up our puzzle’s input.txt into our code and pass it to calculate_winnings and:
❯ /Users/jmalik/.pyenv/versions/3.10.7/bin/python /Users/jmalik/github/advent-of-code-2023/day_7/camel_cards.py
test_calculate_total_winnings passed successfully!
Total winnings: XXXXXXX
Awesome! Let’s put it into answer box…

:(
Ah, back to debugging! After spending a while, I decided to look again into our sorted_cards and we can see the culprit:

💡Observe that our code is ranking
QAAAA>AAJAAwhich is incorrect as according to ourCARD_ORDER->Aranks higher thanQ.

Fixing a bug in our card_strength calculation, a quite low level solution where I am just keeping an array of position of each card in hand corresponding to CARD_ORDER in able to sort it directly using sort(key=...) function:

Let’s run the code again and use our new answer in the box….and …..voila!!! 🎉

Here’s the code till now:
# Day 7: Camel Cards
from collections import Counter
class CamelCardsGame:
HAND_STRENGTH_ORDER = [
'Five of a kind', 'Four of a kind', 'Full house',
'Three of a kind', 'Two pair', 'One pair', 'High card'
]
CARD_ORDER = 'AKQJT98765432'
def __init__(self, hands_input_str):
self.hands = self.parse_input(hands_input_str)
def parse_input(self, input_str):
lines = input_str.strip().split('\n')
return [(line.split()[0], int(line.split()[1])) for line in lines]
def classify_hand(self, hand):
counts = Counter(hand).most_common()
hand_type = ""
card_strengths = []
for card in hand:
card_strengths.append(self.CARD_ORDER.index(card))
if counts[0][1] == 5:
hand_type = 'Five of a kind'
elif counts[0][1] == 4:
hand_type = 'Four of a kind'
elif counts[0][1] == 3 and counts[1][1] == 2:
hand_type = 'Full house'
elif counts[0][1] == 3:
hand_type = 'Three of a kind'
elif counts[0][1] == 2 and counts[1][1] == 2:
hand_type = 'Two pair'
elif counts[0][1] == 2:
hand_type = 'One pair'
else:
hand_type = 'High card'
return (hand_type, card_strengths)
def sort_hands(self):
sorted_hands = []
for index, (hand, _) in enumerate(self.hands):
hand_type, card_strengths = self.classify_hand(hand)
sorted_hands.append((hand_type, card_strengths, index, hand))
# sort by hand type, then by card strengths
sorted_hands.sort(key=lambda x: (self.HAND_STRENGTH_ORDER.index(x[0]), x[1]))
return sorted_hands
def calculate_winnings(self):
sorted_hands = self.sort_hands()
total_winnings = 0
num_hands = len(sorted_hands)
for rank, (_, _, index, _) in enumerate(sorted_hands, start=1):
bid = self.hands[index][1]
total_winnings += (num_hands - rank + 1) * bid # Adjust rank for winnings calculation
return total_winnings
def part_1():
with open('day_7/input.txt', 'r') as f:
hands_input = f.read()
game = CamelCardsGame(hands_input)
total_winnings = game.calculate_winnings()
print(f"Total winnings: {total_winnings}")
def test_calculate_winnings():
# puzzle input contains game hands and their bids
hands_input = """32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483"""
# Create an instance of CamelCardsGame and calculate total winnings
game = CamelCardsGame(hands_input)
total_winnings = game.calculate_winnings()
assert total_winnings == 6440, f"Expected 6440, got {total_winnings}"
print("test_calculate_total_winnings passed successfully!")
if __name__ == "__main__":
test_calculate_winnings()
part_1()
Part Two
To make things a little more interesting, the Elf introduces one additional rule. Now,
Jcards are [jokers](https://en.wikipedia.org/wiki/Joker_(playing_card%29) - wildcards that can act like whatever card would make the hand the strongest type possible.To balance this,
Jcards are now the weakest individual cards, weaker even than2. The other cards stay in the same order:A,K,Q,T,9,8,7,6,5,4,3,2,J.
Jcards can pretend to be whatever card is best for the purpose of determining hand type; for example,QJJQ2is now considered four of a kind. However, for the purpose of breaking ties between two hands of the same type,Jis always treated asJ, not the card it’s pretending to be:JKKK2is weaker thanQQQQ2becauseJis weaker thanQ.Now, the above example goes very differently:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
32T3Kis still the only one pair; it doesn’t contain any jokers, so its strength doesn’t increase.
KK677is now the only two pair, making it the second-weakest hand.
T55J5,KTJJT, andQQQJAare now all four of a kind!T55J5gets rank 3,QQQJAgets rank 4, andKTJJTgets 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?
What the Joker!
Nice! We now have to consider J as a joker card that can act as any card to make the hand strongest. However, for tie-breaking, J is considered the weakest card. Let’s update the CARD_ORDER:
HAND_STRENGTH_ORDER = [
'Five of a kind', 'Four of a kind', 'Full house',
'Three of a kind', 'Two pair', 'One pair', 'High card'
]
CARD_ORDER = 'AKQT98765432J'
💭 We will now have to update the classification order as well. I think we can just do a quick count of
Jin our hand and use that to classifyHAND_STRENGTHand in case of a tie, our current logic should take care of sorting viaCARD_ORDERscore.

This is getting a bit crowded, I might come back and refactor this!
💡 Edge case: Remember there can be multiple
Jin a hand.
Let’s update our base test case and try to see if it works!
[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_7/camel_cards_part_2.py"
test_calculate_total_winnings passed successfully!
Awesome! Let’s load up the input and try to run for part_2().
The code does compile and gives us a number, let’s see if it is the correct answer?
And NOPE!

😢
Doing back to debugging to see how’s our final sorted_cards under part_2() …

The ranking seems to be correct! 🤔
Ah, found another bug in our code. Seems like our classification is recognising “Two Pair” incorrectly for single jokers:

Let’s fix that!
💡 Interesting insight: A “Two Pair” hand with a
Jis never possible in this game as we will always be making “Three of a kind” or “Full House”.
After a lot of debugging and looking into edge cases, here’s our update classify_hand :

Will optimise once my code is validated!
I have validated all 7 buckets of hands manually for puzzle input now, for almost all 1000 enteries while doing a dry run. It should work now. 🥺
Also, I have tried a few too many times, so now Advent of Code is also rate limiting me!
Let’s execute!
[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_7/camel_cards_part_2.py"
test_calculate_total_winnings passed successfully!
Total winnings: XXXXXXXX
We do get an answer, and let’s put it in the answer box…and…. 🤞

Here’s the final pythonic code:
from collections import Counter
class CardHand:
CARD_ORDER = "AKQT98765432J"
def __init__(self, hand_str):
self.hand_str = hand_str
self.counts = Counter(hand_str)
self.hand_type, self.strength = self.classify_and_strength()
def classify_and_strength(self):
counts_filtered = [c for c in self.counts.items() if c[0] != 'J']
joker_count = self.counts.get("J", 0)
top_count = counts_filtered[0][1] if counts_filtered else 0
hand_type = self.determine_hand_type(counts_filtered, top_count, joker_count)
strength = [self.CARD_ORDER.index(card) for card in self.hand_str if card != 'J']
return hand_type, strength
def determine_hand_type(self, counts, top_count, joker_count):
# Determine hand type considering jokers
if top_count + joker_count == 5:
return "Five of a kind"
if top_count + joker_count == 4:
return "Four of a kind"
if top_count + joker_count == 3 and (len(counts) > 1 and counts[1][1] >= 2):
return "Full house"
if top_count + joker_count == 3:
return "Three of a kind"
if top_count == 2 and (len(counts) > 1 and counts[1][1] + min(joker_count, 1) == 2):
return "Two pair"
if top_count + joker_count == 2:
return "One pair"
return "High card"
class CamelCardsGame:
HAND_STRENGTH_ORDER = ["Five of a kind", "Four of a kind", "Full house", "Three of a kind", "Two pair", "One pair", "High card"]
def __init__(self, hands_input_str):
self.hands = self.parse_input(hands_input_str)
def parse_input(self, input_str):
return [(CardHand(hand), int(bid)) for hand, bid in (line.split() for line in input_str.strip().split('\n'))]
def sort_hands(self):
return sorted(self.hands, key=lambda x: (self.HAND_STRENGTH_ORDER.index(x[0].hand_type), x[0].strength))
def calculate_winnings(self):
sorted_hands = self.sort_hands()
return sum((len(sorted_hands) - rank) * bid for rank, (hand, bid) in enumerate(sorted_hands))
def part_2():
with open("day_7/input.txt", "r") as f:
hands_input = f.read()
game = CamelCardsGame(hands_input)
total_winnings = game.calculate_winnings()
print(f"Total winnings: {total_winnings}")
def test_calculate_winnings():
# puzzle input contains game hands and their bids
hands_input = """32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483"""
# Create an instance of CamelCardsGame and calculate total winnings
game = CamelCardsGame(hands_input)
total_winnings = game.calculate_winnings()
assert total_winnings == 5905, f"Expected 5905, got {total_winnings}"
print("test_calculate_total_winnings passed successfully!")
if __name__ == "__main__":
test_calculate_winnings()
part_2()
Phew! This one took me hours, ngl but such a fun and refreshing problem to solve! I am running a bit behind Advent of Code now, in case you are following the series and would like me to hurry up, apologies!
But I am having fun, nonetheless!
p.s: I am deliberating not adding answers here so you can actually spend some time trying to understand how we solved the problem!
Feel free to checkout code on my Github repo and try to set it up and run on your machine 👇
GitHub - jatinkrmalik/advent-of-code-2023: This is my live attempt to solve all 25 problems of…
This is my live attempt to solve all 25 problems of Advent of Code 2023 with Python! - GitHub …github.com
Keep reading
🎄Solve Advent of Code 2024 With Me!
Announcing a spoiler-free YouTube video series for Advent of Code 2024 that focuses on how to analyze and approach each puzzle, not just the answers.
The Redis Paradox: my DelhiFOSS 2.0 talk at IIT Delhi, written up
The written version of my DelhiFOSS 2.0 talk on Redis beyond caching: a short history of caches, live driver matching with GEO commands, and Pub/Sub.
Interview prep, senior edition: a system design checklist, one backtracking template and what loops grade on
My 2022 prep notes for senior and staff interviews: a system design checklist, one Python DFS for seven backtracking problems, rubrics and a STAR story bank.
Leave a comment