187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
from typing import List, Dict, Tuple
|
|
from aocpy import BaseChallenge, foldl
|
|
|
|
|
|
Point = Tuple[int, int]
|
|
Cave = Dict[Point, int]
|
|
Basin = List[Point]
|
|
|
|
|
|
def parse(instr: str) -> Cave:
|
|
o = {}
|
|
for y, line in enumerate(instr.strip().splitlines()):
|
|
for x, digit in enumerate(line):
|
|
o[(x, y)] = int(digit)
|
|
return o
|
|
|
|
|
|
def find_adjacent_points(cave: Cave, position: Point) -> List[Point]:
|
|
# Returns a list of points that are horizontally or vertically adjacent to
|
|
# `point` that exist within `cave`.
|
|
|
|
# (x, y+1)
|
|
# (x-1, y) (x, y) (x+1, y)
|
|
# (x, y-1)
|
|
#
|
|
|
|
x, y = position
|
|
return [
|
|
pos for pos in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)] if pos in cave
|
|
]
|
|
|
|
|
|
def find_adjacent_heights(cave: Cave, position: Point) -> List[int]:
|
|
# Returns a list of heights of points horizontally or vertically adjacent to
|
|
# `point`
|
|
|
|
return [cave[pos] for pos in find_adjacent_points(cave, position) if pos in cave]
|
|
|
|
|
|
def find_low_points(cave: Cave) -> List[Point]:
|
|
# Retusn a list of points in `cave` where all adjacent points are higher
|
|
# than the current point under inspection.
|
|
|
|
o = []
|
|
for position in cave:
|
|
height = cave[position]
|
|
are_adjacent_heights_higher = [
|
|
height < adjacent for adjacent in find_adjacent_heights(cave, position)
|
|
]
|
|
if False not in are_adjacent_heights_higher:
|
|
o.append(position)
|
|
return o
|
|
|
|
|
|
def find_points_in_basin(cave: Cave, low_point: Point) -> List[Point]:
|
|
# Returns a list of points that belong to the basin in `cave` that's
|
|
# centered around `low_point`
|
|
|
|
queue = find_adjacent_points(cave, low_point)
|
|
points = [low_point]
|
|
|
|
while len(queue) != 0:
|
|
point = queue.pop(0)
|
|
|
|
if cave[point] == 9:
|
|
continue
|
|
|
|
points.append(point)
|
|
for adjacent in find_adjacent_points(cave, point):
|
|
if adjacent in queue or adjacent in points:
|
|
continue
|
|
queue.append(adjacent)
|
|
|
|
return points
|
|
|
|
|
|
def find_basins(cave: Cave) -> List[Basin]:
|
|
# Returns all the basins that exist within `cave`
|
|
|
|
low_points = find_low_points(cave)
|
|
basins = []
|
|
for point in low_points:
|
|
basins.append(find_points_in_basin(cave, point))
|
|
return basins
|
|
|
|
|
|
class Challenge(BaseChallenge):
|
|
@staticmethod
|
|
def one(instr: str) -> int:
|
|
cave = parse(instr)
|
|
|
|
cumulative_risk_level = 0
|
|
for low_point in find_low_points(cave):
|
|
cumulative_risk_level += cave[low_point] + 1
|
|
|
|
return cumulative_risk_level
|
|
|
|
@staticmethod
|
|
def two(instr: str) -> int:
|
|
cave = parse(instr)
|
|
basins = find_basins(cave)
|
|
|
|
# reverse == descending order
|
|
basins = list(sorted(basins, key=lambda x: len(x), reverse=True))
|
|
|
|
return foldl(lambda x, y: x * len(y), basins[0:3], 1)
|
|
|
|
@staticmethod
|
|
def vis(instr: str, outputDir: str):
|
|
from PIL import Image, ImageDraw
|
|
import os
|
|
from aocpy.vis import SaveManager
|
|
import shutil
|
|
|
|
COLOUR_BACKGROUND = tuple(bytes.fromhex("FFFFFF"))
|
|
COLOUR_SCANNING = tuple(bytes.fromhex("D4F1F4"))
|
|
COLOUR_LOW_POINT = tuple(bytes.fromhex("05445E"))
|
|
COLOUR_BORDER = tuple(bytes.fromhex("189AB4"))
|
|
COLOUR_BASIN = tuple(bytes.fromhex("75E6DA"))
|
|
|
|
cave = parse(instr)
|
|
|
|
max_x = max(x for x, _ in cave)
|
|
max_y = max(y for _, y in cave)
|
|
|
|
img = Image.new("RGB", (max_x + 1, max_y + 1), COLOUR_BACKGROUND)
|
|
|
|
temp_dir = os.path.join(outputDir, "vis-temp")
|
|
try:
|
|
os.makedirs(temp_dir)
|
|
except FileExistsError:
|
|
pass
|
|
|
|
manager = SaveManager(temp_dir)
|
|
|
|
# now we reimplement bits of the challenge
|
|
|
|
def find_points_in_basin(cave: Cave, low_point: Point) -> List[Point]:
|
|
queue = find_adjacent_points(cave, low_point)
|
|
points = [low_point]
|
|
|
|
while len(queue) != 0:
|
|
point = queue.pop(0)
|
|
|
|
if cave[point] == 9:
|
|
img.putpixel(point, COLOUR_BORDER)
|
|
manager.save(img)
|
|
continue
|
|
|
|
points.append(point)
|
|
img.putpixel(point, COLOUR_BASIN)
|
|
manager.save(img)
|
|
for adjacent in find_adjacent_points(cave, point):
|
|
if adjacent in queue or adjacent in points:
|
|
continue
|
|
queue.append(adjacent)
|
|
|
|
return points
|
|
|
|
def find_low_points(cave: Cave) -> List[Point]:
|
|
o = []
|
|
for position in cave:
|
|
img.putpixel(position, COLOUR_SCANNING)
|
|
manager.save(img)
|
|
|
|
height = cave[position]
|
|
are_adjacent_heights_higher = [
|
|
height < adjacent
|
|
for adjacent in find_adjacent_heights(cave, position)
|
|
]
|
|
if False not in are_adjacent_heights_higher:
|
|
o.append(position)
|
|
img.putpixel(position, COLOUR_LOW_POINT)
|
|
else:
|
|
img.putpixel(position, COLOUR_BACKGROUND)
|
|
manager.save(img)
|
|
return o
|
|
|
|
# actual challenge run
|
|
low_points = find_low_points(cave)
|
|
for point in low_points:
|
|
find_points_in_basin(cave, point)
|
|
|
|
os.system(
|
|
f"""ffmpeg -framerate 480 -i {outputDir}/vis-temp/frame_%04d.png -start_number 0 -c:v libx264 -vf "scale=iw*5:ih*5:flags=neighbor" -r 30 -pix_fmt yuv420p {outputDir}/out.mp4"""
|
|
)
|
|
shutil.rmtree(temp_dir)
|