Skip to content

Commit

Permalink
2022 d19
Browse files Browse the repository at this point in the history
  • Loading branch information
vLabayen committed Dec 2, 2023
1 parent 5789e86 commit 3ba9d4a
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 0 deletions.
218 changes: 218 additions & 0 deletions vLabayen/2022/d19/d19.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
from typing import Iterable, Dict, Tuple, List, Set
from attrs import define, field
from enum import IntEnum, auto
from itertools import count
import re
import logging


# https://es.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF
def triangular_number(n: int) -> int:
if n <= 0: return 0
return n * (n + 1) // 2

# Precompute for all required numbers
triangular_numbers = {n: triangular_number(n) for n in range(32)}

@define(kw_only=True)
class Resources:
ore : int = 0
clay : int = 0
obsidian: int = 0
geode : int = 0

def __add__(self, other: 'Resources'): return Resources(
ore = self.ore + other.ore,
clay = self.clay + other.clay,
obsidian = self.obsidian + other.obsidian,
geode = self.geode + other.geode,
)

def __sub__(self, other: 'Resources'): return Resources(
ore = self.ore - other.ore,
clay = self.clay - other.clay,
obsidian = self.obsidian - other.obsidian,
geode = self.geode - other.geode,
)

def __le__(self, other: 'Resources'): return all((
self.ore <= other.ore,
self.clay <= other.clay,
self.obsidian <= other.obsidian,
self.geode <= other.geode,
))

class BuildOption(IntEnum):
NOP = auto()
ORE = auto()
CLAY = auto()
OBSIDIAN = auto()
GEODE = auto()

def __repr__(self) -> str: return self.name

@define
class Blueprint:
id : int
ore : Resources
clay : Resources
obsidian: Resources
geode : Resources

max_costs: 'Resources' = field(init=False)
robot_costs: Dict[BuildOption, Resources] = field(init=False)
def __attrs_post_init__(self):
self.robot_costs = {
BuildOption.ORE : self.ore,
BuildOption.CLAY : self.clay,
BuildOption.OBSIDIAN: self.obsidian,
BuildOption.GEODE : self.geode,
}
self.max_costs = Resources(
ore = max(costs.ore for costs in self.robot_costs.values()),
clay = max(costs.clay for costs in self.robot_costs.values()),
obsidian = max(costs.obsidian for costs in self.robot_costs.values()),
geode = max(costs.geode for costs in self.robot_costs.values()),
)

def is_buildable(self, resources: Resources, option: BuildOption) -> bool:
if option == BuildOption.NOP: return True
return self.robot_costs[option] <= resources

@staticmethod
def from_file(file: str) -> Iterable['Blueprint']:
parse_blueprint = re.compile(r'Blueprint (\d+): Each ore robot costs (\d+) ore. Each clay robot costs (\d+) ore. Each obsidian robot costs (\d+) ore and (\d+) clay. Each geode robot costs (\d+) ore and (\d+) obsidian.')
with open(file, 'r') as f:
for line in f:
m = parse_blueprint.match(line)
if not m: raise ValueError(f'Failed to pase line: {line}')

bp_id, ore_ore, clay_ore, obsidian_ore, obsidian_clay, geode_ore, geode_obsidian = (int(v) for v in m.groups())
yield Blueprint(bp_id,
ore = Resources(ore = ore_ore),
clay = Resources(ore = clay_ore),
obsidian = Resources(ore = obsidian_ore, clay = obsidian_clay),
geode = Resources(ore = geode_ore, obsidian = geode_obsidian)
)

def best_collect_time(mineral_amount: int) -> int:
''' Compute the best time to collect the given amount of any mineral, assuming a current production of 0 and infinite of the required resources '''
remaining = mineral_amount
for i in count():
remaining -= i
if remaining <= 0: return i

return 0 # Just to make linter shut up

def max_aditional_geode(resources: Resources, production: Resources, blueprint: Blueprint, time: int) -> int:
# If we assume we have infinite resources, the max generated geodes will be the triangular number of time - 1
# if no clay robot is available, we need aditional time to at least collect enough clay to build the first obsidian robot
# if no obsidian robot is available, we need additional time to at least collect enough obsidian for the first geode robot
build_time = 1
if production.clay == 0: build_time += best_collect_time(resources.clay - blueprint.obsidian.clay)
if production.obsidian == 0: build_time += best_collect_time(resources.obsidian - blueprint.geode.obsidian)
return triangular_numbers[time - build_time]

def ensured_resources(resources: Resources, production: Resources, time: int) -> int:
return resources.geode + (production.geode * time)


def build_options(resources: Resources, production: Resources, blueprint: Blueprint, time: int, choices: List[BuildOption]) -> Iterable[Tuple[BuildOption, Resources, Resources]]:
''' Return an iterable with all the available options
Each option consists in: (OptionIdentifier, OptionCosts, ProductionIncrease)
'''
# If there is just one turn left, there is no point in start building anything
if time <= 1:
yield BuildOption.NOP, Resources(), Resources()
return

# If we can build a geode robot, just do it
if blueprint.is_buildable(resources, BuildOption.GEODE):
yield BuildOption.GEODE, blueprint.robot_costs[BuildOption.GEODE], Resources(geode=1)
return


# When a NOPE is generated having the option to build some robots,
# those robots should become banned from being built again until something else is built,
# since there is no point in delaying it
banned_robots: Set[BuildOption] = set()
if len(choices) > 0 and choices[-1] == BuildOption.NOP:
prev_resources = resources - production
banned_robots = set(robot for robot in blueprint.robot_costs if blueprint.is_buildable(prev_resources, robot))


# Yield every buildable robot, but do not build a robot if it's production is >= greater cost of that resource
if BuildOption.OBSIDIAN not in banned_robots and production.obsidian < blueprint.max_costs.obsidian and blueprint.is_buildable(resources, BuildOption.OBSIDIAN):
yield BuildOption.OBSIDIAN, blueprint.robot_costs[BuildOption.OBSIDIAN], Resources(obsidian=1)

if BuildOption.CLAY not in banned_robots and production.clay < blueprint.max_costs.clay and blueprint.is_buildable(resources, BuildOption.CLAY):
yield BuildOption.CLAY, blueprint.robot_costs[BuildOption.CLAY], Resources(clay=1)

if BuildOption.ORE not in banned_robots and production.ore < blueprint.max_costs.ore and blueprint.is_buildable(resources, BuildOption.ORE):
yield BuildOption.ORE, blueprint.robot_costs[BuildOption.ORE], Resources(ore=1)

yield BuildOption.NOP, Resources(), Resources()


def compute_largest_number_of_geodes(resources: Resources, production: Resources, blueprint: Blueprint, time: int, lower_limit: int = 0, choices: List[BuildOption] = [], **kwargs) -> int:
max_remaining_geodes = max_aditional_geode(resources, production, blueprint, time)
ensured_geodes = ensured_resources(resources, production, time)
upper_limit = ensured_geodes + max_remaining_geodes

if max_remaining_geodes == 0 or upper_limit <= lower_limit:
# print(f'{lower_limit=}, {ensured_geodes=}, {choices=}')
return ensured_geodes

highest_geodes = 0
for opt, spend, production_increase in build_options(resources, production, blueprint, time, choices):
max_geodes = compute_largest_number_of_geodes(
resources - spend + production,
production + production_increase,
blueprint,
time - 1,
lower_limit = lower_limit,
choices = [*choices, opt],
# **{
# 'debug_resources': resources - spend + production,
# 'debug_production': production + production_increase,
# 'debug_time': time - 1,
# }
)

highest_geodes = max(highest_geodes, max_geodes)
lower_limit = max(lower_limit, highest_geodes)

return highest_geodes


def p1(args):
blueprints = list(Blueprint.from_file(args.file))

quality_levels = []
for bp in blueprints:
max_geodes = compute_largest_number_of_geodes(Resources(), Resources(ore=1), bp, time=24)
quality_levels.append(bp.id * max_geodes)

print(sum(quality_levels))

def p2(args):
blueprints = list(Blueprint.from_file(args.file))[:3]

result = 1
for bp in blueprints:
max_geodes = compute_largest_number_of_geodes(Resources(), Resources(ore=1), bp, time=32)
result *= max_geodes

print(result)

if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--file', type=str, default='input.txt')
parser.add_argument('-v', '--verbose', type=str, choices={'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'}, default='WARNING')
args = parser.parse_args()

logging.basicConfig(level=args.verbose)

p1(args)
p2(args)
2 changes: 2 additions & 0 deletions vLabayen/2022/d19/example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian.
Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian.
30 changes: 30 additions & 0 deletions vLabayen/2022/d19/input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Blueprint 1: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 3 ore and 14 obsidian.
Blueprint 2: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 2 ore and 20 obsidian.
Blueprint 3: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 16 clay. Each geode robot costs 3 ore and 9 obsidian.
Blueprint 4: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 2 ore and 13 obsidian.
Blueprint 5: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 16 clay. Each geode robot costs 3 ore and 13 obsidian.
Blueprint 6: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 3 ore and 14 obsidian.
Blueprint 7: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 6 clay. Each geode robot costs 2 ore and 20 obsidian.
Blueprint 8: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 5 clay. Each geode robot costs 4 ore and 8 obsidian.
Blueprint 9: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 3 ore and 8 obsidian.
Blueprint 10: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 3 ore and 8 obsidian.
Blueprint 11: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 4 ore and 13 obsidian.
Blueprint 12: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 4 ore and 18 obsidian.
Blueprint 13: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 16 clay. Each geode robot costs 4 ore and 16 obsidian.
Blueprint 14: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 16 obsidian.
Blueprint 15: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 11 clay. Each geode robot costs 3 ore and 8 obsidian.
Blueprint 16: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 19 clay. Each geode robot costs 4 ore and 12 obsidian.
Blueprint 17: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 3 ore and 15 obsidian.
Blueprint 18: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 4 ore and 20 obsidian.
Blueprint 19: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 4 ore and 9 obsidian.
Blueprint 20: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 2 ore and 9 obsidian.
Blueprint 21: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 3 ore and 19 obsidian.
Blueprint 22: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 17 clay. Each geode robot costs 3 ore and 13 obsidian.
Blueprint 23: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 18 clay. Each geode robot costs 4 ore and 19 obsidian.
Blueprint 24: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 17 clay. Each geode robot costs 2 ore and 13 obsidian.
Blueprint 25: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 3 ore and 16 obsidian.
Blueprint 26: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 15 clay. Each geode robot costs 2 ore and 13 obsidian.
Blueprint 27: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 18 clay. Each geode robot costs 4 ore and 9 obsidian.
Blueprint 28: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 7 clay. Each geode robot costs 2 ore and 19 obsidian.
Blueprint 29: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 4 ore and 17 obsidian.
Blueprint 30: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 9 clay. Each geode robot costs 4 ore and 16 obsidian.

0 comments on commit 3ba9d4a

Please sign in to comment.