This commit is contained in:
dave 2018-12-16 20:55:26 -08:00
parent 7928de5dfe
commit f514774857
3 changed files with 390 additions and 0 deletions

234
15/a.py Executable file
View File

@ -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()

124
15/b.py Executable file
View File

@ -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()

32
15/input.txt Normal file
View File

@ -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.#..........#################
#...#..#.......#################
################################