diff --git a/15/a.py b/15/a.py new file mode 100755 index 0000000..3a8b639 --- /dev/null +++ b/15/a.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 + + +from enum import Enum +from collections import deque + + +class Tile(Enum): + WALL = 0 + OPEN = 1 + + +def char2tile(char): + return {"#": Tile.WALL, + ".": Tile.OPEN}[char] + + +def tile2char(tile): + return {Tile.WALL: "#", + Tile.OPEN: "."}[tile] + + +class Unit(object): + def __init__(self, coord, cls, powerlevel=3): + self.pos = coord + self.cls = cls # G or E + self.hp = 200 + self.power = powerlevel + + def __repr__(self): + return "<{} @ {}, hp={}>".format(self.cls, self.pos, self.hp) + + +def printfield(field, units, width, height, markers=None): + """ + Print out the map. Optionally, add markers using a list of (x,y) tuples + :param field: dict of (x,y)->Tile + :param units: list of Unit objects + :param width: width of the map + :param height: height of the map + :param markers: list of (x,y) coordinates making up the path + """ + unitskeyed = {unit.pos: unit for unit in units} + + for y in range(0, height): + inrow = [] + for x in range(0, width): + c = (x, y) + if c in unitskeyed: + u = unitskeyed[c] + print(u.cls, end="") + inrow.append("{}({})".format(u.cls, u.hp)) + elif markers and c in markers: + print("o", end="") + else: + print(tile2char(field[c]), end="") + if inrow: + print(" ", ", ".join(inrow)) + else: + print() + print() + + +def findpath(field, start, end, unitscoords): + """ + Given the field and start/end (x,y) tuples, return a list of (x,y) coordinate tuples representing a path back from + end to the start. Units are counted as blocked squares + :param field: dict of (x,y)->Tile + :param start: (x,y) coordinate tuple + :param end: (x,y) coordinate tuple + :param unitscoords: list of (x,y) unit coordinate tuples + """ + frontier = deque([start]) + came_from = {} + came_from[start] = None + while True: + try: + current = frontier.pop() + except IndexError: # Runs out of spaces to move + return None + + if current == end: + break + for move in getneighbors(field, current): + if move in unitscoords: + continue + if move not in came_from: + frontier.appendleft(move) + came_from[move] = current + else: + return None # No path available + + # use the data in came_from to build a path + path = [] + position = end + while position != start: + path.append(position) + position = came_from[position] + return path + + +TRNSLATIONS = (0, -1), (-1, 0), (1, 0), (0, 1), + + +def getneighbors(field, start): + """ + Return a list of (x,y) tuples of squares that are valid moves from the start square + """ + valid = [] + for t in TRNSLATIONS: + new = cmbcoords(start, t) + if field[new] == Tile.OPEN: + valid.append(new) + return valid + + +def cmbcoords(c1, c2): + """ + Combine coordinates such that (1,2) + (3,4) = (4,6) + """ + return (c1[0] + c2[0], c1[1] + c2[1]) + + +def loadmap(fpath): + field = {} + units = [] + width = 0 + height = 0 + with open(fpath) as f: + for y, line in enumerate(f.readlines()): + height = y + 1 + for x, char in enumerate(line.strip()): + width = max(width, x + 1) + if char in set(["E", "G"]): + field[(x, y)] = Tile.OPEN + units.append(Unit((x, y), char)) + else: + field[(x, y)] = char2tile(char) + return field, units, width, height + + +def main(): + field, units, width, height = loadmap("input.txt") + printfield(field, units, width, height) + + rounds = 0 + while True: + # Put units into the order they'll be updated + units.sort(key=lambda u: u.pos[0] + u.pos[1] * 1000) + units_toupdate = units[:] + + # Process units in order + while units_toupdate: + current_unit = units_toupdate.pop(0) + + # Find desirable destinations - squares that border an enemy + enemies = [u for u in units if u.cls != current_unit.cls] + dests = set() + for enemy in enemies: + dests.update(getneighbors(field, enemy.pos)) + occupied = {u.pos for u in units if u != current_unit} # Omit occupied dests + dests -= occupied + + unitscoords = {u.pos: u for u in units} + + if current_unit.pos not in dests: + # No targets in range, unit must move + + costs = [] # list of (dest, pathlength, firstmove) + for dest in dests: + path = findpath(field, current_unit.pos, dest, unitscoords) + if not path: + continue + costs.append((dest, len(path), path[-1])) + + if costs: # A path to a dest exists + + costs.sort(key=lambda i: i[1]) + closest_cost = costs[0][1] + + next_moves = [] + + for cost in costs: + if cost[1] == closest_cost: + next_moves.append(cost) + + next_moves.sort(key=lambda i: i[0][0] + i[0][1] * 1000) + newpos = next_moves[0][2] + del unitscoords[current_unit.pos] + unitscoords[newpos] = current_unit + current_unit.pos = newpos + + if current_unit.pos in dests: + targets = [] # Units we can hit from where we are + for pos in getneighbors(field, current_unit.pos): + if pos in unitscoords and unitscoords[pos].cls != current_unit.cls: + targets.append(unitscoords[pos]) + + targets.sort(key=lambda t: t.hp) # We want to hit the lowest hp'd units + lowhp = targets[0].hp + + candidates = [] + for target in targets: + if target.hp == lowhp: + candidates.append(target) + else: + break + targets = candidates + + # reading order for targets + targets.sort(key=lambda i: i.pos[0] + i.pos[1] * 1000) + target = targets[0] + target.hp -= current_unit.power + + if target.hp <= 0: + units.remove(target) + if target in units_toupdate: + units_toupdate.remove(target) + + if len(enemies) == 1: + print("End of combat") + print() + hpsum = sum([u.hp for u in units]) + print("Rounds:", rounds) + print("HP sum:", hpsum) + print("Answer:", hpsum * rounds) + return + + printfield(field, units, width, height) + rounds += 1 + + +if __name__ == '__main__': + main() diff --git a/15/b.py b/15/b.py new file mode 100755 index 0000000..410d248 --- /dev/null +++ b/15/b.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + + +from a import Tile, loadmap, getneighbors, findpath + + +def char2tile(char): + return {"#": Tile.WALL, + ".": Tile.OPEN}[char] + + +def tile2char(tile): + return {Tile.WALL: "#", + Tile.OPEN: "."}[tile] + + +def main(): + minpower = 24 # found with some guesswork, change to suit your needs + while True: + print("Trying", minpower) + result = rungame(minpower) + if result: + return + minpower += 1 + + +def rungame(powerlevel): + field, units, width, height = loadmap("input.txt") + for u in units: + if u.cls == "E": + u.power = powerlevel + + rounds = 0 + while True: + # print("BEGIN ROUND", rounds) + # input() + + # Put units into the order they'll be updated + units.sort(key=lambda u: u.pos[0] + u.pos[1] * 1000) + units_toupdate = units[:] + + # Process units in order + while units_toupdate: + current_unit = units_toupdate.pop(0) + + # Find desirable destinations - squares that border an enemy + enemies = [u for u in units if u.cls != current_unit.cls] + dests = set() + for enemy in enemies: + dests.update(getneighbors(field, enemy.pos)) + occupied = {u.pos for u in units if u != current_unit} # Omit occupied dests + dests -= occupied + + if current_unit.pos not in dests: + # No targets in range, unit must move + + costs = [] # list of (dest, pathlength, firstmove) + for dest in dests: + unitscoords = {u.pos: u for u in units} + path = findpath(field, current_unit.pos, dest, unitscoords) + if not path: + continue + costs.append((dest, len(path), path[-1])) + + if costs: # A path to a dest exists + + costs.sort(key=lambda i: i[1]) + closest_cost = costs[0][1] + + next_moves = [] + + for cost in costs: + if cost[1] == closest_cost: + next_moves.append(cost) + + next_moves.sort(key=lambda i: i[0][0] + i[0][1] * 1000) + current_unit.pos = next_moves[0][2] + + if current_unit.pos in dests: + unitscoords = {u.pos: u for u in units} + targets = [] # Units we can hit from where we are + for pos in getneighbors(field, current_unit.pos): + if pos in unitscoords and unitscoords[pos].cls != current_unit.cls: + targets.append(unitscoords[pos]) + + targets.sort(key=lambda t: t.hp) # We want to hit the lowest hp'd units + lowhp = targets[0].hp + + candidates = [] + for target in targets: + if target.hp == lowhp: + candidates.append(target) + else: + break + targets = candidates + + # reading order for targets + targets.sort(key=lambda i: i.pos[0] + i.pos[1] * 1000) + target = targets[0] + + target.hp -= current_unit.power + + if target.hp <= 0: + if target.cls == "E": + return False + + units.remove(target) + if target in units_toupdate: + units_toupdate.remove(target) + + if len(enemies) == 1: + print("End of combat") + print() + hpsum = sum([u.hp for u in units]) + print("Rounds:", rounds) + print("HP sum:", hpsum) + print("Answer:", hpsum * rounds) + return True + + rounds += 1 + + +if __name__ == '__main__': + main() diff --git a/15/input.txt b/15/input.txt new file mode 100644 index 0000000..dabc5df --- /dev/null +++ b/15/input.txt @@ -0,0 +1,32 @@ +################################ +#######.G...#################### +#########...#################### +#########.G.#################### +#########.###################### +#########.###################### +#########G###################### +#########.#...################## +#########.....#..############### +########...G....###.....######## +#######............G....######## +#######G....G.....G....######### +######..G.....#####..G...####### +######...G...#######......###### +#####.......#########....G..E### +#####.####..#########G...#....## +####..####..#########..G....E..# +#####.####G.#########...E...E.## +#########.E.#########.........## +#####........#######.E........## +######........#####...##...#..## +###...................####.##.## +###.............#########..##### +#G#.#.....E.....#########..##### +#...#...#......##########.###### +#.G............#########.E#E#### +#..............##########...#### +##..#..........##########.E##### +#..#G..G......###########.###### +#.G.#..........################# +#...#..#.......################# +################################