Part 1
I've been a bit busy - and also questioning the motivation for programming... - so I didn't go at this one right away. Yesterday (the 12th! Oh my.) I had a bit of spare time, and hacked at it. I tried to stick with functional principles, and also did some tests. Of course, I remembered how I should probably be a bit more intentional with my testing.
Tests
TEST_DATA = """Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"""
class Test202302_01(unittest.TestCase):
def test_transform_input_into_game_data(self):
gen = get_game_data_from_input(TEST_DATA)
self.assertEqual(next(gen),
"Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green")
self.assertEqual(next(gen),
"Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue")
def test_parse_round_into_ball_counts(self):
rounds_gen = get_game_data_from_input(TEST_DATA)
self.assertEqual(
get_round_ball_count(next(get_round_strings(next(rounds_gen)))),
(('b', 3),
('r', 4)),
)
def test_parse_game_input_into_tuple(self):
gen = get_game_data_from_input(TEST_DATA)
self.assertEqual(
game_to_round_tuple(next(gen)),
(
('game', 1),
(('b', 3),
('r', 4)),
(('r', 1),
('g', 2),
('b', 6)),
(('g', 2),)
)
)
# function must return the ID of the valid games
def test_filter_valid_games(self):
rounds_gen = get_game_data_from_input(TEST_DATA)
max_rgb = (12, 13, 14)
# for each game, take the maximum of the ball counts in a functional manner;
# each game has a given number of rounds, and the maximum across all rounds must
# be calculated for all balls
self.assertEqual(
is_valid_game(max_rgb, next(rounds_gen)),
1,
)
self.assertEqual(
is_valid_game(max_rgb, next(rounds_gen)),
2,
)
self.assertEqual(
is_valid_game(max_rgb, next(rounds_gen)),
0,
)
self.assertEqual(
is_valid_game(max_rgb, next(rounds_gen)),
0,
)
self.assertEqual(
is_valid_game(max_rgb, next(rounds_gen)),
5,
)
def test_sum_of_possible_games(self):
rounds_gen = get_game_data_from_input(TEST_DATA)
max_rgb = (12, 13, 14)
self.assertEqual(sum_of_id_of_valid_games(max_rgb, rounds_gen),
8)
Code
This was not a difficult exercise, albeit I probably over-engineered stuff. It feels like the workflow I strive for is writing a test, writing code that passes that test, refactoring, and then move along.
from typing import Generator
from re import findall
from itertools import chain
def get_game_data_from_input(data: str) -> Generator[str, None, None]:
yield from data.splitlines()
def get_round_strings(game_round: str) -> Generator[str, None, None]:
yield from game_round.split(';')
def get_round_ball_count(round_string: str) -> tuple:
return (tuple((y, int(x))
for x, y in findall(r'\s(\d+)\s(\w)[,]*',
round_string)))
def game_to_round_tuple(game_round: str) -> tuple:
game = int(findall(r'Game (\d+)', game_round)[0])
ball_counts = tuple(map(get_round_ball_count,
get_round_strings(game_round)))
return (('game', game),) + ball_counts
def is_valid_game(max_balls_counts: tuple,
game_round: str
) -> int:
round_tuple = game_to_round_tuple(game_round)
game_id = round_tuple[0][1]
rounds = round_tuple[1:]
flattened_rounds = list(chain.from_iterable(rounds))
max_counts = tuple(max(t[1] for t in flattened_rounds
if t[0] == ball)
for ball in 'rgb')
if all(max_count >= count
for max_count, count
in zip(max_balls_counts, max_counts)):
return game_id
else:
return 0
def sum_of_id_of_valid_games(max_balls_counts: tuple,
games: Generator
) -> int:
return sum(map(lambda game: is_valid_game(max_balls_counts,
game),
games))
with open('input', 'r') as file:
data = file.read().strip()
print(sum_of_id_of_valid_games((12, 13, 14),
get_game_data_from_input(data)))
Part 2
For part 2, I could still rely on some util functions, and instead of checking the validity of the game, it was only necessary to get the minimum number of balls that would make a game possible. Much of the exercise logic was already done. Tests were a bit more succint.
Tests
class Test202302_02(unittest.TestCase):
def test_get_minimum_balls_for_game(self):
rounds_gen = get_game_data_from_input(TEST_DATA)
self.assertEqual(
get_minimum_balls_for_game(next(rounds_gen)),
(4, 2, 6),
)
def test_power_set_of_cubes(self):
self.assertEqual(
power_set_of_cubes((4, 2, 6)),
48
)
def test_sum_of_power_set(self):
rounds_gen = get_game_data_from_input(TEST_DATA)
self.assertEqual(
sum_of_power_set_of_games(rounds_gen),
2286
)
Code
from typing import Generator
from re import findall
from itertools import chain
from functools import reduce
def get_game_data_from_input(data: str) -> Generator[str, None, None]:
yield from data.splitlines()
def get_round_strings(game_round: str) -> Generator[str, None, None]:
yield from game_round.split(';')
def get_round_ball_count(round_string: str) -> tuple:
return (tuple((y, int(x))
for x, y in findall(r'\s(\d+)\s(\w)[,]*',
round_string)))
def game_to_round_tuple(game_round: str) -> tuple:
game = int(findall(r'Game (\d+)', game_round)[0])
ball_counts = tuple(map(get_round_ball_count,
get_round_strings(game_round)))
return (('game', game),) + ball_counts
def get_minimum_balls_for_game(
game_round: str
) -> tuple[int, int, int]:
rounds = game_to_round_tuple(game_round)[1:]
flattened_rounds = list(chain.from_iterable(rounds))
max_counts = tuple(max(t[1] for t in flattened_rounds
if t[0] == ball)
for ball in 'rgb')
return max_counts
def power_set_of_cubes(cubes: tuple[int, int, int]) -> int:
return reduce(lambda x, y: x * y, cubes)
def sum_of_power_set_of_games(
games: Generator
) -> int:
return sum(map(lambda game: power_set_of_cubes(get_minimum_balls_for_game(game)),
games))
with open('input', 'r') as file:
data = file.read().strip()
print(sum_of_power_set_of_games(get_game_data_from_input(data)))