with open('day15-input-dev', 'r') as file: data = [l.strip('\n') for l in file] # data = [ # '#######', # '#.G...#', # '#...EG#', # '#.#.#G#', # '#..G#E#', # '#.....#', # '#######', # ] import numpy as np from pathfinding.core.grid import Grid from pathfinding.finder.dijkstra import DijkstraFinder import sys grid = np.zeros((len(data), len(data[0])), dtype=np.int64) cells = {'#': 0, '.': 1, 'G': 1, 'E': 1} goblins = [] elves = [] starting_hp = 200 starting_atk = 3 starting_atk_elf = 29 adjacent = np.array([[0,-1], [-1,0], [1,0], [0,1]], dtype=np.int64) enemy_teams = [goblins, elves] unit_positions = [] pathfind_grid = None # pathfinder = AStarFinder() pathfinder = DijkstraFinder() UID = 0 class Unit: def __init__(self, x, y, starting_hp, atk, team): global UID self.team = team self.atk = atk self.hp = starting_hp self.pos = np.array([x, y], dtype=np.int64) self.uid = UID UID += 1 def __eq__(self, other): return self.uid == other.uid @property def x(self): return self.pos[0] @property def y(self): return self.pos[1] def distance(self, pos): return np.abs(pos - self.pos).sum() def path_to(self, pos, exhaustive=False): if not exhaustive: pathfind_grid = Grid(matrix=grid2) start = pathfind_grid.node(*tuple(self.pos)) end = pathfind_grid.node(*tuple(pos)) path, runs = pathfinder.find_path(start, end, pathfind_grid) return path else: paths = [] for d in range(len(adjacent)): try: trystart = tuple(self.pos + adjacent[d]) if grid2[trystart] and trystart not in unit_positions: pathfind_grid = Grid(matrix=grid2.T) start = pathfind_grid.node(*trystart) end = pathfind_grid.node(*tuple(pos)) path, runs = pathfinder.find_path(start, end, pathfind_grid) # print(pathfind_grid.grid_str(path=path, start=start, end=end)) if len(path) > 0: paths.append(path) except Exception: pass if paths: paths = sorted(paths, key=lambda x: len(x)) # print(paths) return paths[0] return [] def path_distance(self, pos): return len(self.path_to(pos, True)) or 90000 # Sorting hack def path_distance_and_priority(self, pos): path = self.path_to(pos, True) if path: for i in range(len(adjacent)): if path[0] == tuple(self.pos + adjacent[i]): priority = i break return len(path), priority return (90000, 90000) # Sorting hack def __repr__(self): return f'[{"Elf" if not self.team else "Gob"}, Position {self.x},{self.y}, HP {self.hp}]' for y, row in enumerate(data): for x, c in enumerate(row): grid[x,y] = cells[c] if c == 'E': elves.append(Unit(x, y, starting_hp, starting_atk_elf, 0)) elif c == 'G': goblins.append(Unit(x, y, starting_hp, starting_atk, 1)) grid2 = grid.copy() def turn_sort(unit): return unit.y, unit.x def target_sort(unit): return unit.hp, unit.y, unit.x def do_round(): global unit_positions, pathfind_grid combatants = sorted(goblins + elves, key=turn_sort) i = 0 while i < len(combatants): unit_positions = [tuple(u.pos) for u in combatants] grid2[:] = grid[:] for p in unit_positions: grid2[p] = -1 unit = combatants[i] i += 1 enemy_team = enemy_teams[unit.team] # If no enemies left, end combat if len(enemy_team) == 0: return # If adjacent to an enemy, attack it adjacent_enemies = [] for enemy in enemy_team: if unit.distance(enemy.pos) == 1: adjacent_enemies.append(enemy) if adjacent_enemies: # print(sorted(adjacent_enemies, key=target_sort)) target = sorted(adjacent_enemies, key=target_sort)[0] # print(f'Unit #{unit.uid} attacking unit {target.uid}') target.hp -= unit.atk if target.hp <= 0: if target.team == 0: print('Elf died, aborting!') sys.exit(1) enemy_team.remove(target) j = combatants.index(target) combatants.pop(j) if j < i: i -= 1 continue # Find all cells to move to target_cells = {tuple(t.pos + adjacent[j]) for t in enemy_team for j in range(4)} target_cells = {c for c in target_cells if grid[c] == 1 and c not in unit_positions} target_cells = sorted([np.array(c) for c in target_cells], key=lambda x: unit.path_distance_and_priority(x)) # Move to closest target cell if target_cells: path = unit.path_to(target_cells[0], exhaustive=True) if path: # print(f'Unit #{unit.uid} moving from {unit.pos} to {path[0]}') unit.pos[:] = path[0] else: pass # print(f'Unit #{unit.uid} cannot move!') # If adjacent to an enemy, attack it adjacent_enemies = [] for enemy in enemy_team: if unit.distance(enemy.pos) == 1: adjacent_enemies.append(enemy) if adjacent_enemies: target = sorted(adjacent_enemies, key=target_sort)[0] # print(f'Unit #{unit.uid} attacking unit {target.uid}') target.hp -= unit.atk if target.hp <= 0: enemy_team.remove(target) j = combatants.index(target) combatants.pop(j) if j < i: i -= 1 continue def print_state(): char_array = np.zeros_like(grid) char_array[(grid == 0)] = ord('#') char_array[(grid == 1)] = ord('.') unit_strs = [[] for i in range(len(char_array))] for g in goblins: char_array[g.x, g.y] = ord('G') unit_strs[g.y].append((f'G({g.hp})', g.x)) for e in elves: char_array[e.x, e.y] = ord('E') unit_strs[e.y].append((f'E({e.hp})', e.x)) for row in range(len(char_array)): maprow =''.join([chr(c) for c in char_array.T[row]]) unithps = ', '.join([s[0] for s in sorted(unit_strs[row], key=lambda x: x[1])]) print(maprow, unithps) def do_exterminatus(quit_after=9999999): rounds = 0 while len(goblins) and len(elves) and rounds < quit_after: print(f'Round #{rounds}') print_state() do_round() rounds += 1 return rounds round = do_exterminatus() if goblins: num = sum(u.hp for u in goblins) else: num = sum(u.hp for u in elves) print(round, num, round*num)