Day 4: Scratchcards

 17th December 2023 at 10:16pm

Part 1

This was fairly easy to understand and implement. I suppose the hardest part was trying to figure out an elegant, functional way to apply the same function to the two resulting lists of splitting the card line at the | char.

Tests

import unittest
from d0401 import (
    get_game_data_from_input,
    get_score_from_card,
    calculate_sum_of_cards
        )

TEST_DATA = """
Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
"""

class Test202304_01(unittest.TestCase):
    def test_transform_input_into_game_data(self):
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            cards,
            [[[41, 48, 83, 86, 17],
             [83, 86, 6, 31, 17, 9, 48, 53]],
             [[13, 32, 20, 16, 61], [61, 30, 68, 82, 17, 32, 24, 19]], [[1, 21, 53, 59, 44], [69, 82, 63, 72, 16, 21, 14, 1]], [[41, 92, 73, 84, 69], [59, 84, 76, 51, 58, 5, 54, 83]], [[87, 83, 26, 28, 32], [88, 30, 70, 12, 93, 22, 82, 36]], [[31, 18, 13, 56, 72], [74, 77, 10, 23, 35, 67, 36, 11]]]
                         )

    def test_get_score_from_card(self):
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            get_score_from_card(cards[0]),
            8
        )
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            get_score_from_card(cards[1]),
            2
        )
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            get_score_from_card(cards[3]),
            1
        )
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            get_score_from_card(cards[4]),
            0
        )

    def test_sum_of_cards(self):
        cards = get_game_data_from_input(TEST_DATA)
        self.assertEqual(
            calculate_sum_of_cards(cards),
            13
        )

if __name__ == '__main__':
    unittest.main()
     

Code

def str_to_int_list(s: str) -> list[int]:
   return list(map(int, s.split()))

def get_game_data_from_input(
        data: str
        ) -> list[list[int]]:
    res = list(map(lambda pair: [str_to_int_list(pair[0]),
                                 str_to_int_list(pair[1])],
                   map(lambda line: line.split(":")[1].split("|"),
                       data.strip().splitlines())))
    return res

def get_score_from_card(
        card: list[list[int]]
        ) -> int:
    winning, chance = card
    found = [x for x in chance if x in winning]
    return (2 ** (len(found) - 1) if len(found) > 1 else len(found))
    
def calculate_sum_of_cards(
        cards: list[list[list[int]]]
        ) -> int:
    return sum(map(lambda card: get_score_from_card(card),
                   cards))

def solve_part_1():
    with open("input") as f:
        data = f.read()
    cards = get_game_data_from_input(data)
    return calculate_sum_of_cards(cards)

Part 2

Part two introduces the fun twist of accumulating more scratchcards. There are many possibilities to implement this algorithm, but repeated calculations will ideally be avoided; eg. card 1 duplicates cards 2, 3, 4 and 5; card 2 will duplicate 3 and 4; etc.

I'm not quite sure this was the right approach.

Tests

class Test202304_02(unittest.TestCase):
    def test_transform_input_into_game_data(self):
        cards = get_game_data_from_input_with_card_nbr(TEST_DATA)
        self.assertEqual(
                cards[:3],
                ((1, [[41, 48, 83, 86, 17],
                  [83, 86, 6, 31, 17, 9, 48, 53]]),
                 (2, [[13, 32, 20, 16, 61],
                  [61, 30, 68, 82, 17, 32, 24, 19]]),
                 (3, [[1, 21, 53, 59, 44],
                  [69, 82, 63, 72, 16, 21, 14, 1]]))
                  )

    def test_get_card_nbrs_to_repeat(self):
        cards = get_game_data_from_input_with_card_nbr(TEST_DATA)
        self.assertEqual(
            get_card_nbrs_to_repeat(cards[0]),
            [2, 3, 4, 5]
        )
        self.assertEqual(
            get_card_nbrs_to_repeat(cards[1]),
            [3, 4]
        )
        self.assertEqual(
            get_card_nbrs_to_repeat(cards[2]),
            [4, 5]
        )
        self.assertEqual(
            get_card_nbrs_to_repeat(cards[3]),
            [5]
        )
        self.assertEqual(
            get_card_nbrs_to_repeat(cards[4]),
            []
        )

    def test_get_new_card_counts(self):
        cards = get_game_data_from_input_with_card_nbr(TEST_DATA)
        current_counts = [1 for _ in range(len(cards))]
        all_counts = [
            [1, 2, 2, 2, 2, 1],
            [1, 2, 4, 4, 2, 1],
            [1, 2, 4, 8, 6, 1],
            [1, 2, 4, 8, 14, 1],
            [1, 2, 4, 8, 14, 1],
            [1, 2, 4, 8, 14, 1]
                ]
        for i in range(len(cards)):
            card = cards[i]
            current_counts = get_new_card_count(card, current_counts)
            self.assertEqual(
                    current_counts,
                    all_counts[i]
            )

    def test_get_total_card_count(self):
        cards = get_game_data_from_input_with_card_nbr(TEST_DATA)
        self.assertEqual(
                calculate_total_card_count(
                    cards,
                    [1 for _ in range(len(cards))],
                ),
                30)
```             

<h1 style="text-align: center">Code</h1>

```python
def str_to_int_list(s: str) -> list[int]:
   return list(map(int, s.split()))

def get_game_data_from_input_with_card_nbr(
        data: str
        ) -> tuple[int, tuple[list[int], list[int]]]:
    cards = tuple(map(lambda pair: [str_to_int_list(pair[0]),
                                 str_to_int_list(pair[1])],
                   map(lambda line: line.split(":")[1].split("|"),
                       data.strip().splitlines())))
    return tuple(zip(range(1, len(cards) + 1),
                    cards))

def get_card_nbrs_to_repeat(
        card: tuple[int, tuple[list[int], list[int]]],
        ) -> list:
    nbr, [winning, chance] = card
    found = [x for x in chance if x in winning]
    return (list(range(nbr + 1, nbr + 1 + len(found)))
            if len(found) >= 1 else [])
    
def get_new_card_count(
        card: tuple[int, tuple[list[int], list[int]]],
        counts: list[int]
        ) -> list[int]:
    card_nbr, _ = card
    dups = get_card_nbrs_to_repeat(card)
    return (counts[:card_nbr] +
            [counts[card_nbr - 1] + counts[nbr - 1]
             for nbr in dups]
            + counts[card_nbr + len(dups):])

def calculate_total_card_count(
        cards: list[list[list[int]]],
        current_counts: list[int],
        ) -> int:
    if len(cards) == 0:
        return sum(current_counts)
    else:
        return calculate_total_card_count(
                cards[1:],
                get_new_card_count(cards[0], current_counts)
                )

def solve_part_2():
    with open("input") as f:
        data = f.read()
    cards = get_game_data_from_input_with_card_nbr(data)
    return calculate_total_card_count(
            cards,
            [1 for _ in range(len(cards))])