Advent of code 2023 - Day 7: Camel Cards

Advent of code 2023 - Day 7: Camel Cards

2024-01-04
python

This year I try to record my attempt at solving the Advent of Code 2023 riddles. This is Day 7 - see https:adventofcode.com/2023/day/7

Part 1 #

Lets first read the task:

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:

32T3K 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 and KTJJT are both two pair. Their first cards both have the same label, but the second card of KK677 is stronger (K vs T), so KTJJT gets rank 2 and KK677 gets rank 3.
  • T55J5 and QQQJA are both three of a kind. QQQJA has a stronger first card, so it gets rank 5 and T55J5 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?

As always - let’s read in the data.

import pandas as pd

camel = pd.read_table('data/2023-12-07-1-aoc.txt', delim_whitespace=True,
                      header=None, names=['cards', 'bid'],
                      dtype={'cards': 'string', 'bid': 'Int64'})
camel
cards bid
0 A833A 309
1 Q33J3 205
2 55KK5 590
3 K4457 924
4 Q3QT3 11
995 KQJ53 367
996 57866 537
997 Q94A9 210
998 J448A 903
999 6J66J 114

1000 rows × 2 columns

For sorting, we need to compute a type column. The 7 types can be deduced based on how many different cards are there (through set()) and the number of cards.

Also, for the next step we prepare one column per card string in the cards.

def get_type(cards):
    cardset = set(cards)
    cardlen = len(cardset)
    cardlist = list(cards)
    cardnum = [cardlist.count(i) for i in cardset]
    if cardlen == 1:
        ctype = 1
    elif cardlen in (4, 5):
        ctype = cardlen + 2
    elif cardlen == 2:
        ctype = 2 if set(cardnum) == {1, 4} else 3
    else:
        ctype = 4 if 3 in cardnum else 5
    return ctype

camel['type'] = camel.loc[:, 'cards'].apply(lambda x: get_type(x))
for i in range(5):
    camel[f'c{i}'] = camel.loc[:, 'cards'].apply(lambda x: list(x)[i])
camel
cards bid type c0 c1 c2 c3 c4
0 A833A 309 5 A 8 3 3 A
1 Q33J3 205 4 Q 3 3 J 3
2 55KK5 590 3 5 5 K K 5
3 K4457 924 6 K 4 4 5 7
4 Q3QT3 11 5 Q 3 Q T 3
995 KQJ53 367 7 K Q J 5 3
996 57866 537 6 5 7 8 6 6
997 Q94A9 210 6 Q 9 4 A 9
998 J448A 903 6 J 4 4 8 A
999 6J66J 114 3 6 J 6 6 J

1000 rows × 8 columns

For sorting, pandas contributes the sort_values method, which supports sorting by multiple columns (here, we first sort according to type, then c0, c1, …). For easier sorting by numerical values, we convert the values using the card_dict dictionary, which implements our giving sorting order.

card_dict = {c: i for i, c in enumerate([7, 6, 5, 4, 3, 2, 1, '2', '3',
                                         '4', '5', '6', '7', '8', '9',
                                         'T', 'J', 'Q', 'K', 'A'])}

camel = camel.sort_values(by=['type', 'c0', 'c1', 'c2', 'c3', 'c4'],
                          ignore_index=True,
                          key=lambda x: x.map(card_dict))
camel.index += 1
camel
cards bid type c0 c1 c2 c3 c4
1 237AQ 157 7 2 3 7 A Q
2 23K47 759 7 2 3 K 4 7
3 249K8 612 7 2 4 9 K 8
4 264A7 341 7 2 6 4 A 7
5 26578 10 7 2 6 5 7 8
996 AA5AA 565 2 A A 5 A A
997 AAATA 704 2 A A A T A
998 AAAA3 145 2 A A A A 3
999 AAAAJ 594 2 A A A A J
1000 JJJJJ 219 1 J J J J J

1000 rows × 8 columns

Lastly, we multiply the bid with the index and sum the result.

camel.loc[:, 'bid'].mul(camel.index).sum()
246912307

Part 2 #

Lets first read the task:

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:

32T3K 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, and QQQJA are now all four of a kind! T55J5 gets rank 3, QQQJA gets rank 4, and KTJJT 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?

What changes? We have to update our get_type() function and our card_dict dictionary, otherwise the method stays the same!

Let’s first write get_type_joker(): if no joker is in the cards, we can just refer to get_type(). If we have 1, 2 or 5 different card faces, the answer is trivial. Only if we have 3 or 4 different faces, we have to check the number of jokers and the maximum number of other faces.

Note: if a J is present, there can never be high card (type = 7), because we always can get one pair. Also we can never get two pair (type = 5), because we can always construct three of a kind or full house.

def get_type_joker(cards):
    cardset = set(cards)
    cardlen = len(cardset)
    cardlist = list(cards)
    cardnum = [cardlist.count(i) for i in cardset]
    if not 'J' in cardset:
        ctype = get_type(cards)
    else:
        idxj = list(cardset).index('J')
        numj = cardnum[idxj]
        cardnum.pop(idxj)
        if cardlen in [1, 2]:
            ctype = 1
        elif cardlen == 5:
            ctype = 6
        else:
            if (numj in [1, 2]) and (cardlen == 4):
                ctype = 4
            else:
                ctype = 6 - max(cardnum) - numj
    return ctype

camel['type'] = camel.loc[:, 'cards'].apply(lambda x: get_type_joker(x))

camel
cards bid type c0 c1 c2 c3 c4
1 237AQ 157 7 2 3 7 A Q
2 23K47 759 7 2 3 K 4 7
3 249K8 612 7 2 4 9 K 8
4 264A7 341 7 2 6 4 A 7
5 26578 10 7 2 6 5 7 8
996 AA5AA 565 2 A A 5 A A
997 AAATA 704 2 A A A T A
998 AAAA3 145 2 A A A A 3
999 AAAAJ 594 1 A A A A J
1000 JJJJJ 219 1 J J J J J

1000 rows × 8 columns

Now, we just move J in our new card_dict_joker to where it belongs, the rest of the solution is the same.

card_dict_joker = {c: i for i, c in enumerate(
    [7, 6, 5, 4, 3, 2, 1, 'J', '2', '3', '4', '5', '6', '7', '8', '9',
     'T', 'Q', 'K', 'A'])}

camel = camel.sort_values(by=['type', 'c0', 'c1', 'c2', 'c3', 'c4'],
                          ignore_index=True,
                          key=lambda x: x.map(card_dict_joker))
camel.index += 1
display(camel)
print('Our new solution: ', camel.loc[:, 'bid'].mul(camel.index).sum())
cards bid type c0 c1 c2 c3 c4
1 237AQ 157 7 2 3 7 A Q
2 23K47 759 7 2 3 K 4 7
3 249K8 612 7 2 4 9 K 8
4 264A7 341 7 2 6 4 A 7
5 26578 10 7 2 6 5 7 8
996 TJTTJ 609 1 T J T T J
997 QQJQJ 131 1 Q Q J Q J
998 QQQJQ 831 1 Q Q Q J Q
999 KKKJK 183 1 K K K J K
1000 AAAAJ 594 1 A A A A J

1000 rows × 8 columns

Our new solution:  246894760

comments powered by Disqus