2018: d15: ex2: add solution
This commit is contained in:
parent
a7a5f62afe
commit
fd604ff8a4
239
2018/d15/ex2/ex2.py
Executable file
239
2018/d15/ex2/ex2.py
Executable file
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Iterator, NamedTuple
|
||||
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
# Returned in reading order
|
||||
def neighbours(self) -> Iterator["Point"]:
|
||||
for dx, dy in (
|
||||
(-1, 0),
|
||||
(0, -1),
|
||||
(0, 1),
|
||||
(1, 0),
|
||||
):
|
||||
yield Point(self.x + dx, self.y + dy)
|
||||
|
||||
|
||||
class Unit(enum.StrEnum):
|
||||
ELF = "E"
|
||||
GOBLIN = "G"
|
||||
|
||||
def ennemy(self) -> "Unit":
|
||||
if self == Unit.ELF:
|
||||
return Unit.GOBLIN
|
||||
if self == Unit.GOBLIN:
|
||||
return Unit.ELF
|
||||
assert False # Sanity check
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class UnitData:
|
||||
hp: int = 200
|
||||
power: int = 3
|
||||
|
||||
|
||||
class ElfDiedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def solve(input: str) -> int:
|
||||
def parse(input: list[str]) -> tuple[set[Point], dict[Unit, set[Point]]]:
|
||||
walls: set[Point] = set()
|
||||
units: dict[Unit, set[Point]] = {u: set() for u in Unit}
|
||||
|
||||
for x, line in enumerate(input):
|
||||
for y, c in enumerate(line):
|
||||
p = Point(x, y)
|
||||
if c in Unit:
|
||||
units[Unit(c)].add(p)
|
||||
if c == "#":
|
||||
walls.add(p)
|
||||
|
||||
return walls, units
|
||||
|
||||
def double_bfs(
|
||||
unit_type: Unit,
|
||||
unit_pos: Point,
|
||||
walls: set[Point],
|
||||
units: dict[Unit, set[Point]],
|
||||
) -> Point | None:
|
||||
def bfs(
|
||||
start: Point,
|
||||
targets: set[Point],
|
||||
blockers: set[Point],
|
||||
) -> Point | None:
|
||||
frontier = [start]
|
||||
seen: set[Point] = set()
|
||||
while frontier:
|
||||
new_frontier: set[Point] = set()
|
||||
|
||||
for p in frontier:
|
||||
if p in targets:
|
||||
return p
|
||||
seen.add(p)
|
||||
for n in p.neighbours():
|
||||
if n in seen:
|
||||
continue
|
||||
if n in blockers:
|
||||
continue
|
||||
new_frontier.add(n)
|
||||
frontier = sorted(new_frontier)
|
||||
|
||||
return None
|
||||
|
||||
blockers = walls | units[unit_type]
|
||||
ennemies = units[unit_type.ennemy()]
|
||||
|
||||
# First BFS from start to square next to an ennemy
|
||||
targets_in_range = {
|
||||
n for ennemy in ennemies for n in ennemy.neighbours() if n not in blockers
|
||||
}
|
||||
if (target := bfs(unit_pos, targets_in_range, blockers)) is None:
|
||||
return None
|
||||
|
||||
# Then back from chosen target to one of the movement squares
|
||||
movement_squares = {n for n in unit_pos.neighbours() if n not in blockers}
|
||||
return bfs(target, movement_squares, blockers)
|
||||
|
||||
def do_move(
|
||||
unit_type: Unit,
|
||||
unit_pos: Point,
|
||||
walls: set[Point],
|
||||
units: dict[Unit, set[Point]],
|
||||
unit_data: dict[Point, UnitData],
|
||||
) -> Point:
|
||||
# If already next to an ennemy, do not move
|
||||
if any(n in units[unit_type.ennemy()] for n in unit_pos.neighbours()):
|
||||
return unit_pos
|
||||
|
||||
new_pos = double_bfs(unit_type, unit_pos, walls, units)
|
||||
|
||||
# Nowhere to move to, no-op
|
||||
if new_pos is None:
|
||||
return unit_pos
|
||||
|
||||
assert new_pos != unit_pos # Sanity check
|
||||
assert unit_pos in units[unit_type] # Sanity check
|
||||
assert new_pos not in units[unit_type] # Sanity check
|
||||
|
||||
# Make the movement in-place
|
||||
units[unit_type] ^= {unit_pos, new_pos}
|
||||
unit_data[new_pos] = unit_data.pop(unit_pos)
|
||||
|
||||
return new_pos
|
||||
|
||||
def do_attack(
|
||||
unit_type: Unit,
|
||||
unit_pos: Point,
|
||||
units: dict[Unit, set[Point]],
|
||||
unit_data: dict[Point, UnitData],
|
||||
) -> None:
|
||||
# Look for an attack target
|
||||
target = min(
|
||||
(n for n in unit_pos.neighbours() if n in units[unit_type.ennemy()]),
|
||||
key=lambda p: unit_data[p].hp,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# If not in range, no-op
|
||||
if target is None:
|
||||
return
|
||||
|
||||
assert target not in units[unit_type] # Sanity check
|
||||
assert target in units[unit_type.ennemy()] # Sanity check
|
||||
assert unit_data[target].hp > 0 # Sanity check
|
||||
|
||||
# Make the attack in-place
|
||||
unit_data[target].hp -= unit_data[unit_pos].power
|
||||
# And if we killed it, remove it from `units`
|
||||
if unit_data[target].hp <= 0:
|
||||
if unit_type.ennemy() == Unit.ELF:
|
||||
raise ElfDiedError
|
||||
units[unit_type.ennemy()].remove(target)
|
||||
unit_data.pop(target)
|
||||
|
||||
def turn(
|
||||
walls: set[Point],
|
||||
units: dict[Unit, set[Point]],
|
||||
unit_data: dict[Point, UnitData],
|
||||
) -> bool:
|
||||
turn_order = sorted((p, u) for u, points in units.items() for p in points)
|
||||
for p, u in turn_order:
|
||||
# Don't do anything if the unit is dead
|
||||
if p not in units[u]:
|
||||
continue
|
||||
|
||||
# If no ennemies left, finish the turn early and indicate that we're done
|
||||
if not units[u.ennemy()]:
|
||||
return False
|
||||
|
||||
# Movements and attacks are made in-place
|
||||
p = do_move(u, p, walls, units, unit_data)
|
||||
do_attack(u, p, units, unit_data)
|
||||
|
||||
return True
|
||||
|
||||
def print_map(walls: set[Point], units: dict[Unit, set[Point]]) -> None:
|
||||
max_x, max_y = max(p.x for p in walls), max(p.y for p in walls)
|
||||
for x in range(0, max_x + 1):
|
||||
for y in range(0, max_y + 1):
|
||||
p = Point(x, y)
|
||||
for u in Unit:
|
||||
if p in units[u]:
|
||||
print(str(u), end="")
|
||||
break
|
||||
else:
|
||||
print("#" if p in walls else ".", end="")
|
||||
print()
|
||||
print()
|
||||
|
||||
def run_to_completion(
|
||||
walls: set[Point],
|
||||
units: dict[Unit, set[Point]],
|
||||
unit_data: dict[Point, UnitData],
|
||||
) -> int:
|
||||
turns = 0
|
||||
while turn(walls, units, unit_data):
|
||||
turns += 1
|
||||
return turns * sum(data.hp for data in unit_data.values())
|
||||
|
||||
def arm_elves(
|
||||
walls: set[Point],
|
||||
units: dict[Unit, set[Point]],
|
||||
) -> int:
|
||||
for elf_power in itertools.count(start=3):
|
||||
try:
|
||||
unit_data = {
|
||||
p: UnitData(power=elf_power if u == Unit.ELF else 3)
|
||||
for u, points in units.items()
|
||||
for p in points
|
||||
}
|
||||
return run_to_completion(
|
||||
copy.deepcopy(walls),
|
||||
copy.deepcopy(units),
|
||||
unit_data,
|
||||
)
|
||||
except ElfDiedError:
|
||||
pass
|
||||
assert False # Sanity check
|
||||
|
||||
walls, units = parse(input.splitlines())
|
||||
return arm_elves(walls, units)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
input = sys.stdin.read()
|
||||
print(solve(input))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue