AOC 23 - Day 02 - Cube Conundrum

Link to Advent of Code day 2

After landing in a pile of leaves on a floating island, you walk with an Elf and play a game involving a bag full of red, green and blue cubes.

Part 1

Rules

For part one of the challenge, we are given a list of games, which looks like this:

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

Each game is composed of multiple rounds, and each round is composed of a number of color cubes. For example in game one, there is 3 rounds: 3 blue, 4 red, 1 red, 2 green, 6 blue, and 2 green.

We have to determine which games can be played with a specific quantity of cubes: 12 red, 13 green, and 14 blue.

A game can be played only if it doesn't require more color cube than the available ones. For example, a game containing 10 red cubes, 14 green cubes and 15 blue cubes could not be played, as it doesn't match the requirement of at least 12 red cubes.

Code

The parsing part is the following:

from dataclasses import dataclass

@dataclass
class Cubes:
    red: int
    green: int
    blue: int


@dataclass
class Game:
    id: int
    handfuls: list[Cubes]


def get_input(input_file: str) -> list[Game]:
    games = []
    with open(input_file) as f:
        for line in f.readlines():
            left, right = line.split(":")
            game_id = int(left.split(" ")[-1])
            handfuls = [parse_handful(handful) for handful in right.split(";")]
            games.append(Game(id=game_id, handfuls=handfuls))
    return games


def parse_handful(handful_str: str) -> Cubes:
    color_counts = {'red': 0, 'blue': 0, 'green': 0}
    for part in handful_str.split(', '):
        count, color = part.strip().split(' ')
        color_counts[color] = int(count)
    return Cubes(**color_counts)

Then, the logic consists of checking whether each game has the required cubes:

def part_one(input_file: str):
    games = get_input(input_file)
    possible_games_ids = []
    max_cubes = Cubes(red=12, green=13, blue=14)
    for game in games:
        if has_game_enough_cubes(game, max_cubes):
            possible_games_ids.append(game.id)
    return sum(possible_games_ids)

def has_game_enough_cubes(game: Game, handful: Cubes) -> bool:
    for game_handful in game.handfuls:
        if game_handful.red > handful.red or game_handful.green > handful.green or game_handful.blue > handful.blue:
            return False
    return True

Part 2

Rules

We now need to determine the minimum number of cubes required to play each game.

For example, for game 1:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

The minimum required cubes are 4 red, 2 green, 6 blue.

Code

The process involves simply identifying the highest count of each color in every game.

def get_game_power(game: Game) -> int:
    min_requirements = Cubes(red=0, green=0, blue=0)
    for handful in game.handfuls:
        min_requirements.red = max(min_requirements.red, handful.red)
        min_requirements.green = max(min_requirements.green, handful.green)
        min_requirements.blue = max(min_requirements.blue, handful.blue)
    return min_requirements.red * min_requirements.green * min_requirements.blue

def part_two(input_file: str) -> int:
    games = get_input(input_file)
    return sum(get_game_power(game) for game in games)

Refactoring

We can notice than in both part 1 and part 2, we are only interested in the maximum value each color can have for a given game. This means we can discard the handfuls field from the Game class, and instead just keep the max values, avoiding useless iteration each time to them. By cleaning the dataclasses and the input part:


@dataclass
class Cubes:
    ...


@dataclass
class Game:
    id: int
    max_colors_nbr: Cubes


def parse_handful(handful_str: str) -> Cubes:
    ...

def get_input(input_file: str) -> list[Game]:
    games = []
    with open(input_file) as f:
        for line in f.readlines():
            left, right = line.split(":")
            game_id = int(left.split(" ")[-1])
            handfuls = [parse_handful(handful) for handful in right.split(";")]
            red = max(handful.red for handful in handfuls)
            blue = max(handful.blue for handful in handfuls)
            green = max(handful.green for handful in handfuls)
            games.append(Game(id=game_id, max_colors_nbr=Cubes(red=red, blue=blue, green=green)))
    return games

We can now simplify part 1 and part 2:


def part_one(input_file: str) -> int:
    games = get_input(input_file)
    max_cubes = Cubes(red=12, green=13, blue=14)
    possible_games_ids = [game.id for game in games if has_game_enough_cubes(game, max_cubes)]
    return sum(possible_games_ids)


def part_two(input_file: str) -> int:
    games = get_input(input_file)
    return sum(get_game_power(game) for game in games)


def has_game_enough_cubes(game: Game, handful: Cubes) -> bool:
    return (
            game.max_colors_nbr.red <= handful.red
            and game.max_colors_nbr.green <= handful.green
            and game.max_colors_nbr.blue <= handful.blue
    )


def get_game_power(game: Game) -> int:
    return game.max_colors_nbr.red * game.max_colors_nbr.green * game.max_colors_nbr.blue


All the code is available on github