day 15
This commit is contained in:
parent
7928de5dfe
commit
f514774857
|
@ -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()
|
|
@ -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()
|
|
@ -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.#..........#################
|
||||
#...#..#.......#################
|
||||
################################
|
Loading…
Reference in New Issue