''' @author: olivier.massot, 2019 ''' import heapq import sys DEBUG = True def log(x): if DEBUG: print(x, file=sys.stderr) # -- Base class class Base(): def __repr__(self): return f"<{self.__class__.__name__}: {self.__dict__}>" # --- locations class Location(Base): name = "" passable = False class SpecialLocation(Location): pass class DishWasher(SpecialLocation): pass class IcecreamCrate(SpecialLocation): pass class BlueberriesCrate(SpecialLocation): pass class StrawberriesCrate(SpecialLocation): pass class DoughCrate(SpecialLocation): pass class ChoppingBoard(SpecialLocation): pass class Oven(SpecialLocation): def __init__(self): self._content = None self._timer = 0 @property def content(self): return self._content @content.setter def content(self, content): self._content = match[content] @property def timer(self): return self._timer @timer.setter def timer(self, timer): self._timer = int(timer) def update(self, content, timer): self.content = content self.timer = timer oven = Oven() class Window(SpecialLocation): pass class Start(Location): passable = True class Start0(Start): pass class Start1(Start): pass class FloorCell(Location): passable = True class EmptyTable(Location): pass locations = [DishWasher, IcecreamCrate, BlueberriesCrate, StrawberriesCrate, DoughCrate, ChoppingBoard, Oven, Window, Start0, Start1, FloorCell, EmptyTable] special_locations = [l for l in locations if l is SpecialLocation] tables = [] # -- Grid class PathNode(tuple): def __new__(self, x, y, parent=None): n = tuple.__new__(self, (x, y)) n.parent = parent n.cost = 0 return n class Grid(Base): def __init__(self, cells): self.cells = cells self.w, self.h = len(cells[0]), len(cells) self.add_costs = {} def at(self, x, y): return self.cells[y][x] def flatten(self): return [(x, y, c) for y, row in enumerate(self.cells) for x, c in enumerate(row)] @property def coords(self): return [(x, y) for y in range(self.h) for x in range(self.w)] def where_are(self, content): return [(x, y) for x, y, c in self.flatten() if c is content] @staticmethod def distance(from_, to_): return abs(from_[0] - to_[0]) + abs(from_[1] - to_[1]) def items(self): return [c for row in self.cells for c in row] def closest_in(self, from_, coords): return sorted([(c, Grid.distance(from_, c)) for c in coords], key=lambda k: k[1])[0] def closest(self, from_, content): return self.closest_in(from_, self.where_are(content)) def neighbors(self, x, y, diags=True): neighs = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if diags: neighs += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)] return [(x, y) for x, y in neighs if 0 <= x < self.w and 0 <= y < self.h] def passable(self, x, y): return self.at(x, y).passable and self.cost(x, y) < 50 def cost(self, x, y): return 10 + self.add_costs.get((x, y), 0) def path(self, origin, target, incl_start=False): nodes = [] origin = PathNode(*origin) targets = grid.neighbors(*target) heapq.heappush(nodes, (0, origin)) break_on = 10000 iteration = 0 while nodes: current = heapq.heappop(nodes)[1] if current in targets: path = [] next_ = current while next_: if next_ != origin or incl_start: path.insert(0, next_) next_ = next_.parent return path neighbors = self.neighbors(*current, False) for x, y in neighbors: iteration += 1 if iteration >= break_on: return None if not self.passable(x, y): continue cost = current.cost + self.cost(x, y) priority = cost + 10 * (abs(x - target[0]) + abs(y - target[1])) node = PathNode(x, y, current) node.cost = cost heapq.heappush(nodes, (priority, node)) else: return None ## -- objects class Recipe(Base): name = "" location = None cooking_time = 0 requirements = [] needs_free_hands = False def available(self): return [] def get(self): pass class FinalProduct(Recipe): needs_dish = True class Ingredient(Recipe): needs_free_hands = True class Dish(Recipe): name = "DISH" location = DishWasher class IceCream(FinalProduct): name = "ICE_CREAM" location = IcecreamCrate class Blueberries(FinalProduct): name = "BLUEBERRIES" location = BlueberriesCrate class Strawberries(Ingredient): name = "STRAWBERRIES" location = StrawberriesCrate needs_free_hands = True class ChoppedStrawberries(FinalProduct): name = "CHOPPED_STRAWBERRIES" location = ChoppingBoard requirements = [Strawberries] class Dough(Ingredient): name = "DOUGH" location = DoughCrate needs_free_hands = True class Croissant(FinalProduct): name = "CROISSANT" requirements = [Dough] location = Oven cooking_time = 10 class ChoppedDough(Ingredient): name = "CHOPPED_DOUGH" requirements = [Dough] location = ChoppingBoard class RawTart(Ingredient): name = "RAW_TART" requirements = [ChoppedDough] location = BlueberriesCrate class Tart(FinalProduct): name = "TART" requirements = [RawTart] location = Oven cooking_time = 10 # -- Other class Customer(Base): def __init__(self, item, award): self.order = [match[i] for i in item.split('-')] self.award = int(award) class Table(Base): def __init__(self, x, y, item): self.x = int(x) self.y = int(y) self.item = [match[i] for i in item.split('-')] @property def pos(self): return (self.x, self.y) # --- Actions class Action(Base): needs_dish = False needs_free_hands = False def __init__(self, location): self.location = location def locate(self, player): self.pos, self.dist = grid.closest(player.pos, self.location) class GetAction(Action): def __init__(self, subject): self.subject = subject self.location = self.subject.location def locate(self, player): from_crate = grid.where_are(self.location) on_tables = [t.pos for t in tables if [self.subject] == t.item] self.pos, self.dist = grid.closest_in(player.pos, from_crate + on_tables) class GetDessert(GetAction): needs_dish = True class GetIngredient(GetAction): needs_free_hands = True class GetDish(GetAction): def __init__(self): super().__init__(Dish) class Transform(Action): pass class Cook(Action): def __init__(self): self.location = Oven class GetCookedDessert(GetDessert): def __init__(self, subject, ready_in=0): super().__init__(subject) self.location = Oven self.ready_in = ready_in class Deliver(Action): def __init__(self): self.location = Window class DropHanded(Action): def __init__(self): self.subject = None self.location = EmptyTable def locate(self, player): free_tables = [c for c in grid.where_are(self.location) if not c in [(t.x, t.y) for t in tables]] self.pos, self.dist = grid.closest_in(player.pos, free_tables) class GetOnTable(GetAction): def __init__(self, tables): self._tables = tables def locate(self, player): self.pos, self.dist = grid.closest_in(player.pos, [t.pos for t in self._tables]) class GetIngredientOnTable(GetOnTable): needs_free_hands = True class GetDishOnTable(GetOnTable): pass class Cooker(Base): def __init__(self): self._x = -1 self._y = -1 self._in_hand = [] self.order = [] @property def x(self): return self._x @x.setter def x(self, x): self._x = int(x) @property def y(self): return self._y @y.setter def y(self, y): self._y = int(y) @property def pos(self): return (self.x, self.y) @property def in_hand(self): return self._in_hand @in_hand.setter def in_hand(self, item): self._in_hand = [x for x in [match[i] for i in item.split('-')] if x is not None] def take_order(self, order): self.order = order def order_fullfilled(self): self.order = [] def update(self, x, y, item): self.x = x self.y = y self.in_hand = item @property def hands_free(self): return len(self._in_hand) == 0 @property def hands_full(self): return any(recipe.needs_free_hands for recipe in self._in_hand) @property def dish_handed(self): return Dish in self.in_hand def eval_orders(self, customers): waiting = sorted(customers, reverse=True, key=lambda x: x.award) self.take_order(waiting[0].order) def get_recipe(self, recipe): on_tables = [t for t in tables if t.item == [recipe]] if on_tables: if issubclass(recipe, Ingredient): return GetIngredientOnTable(on_tables) else: return GetOnTable(on_tables) elif recipe.cooking_time and (recipe == oven.content or (any((req == oven.content) for req in recipe.requirements) and oven.timer < 3)): return GetCookedDessert(recipe) else: # check requirements if recipe.requirements: if all((req in self.in_hand) for req in recipe.requirements): if recipe.cooking_time: return Cook() else: return Transform(recipe.location) else: for req in recipe.requirements: if not req in self.in_hand and not req == oven.content: return self.get_recipe(req) else: # no requirements, simple get if issubclass(recipe, Ingredient): return GetIngredient(recipe) elif issubclass(recipe, Dish): return GetDish() else: return GetAction(recipe) def todo(self): todo = [] # nothing left to do: deliver if all((i in self.in_hand) for i in self.order): todo = [Deliver()] store = None for table in tables: if Dish in table.item and all((i in self.order and not i in self.in_hand) for i in table.item): store = table for item in self.order: if item in self.in_hand or (store and item in store.item): # already done continue action = self.get_recipe( item ) if action: todo.append( action ) if store: todo.append(GetDishOnTable([store])) for action in todo: action.locate(self) return todo def act(self, action): if isinstance(action, Deliver) and action.pos in grid.neighbors(*self.pos): # we're about to deliver, clean the order self.order = [] elif (action.needs_free_hands and not self.hands_free) or (isinstance(action, GetAction) and self.hands_full): # cannot act, needs to drop action = DropHanded() action.locate(self) log(f"Need to drop before: {action.pos}") self.use(*action.pos) def use(self, x, y, msg=""): print("USE", x, y, msg) def move(self, x, y): print("MOVE", x, y) def wait(self): print("WAIT") # --- constants match = { '0': Start0, '1': Start1, 'B': BlueberriesCrate, 'I': IcecreamCrate, 'S': StrawberriesCrate, 'C': ChoppingBoard, 'H': DoughCrate, 'W': Window, '#': EmptyTable, 'D': DishWasher, '.': FloorCell, 'O': Oven, 'NONE': None, 'DISH': Dish, 'ICE_CREAM': IceCream, 'BLUEBERRIES': Blueberries, 'STRAWBERRIES': Strawberries, 'CHOPPED_STRAWBERRIES': ChoppedStrawberries, 'DOUGH': Dough, 'CROISSANT': Croissant, 'CHOPPED_DOUGH': ChoppedDough, 'RAW_TART': RawTart, 'TART': Tart, } # --- input vars num_all_customers = int(input()) all_customers = [Customer(*input().split()) for _ in range(num_all_customers)] grid = Grid([[match[c] for c in input()] for i in range(7)]) log(f"{num_all_customers} customers: {all_customers}") log(f"grid: {grid}") player = Cooker() partner = Cooker() oven = next((i for i in grid.items() if type(i) is Oven), Oven()) path = [] blocked_since = 0 previous_partner_pos = None partner_did_not_moved_since = 0 while True: # <--- turn input turns_remaining = int(input()) player.update(*input().split()) log(f"*** player: {player}") partner.update(*input().split()) log(f"*** partner: {partner}") if partner.pos != previous_partner_pos: previous_partner_pos = partner.pos partner_did_not_moved_since = 0 else: partner_did_not_moved_since += 1 log(f"partner did not moved since: {partner_did_not_moved_since}") num_tables_with_items = int(input()) # the number of tables in the kitchen that currently hold an item tables = [Table(*input().split()) for _ in range(num_tables_with_items)] log(f"*** tables: {tables}") oven.update(*input().split()) log(f"*** oven: {oven}") num_customers = int(input()) # the number of customers currently waiting for food customers = [Customer(*input().split()) for _ in range(num_customers)] log(f"*** customers: {customers}") # ---> # ## SCRIPT # if no current order, take the most awarded if not player.order or not player.order in [c.order for c in customers]: player.eval_orders(customers) log('>>> new order taken') log(f"order: {player.order}") todo = player.todo() log(f"todo: {todo}") priority = [ # order fulfilled: deliver next((a for a in todo if type(a) is Deliver), None), # cook has a dessert in its hands and no dish, he have to take one next((a for a in todo if isinstance(a, GetDish) and any(r for r in player.in_hand if issubclass(r, FinalProduct))), None), # something to take out from the oven! next((a for a in todo if type(a) is GetCookedDessert and a.ready_in < 3), None), # If cook has an ingredient in hands, he needs to prepare it next((a for a in todo if type(a) is Transform), None), # If cook has an cook_ingredient in hands, he needs to cook it next((a for a in todo if type(a) is Cook and not oven.content), None), # If hands are free and an ingredient is needed, we go for it first next((a for a in todo if a.needs_free_hands and player.hands_free), None), # there is a dish waiting on a table next((a for a in todo if type(a) is GetDishOnTable), None), # get the closest action next((a for a in sorted(todo, key=lambda x: x.dist) if (type(a) != GetCookedDessert or a.ready_in < 3)), None), # wait for the oven to finish next((a for a in todo if type(a) is GetCookedDessert), None), ] log(f"priorities: {priority}") priority = [p for p in priority if not p is None] if not priority: log("nothing to do??") player.wait() continue next_task = priority[0] log(f"next_task: {next_task}") # <--- Update moving costs # update grid movement costs following the probability of finding the partner here partner_could_be_there = [(x, y) for x, y in grid.coords if grid.passable(x, y) and grid.distance(partner.pos, (x, y)) <= 4] grid.add_costs = {} for x, y in partner_could_be_there: k1 = (2 + (5*partner_did_not_moved_since)) if (x, y) == partner.pos else 1 # cell is next to a special cell, partner has more chance to stop there k2 = 2 if any((c for c in grid.neighbors(x, y) if isinstance(grid.at(*c), SpecialLocation))) else 1 grid.add_costs[(x, y)] = k1 * k2 * 3 log(f"add_costs: {grid.add_costs}") # ---> # <--- compute shortest path previous_path = path path = grid.path(player.pos, next_task.pos) log(f"path: {path}") # ---> # <-- try to avoid blocking situations if path and path == previous_path: log(f">> path did not change, blocked? turn: {blocked_since}") blocked_since += 1 else: blocked_since = 0 if blocked_since > 1: aside = next((c for c in grid.neighbors(*player.pos) if grid.passable(*c) and not c == partner.pos), None) if aside: blocked_since = 0 log("step aside") player.move(*aside) continue else: log("ouch, cannot move aside") # ---> if path is not None: if len(path) > 0: if len(path) > 4: player.move(*path[3]) else: player.move(*path[-1]) else: player.act(next_task) else: player.act(next_task)