diff --git a/2018/day15-input b/2018/day15-input new file mode 100644 index 0000000..b5fdc94 --- /dev/null +++ b/2018/day15-input @@ -0,0 +1,32 @@ +################################ +###################........##### +###################..G..#G..#### +####################........#### +##..G###############G......##### +###..G###############.....###### +#####.######..######....G##..### +#####.........####............## +#########...#####.............## +#########...####..............## +#########E#####.......GE......## +#########............E...G...### +######.###....#####..G........## +#.G#....##...#######.........### +##.#....##GG#########.........## +#....G#....E#########....#....## +#...........#########.......#### +#####..G....#########...##....## +#####....G..#########.#.......## +#######...G..#######G.....#...## +######....E...#####............# +######...GG.......E......#...E.# +#######.G...#....#..#...#.....## +#######..........#####..####.### +########.......E################ +#######..........############### +########.............########### +#########...#...##....########## +#########.....#.#..E..########## +################.....########### +################.##E.########### +################################ diff --git a/2018/day15.py b/2018/day15.py new file mode 100644 index 0000000..7d5e008 --- /dev/null +++ b/2018/day15.py @@ -0,0 +1,219 @@ +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)