from enum import Enum from heapq import heappush, heappop from tools.aoc import AOCDay from typing import Any from tools.int_seq import triangular class Material(Enum): ORE = 1 CLAY = 2 OBSIDIAN = 3 GEODE = 4 class Robot: def __init__(self, produces: Material, costs: dict, min_viable: int = 0): self.produces = produces self.costs = costs self.min_viable = min_viable class Inventory: def __init__(self): self.robots = { Material.ORE: 1, Material.CLAY: 0, Material.OBSIDIAN: 0, Material.GEODE: 0, } self.materials = { Material.ORE: 0, Material.CLAY: 0, Material.OBSIDIAN: 0, Material.GEODE: 0, } self.__build_queue = [] def gather(self) -> None: for mat, amount in self.robots.items(): self.materials[mat] += amount def build_robot(self, robot: Robot) -> None: for cost_mat, cost_amount in robot.costs.items(): self.materials[cost_mat] -= cost_amount self.robots[robot.produces] += 1 def copy(self) -> 'Inventory': new_inv = Inventory() new_inv.robots = self.robots.copy() new_inv.materials = self.materials.copy() return new_inv def __lt__(self, other: 'Inventory') -> bool: return self.materials[Material.GEODE] < other.materials[Material.GEODE] class Blueprint: def __init__(self, bp_id: int, robots: dict): self.bp_id = bp_id self.robots = robots self.max_cost = { Material.ORE: max(robot.costs[Material.ORE] for _, robot in self.robots.items()), Material.CLAY: self.robots[Material.OBSIDIAN].costs[Material.CLAY], Material.OBSIDIAN: self.robots[Material.GEODE].costs[Material.OBSIDIAN], Material.GEODE: 1e9 } def pass_minute(bp: Blueprint, inv: Inventory, ignore: list) -> (Inventory, list): could_build = [] for mat in Material: if inv.robots[mat] >= bp.max_cost[mat]: continue for robot_mat, robot_cost in bp.robots[mat].costs.items(): if inv.materials[robot_mat] < robot_cost: break else: if bp.robots[mat] not in ignore: could_build.append(bp.robots[mat]) inv.gather() return inv, could_build def get_bfs_quality(bp: Blueprint, max_time: int = 24) -> int: q = [] max_geode = 0 heappush(q, (max_time, Inventory(), [], 0)) while q: time_left, inv, ignore, no_build = heappop(q) inv, could_build = pass_minute(bp, inv, ignore) time_left -= 1 if time_left == 0: if inv.materials[Material.GEODE] > max_geode: max_geode = inv.materials[Material.GEODE] continue for robot in could_build: if robot.min_viable > time_left: continue sub_inv = inv.copy() sub_inv.build_robot(robot) heappush(q, (time_left, sub_inv, [], 0)) if no_build < bp.max_cost[Material.ORE]: heappush(q, (time_left, inv, could_build, no_build + 1)) return max_geode class Day(AOCDay): inputs = [ [ (33, "input19_test"), (1616, "input19"), ], [ #(56*62, "input19_test"), (8990, "input19"), ] ] def get_blueprints(self) -> list: bp = [] for line in self.getInput(): parts = line.split(" ") blueprint_id = int(parts[1][:-1]) ore_ore_cost = int(parts[6]) clay_ore_cost = int(parts[12]) obsi_ore_cost = int(parts[18]) obsi_clay_cost = int(parts[21]) geode_ore_cost = int(parts[27]) geode_obsi_cost = int(parts[30]) robots = { Material.ORE: Robot(Material.ORE, {Material.ORE: ore_ore_cost}, 7), Material.CLAY: Robot(Material.CLAY, {Material.ORE: clay_ore_cost}, 5), Material.OBSIDIAN: Robot(Material.OBSIDIAN, {Material.ORE: obsi_ore_cost, Material.CLAY: obsi_clay_cost}, 3), Material.GEODE: Robot(Material.GEODE, {Material.ORE: geode_ore_cost, Material.OBSIDIAN: geode_obsi_cost}, 1) } bp.append(Blueprint(blueprint_id, robots)) return bp def part1(self) -> Any: blueprints = self.get_blueprints() score = 0 for b in blueprints: quality = get_bfs_quality(b) score += quality * b.bp_id print("BP", b.bp_id, "Q", quality, "S", score) return score def part2(self) -> Any: blueprints = self.get_blueprints() score = 1 for b in blueprints[:3]: quality = get_bfs_quality(b, 32) score *= quality print("BP", b.bp_id, "Q", quality, "S", score) return score if __name__ == '__main__': day = Day(2022, 19) day.run(verbose=True)