• # Du code, du code, du code !

    Posté par (Mastodon) . En réponse au message Avent du Code, jour 19. Évalué à 4.

    D'abord la modélisation, avec des opérations sur triplets de matière Matters(ore, clay, obsidian). Dans un premier temps j'avais aussi geode, mais en vrai on ne produit, ne consomme, ni ne stocke de geode : c'est le score, on le gère à part.
    En vrai ça change pas grand chose...
    Un frozen dataclass, et les opérations retournent une nouvelle instance, ça permet d'éviter des risques de modifier un truc référencé ailleurs, et on y gagne en bugs et en perfs au bout du compte.

    Chaque blueprint génère une Factory qui ne sert pas à grand chose, c'est des données, c'est figé, ça bouge pas.

    Ensuite une classe d'état, State, qui stocke le temps restant, les stocks, la production, et le nombre de géodes à la fin si on touche plus à rien, donc le score actuel de cet état à la fin du temps imparti. Aussi un dataclass, je découvre, j'en mets partout, selon le biais bien connu de waooouh-nouveauté !

    import sys
    import re
    import math
    from dataclasses import dataclass
    from functools import cached_property
    @dataclass(frozen=True)
    class Matters():
     ore: int = 0
     clay: int = 0
     obsidian: int = 0
     def __add__(self, other):
     return Matters(
     self.ore + other.ore,
     self.clay + other.clay,
     self.obsidian + other.obsidian,
     )
     def __sub__(self, other):
     return Matters(
     self.ore - other.ore,
     self.clay - other.clay,
     self.obsidian - other.obsidian,
     )
     def __mul__(self, t):
     # Calculate production in t minutes
     return Matters(
     self.ore * t,
     self.clay * t,
     self.obsidian * t,
     )
     def __call__(self, name):
     return self.__dict__[name]
    @dataclass(frozen=True)
    class Factory:
     id: int
     robots: dict[Matters]
     @cached_property
     def maxproduction(self):
     return Matters(*(
     max(x)
     for x in zip(*[
     (m.ore, m.clay, m.obsidian)
     for m in self.robots.values()
     ])
     ))
    @dataclass
    class State:
     timeleft: int
     stock: Matters = Matters()
     production: Matters = Matters(1, 0, 0)
     geode: int = 0
     def __lt__(self, other):
     return self.geode < other.geode
     def buildable(self):
     return [
     (robot, self.factory.robots[robot], self.test_build_time)
     for robot in ["geode", "obsidian", "clay", "ore"]
     if self.isbuilduseful(self.factory.robots[robot])
     ]
     def isbuilduseful(self, cost):
     t = self.timetobuild(cost)
     if t is False:
     return False
     # This robot should have the time to produce something
     if t >= self.timeleft:
     return False
     self.test_build_time = t
     return True
     def timetobuild(self, cost):
     t = 0
     if cost.ore > self.stock.ore:
     t = max(math.ceil((cost.ore - self.stock.ore) / self.production.ore), t)
     if cost.clay > self.stock.clay:
     if not self.production.clay:
     return False
     t = max(math.ceil((cost.clay - self.stock.clay) / self.production.clay), t)
     if cost.obsidian > self.stock.obsidian:
     if not self.production.obsidian:
     return False
     t = max(math.ceil((cost.obsidian - self.stock.obsidian) / self.production.obsidian), t)
     return t + 1 # Time to collect enough resources, +1 to build the robot
     def buildrobot(self, robot, cost, time):
     stock = self.stock + self.production * time - cost
     time = self.timeleft - time
     if robot == "geode":
     return State(time, stock, self.production, self.geode + time)
     return State(time, stock, self.production + Matters(**{robot: 1}), self.geode)
     def __str__(self):
     return f"State Score={self.geode}, TimeLeft={self.timeleft}, "\
     f"Production={self.production}, Stocks={self.stock}"

    Ensuite le code en lui-même :

    def iteration(state):
     buildable = state.buildable()
     if not buildable: # end of the line !
     return state, 1
     explored = 0
     r = state
     if buildable[0][0] == "geode" and buildable[0][2] == 1:
     buildable = buildable[:1]
     for robot, cost, time in buildable:
     if robot != "geode" and state.production(robot) >= state.factory.maxproduction(robot):
     continue
     s, e = iteration(state.buildrobot(robot, cost, time))
     explored += e
     r = s if r < s else r
     if not explored: # robot limit attained
     state, 1
     return r, explored
    def input():
     rematter = r"ore|clay|obsidian|geode"
     rerobot = re.compile(fr"Each ({rematter}) robot costs (.*)")
     recost = re.compile(fr"(\d+) ({rematter})")
     for blueprint in sys.stdin:
     id, blueprint = blueprint.strip().split(":")
     yield Factory(
     int(id.split()[-1]), {
     build: Matters(**{b: int(a) for a, b in recost.findall(cost)})
     for robot in blueprint.strip(".").split(".")
     for build, cost in (rerobot.match(robot.strip()).groups(),)
     })
    def ex1(factories):
     score = 0
     expl = 0
     for f in factories:
     State.factory = f
     beststate, explored = iteration(State(timeleft=24))
     print(f"Blueprints#{f.id} Best of {explored} State {str(beststate)}")
     score += f.id * beststate.geode
     expl += explored
     print(f"Score final = {score} (33, 1599) {expl} chemins explorés")
    def ex2(factories):
     score = 1
     expl = 0
     for f in factories:
     State.factory = f
     beststate, explored = iteration(State(timeleft=32))
     print(f"Blueprints#{f.id} Best of {explored} State {str(beststate)}")
     score *= beststate.geode
     expl += explored
     print(f"Score final = {score} ({56*62}, {49*18*16}) {expl} chemins explorés")
    factories = list(input())
    ex1(factories)
    ex2(f for f in factories if f.id <= 3)

    Voilà voilà...

    • Yth.