day24 finished

This commit is contained in:
Ruediger Ludwig 2022-12-27 20:46:26 +01:00
parent db1d74d201
commit b5f193ff3c
6 changed files with 419 additions and 0 deletions

View file

View file

@ -0,0 +1,6 @@
#.######
#>>.<^<#
#.<..<<#
#>v.><>#
#<^v^^>#
######.#

View file

@ -0,0 +1,6 @@
#.######
#.>3.<.#
#<..<<.#
#>2.22.#
#>v..^<#
######.#

View file

@ -0,0 +1,22 @@
#.######################################################################################################################################################
#<v^>^>^^<><^>^^v>vv^v^>>^<>^<v<>v^>>v>v><^..><v<^v<<^v>v>>>>^<v^>>>.v>.v><<v><.^>>^^^>v>>^.<v>>vv<<.<<v<<vv^<^>^v>^v<>^^.^.<<<v^>>^<^>.>vvv><^<^>v.^<>#
#<..v<<^^.^^v<<>>v<vv.><v<<<>>>.<>>^vv^^v>v^>><v<.<<v.v^^>>v^vv<<><>>v.>.v>.<..v>^^>^<<vv>><>vv>>v>.><.<<vv<v^..v<v<v^v>v^vv<v<v^.^<v<.<<><>vvvvv^<^v>.#
#<>>^<v>v^v>v>.^v<>v<v^<<<.^^<.>vv>>^<<^>>v<v.>^^^.<<<.>>v.>.v^<v><v>.>>>^><>v>>v^<.v^<.vv^>^v<><>>v^^<^>..<v><<^<<<^<v><^>^<>v>vvv>>^<<>><><<^^.<<v>v>#
#>.<<<^..>><>v>^vv>vv<v..^>^^>>.><^<v.>>><^.^<>^v>.<^<>>>^>>^><^><^.^>.><><><^>^>>^>>>^<.>>^<^>vv<^v^><vv^v>.>^>>vv.^<>>^>>>><>.^.>v<.^<>v^>v>^^>>v<><>#
#>>v<^v^<^^><>><>.>v^v>^v.>v^.>>.><><<>^><<>^>^v>v^^^>>^v<<<>v<vv>v>>v<^^^<<<<v^vv><>v<^>>.<v.^^>^v<v<v.v..v<^^>>><.v^<.>>vv>v><><.^^^v><.<<v^>^>>><v><#
#>>vv<^vv<<v<^v^.><>^<v<>v<^v.>>v<><>.>^.^....<<^<.<v>><v>^^^<><>.>^<<v^<>>>^^><v><>>v^v<v<v^<.^<v>v^<>v^<vv^.<^v>^^^v.<><^><^v<<.v>>><>>><.v>v>>^.^>^>#
#<v<>>^v>v^vv^.^<<<^><>v^<.>>^^^<^<^>.>>^<<.^>><<<v^v>.>^^.vv>.v<v^>v><^^.<<<v^>v..<^<.vvv<.<v..<.>.v>.>>^v<^^<^v^>^.<<^^.^^<><v^^v<^^^v>v<<<<v<<>v>><>#
#<^>>^>><<>v.v>.vvv><^><v<^v<>v..<<<>>><<vv>^v><^>..<^^v>.<<<v><v^^vv^^>>.v..^<.^v<v>v.><vv>^><>vv><<>v^>v>><^^>^>><^>.>^^<<><><^.>^v<>v^.>v><>>.v^..v>#
#<.<<<^><><^<<^>><..>^<<<>^.<>>v..v^^<<<.><.^.<^>^vvv^^^^>v>^<v>>>>>v<><>v.<>>^.<>v^<.^vv>v^<v>.><v<^.><<^vv<>><>v><^v>>^^^.v^v>v>^v^>.v<<^>.^v<v>^>vv<#
#<<>^^.^>v<vvvv<<^v<v<<<v>.<>>^>^.<<^>^><<>v^.><<<.^^^<<^><<^^>.>vv<v<>>^>^^v<.^>.<v<>^>^<^>^.<>^v.^^^.v><v>^.^v^<<<><<>v.>>>>^<v>..v>^^v<v<^>v<v.>^v>>#
#<^.<^^><>^^^<<>>^>.^^^<^<^<^^<^v.^^v>^v<>.^v^^<<..v<^<>>^<..<^.>><v<>^>.^v.>>v^.v..v.<<vv^>^vvv^v<vv<^.^><><><<^v^v.^^<<>^vv>^^>v>^v<<^>^<vvv.><^.^<v>#
#<^^v^v>^<^><>v><.><v>^>^<vv<>..v^>^v^>^><>v<^v^<<v<<<<v<v..^^^<><v>v>^vv<^v.<><>vv>>>>v<>^vv<^v<>^^.v>>^^<<.<^<v<>>^>>vv>vv^<v^^>>^<v<.v<v>v<<>.>^>^v>#
#<<>>>.<<>^>>v>^<^v>><<^v<><<.^<^<>>><^v>>v>^<^vv.<^^^.vv^<<<>^^>v>^v.>^^v>v.<>vvv><<v>v>^v.<v^<>^>vv<><vv<v^<><vv.>^>.>.v^v.v^vv^<.<>.>vv>^>v<v<v>^<.<#
#>^<^<v<^^<^^v><<.<<..>>.>^^v>>v>^^>^^>>>v>v^<v^><>v>><v^<v>^>vvv>><>>vv>.>^.^<>.><v.>^.v<><v<^v><v.v<v<..^>^^<^v^>>.>v.v<><<<v><>^<<^>^>^>^.vv..<>v<^>#
#..v<<^v^^>^>^<^<vvv>v<v^^v^.<<vvv.v.v^v>^v^^v>><^><<^v.<vv<><.vv^v<<>^v<<^v^v^^.>^>v<^^v...>>vv.<^v>v^<<^<vv>v^>v<^>v<vv>^<>.>v^^^^.^<>^^.^><.<.>>.><.#
#<vv>v^<v^.vv<<.>v<<^v^>>^vv.v>>v>.vv^>>vv.>v>>>^.>>><.^.^<><>^vv<^<>><<vv<<v>^v^>>.<v^<v^<^^^<><>v<>v<....^^><.<^^>v^v>v<v<.><<>>^<>.v.<.vv<.>^.^v^>.>#
#<<v<^vv^>v>>^>.>^^^<.>>^<><>>.>^v^<.v^^.^<<^>^.^v^^v<v<><>>.>.<>.^.v>.<v>^><^>v>v<>.^v<><.<<.><<^v<><><^>v^...v<<^^<^>v>^^vv^^vvv>^>v>>.>>v^^.vv>^<v<>#
#>v<>^v><>^>>><<^.^v<<v^>v<>>>.^v><^v<<>v<^^.<.><><v>^<><.v<v<<^^^>>><^>^>>.<>vvvv.^^v<<>^<>>><<v^v><v>>>^v.<^>.^>><v><<><vv.v<vv<v^^>v<.<^<^>v^^>v^.><#
#><v>vv^<^.<^>v^v^<><><vvv^v>v<>>v^<>^>>>^v^<v>>v<<^.^<v^vvv^^<><vvv.<vv^.>>v^<v.^vv><vv<>v^<^<^^>^^^vv^.<><>><^v.<^<.<>.>^<^<^<>v<^.<v<v<v>^vv><.v<^>>#
#>v^.v>^v<>>v^>>>^^^^>.v<<^>^v>.^^>^>v^<^v^^<^^v>v>.v^<v>^v><^^.><^^>>v>v^v<v.<^>><>vv><v^v^><>^vvv>>v>>^v^^<^>>v^<^vv^<^^^^vv<><vv<.v^><v>^^>v^v<><<<<#
######################################################################################################################################################.#

View file

@ -0,0 +1,336 @@
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum
from queue import PriorityQueue
from typing import Iterator
day_num = 24
def part1(lines: Iterator[str]) -> int:
valley = Valley.parse(lines)
return valley.find_way(1)
def part2(lines: Iterator[str]) -> int:
valley = Valley.parse(lines)
return valley.find_way(3)
def gcd(num1: int, num2: int) -> int:
assert num1 >= 0 and num2 >= 0
if num1 == 0 or num2 == 0:
return 0
while num2 != 0:
num1, num2 = num2, num1 % num2
return num1
def lcm(num1: int, num2: int) -> int:
return num1 * num2 // gcd(num1, num2)
Position = tuple[int, int]
BlizList = list[Position]
BlizTuple = tuple[BlizList, BlizList, BlizList, BlizList]
class Direction(IntEnum):
East = 0
North = 1
West = 2
South = 3
@classmethod
def create(cls, char: str) -> Direction:
match char:
case '>': return Direction.East
case '^': return Direction.North
case '<': return Direction.West
case 'v': return Direction.South
case _: raise Exception("Illegal Direction")
def position(self) -> Position:
match self:
case Direction.East: return 1, 0
case Direction.North: return 0, -1
case Direction.West: return -1, 0
case Direction.South: return 0, 1
@property
def char(self) -> str:
match self:
case Direction.East: return ">"
case Direction.North: return "^"
case Direction.West: return "<"
case Direction.South: return "v"
@dataclass(slots=True, frozen=True)
class Weather:
blizzards: list[dict[Position, str]] = field(repr=False)
extent: Position
repeat: int
def print(self, time: int) -> list[str]:
current = self.blizzards[self.normal_time(time)]
lines: list[str] = []
for row in range(self.extent[1]):
line = ""
for col in range(self.extent[0]):
if (char := current.get((col, row))) is not None:
line += char
else:
line += '.'
lines.append(line)
return lines
def normal_time(self, time: int) -> int:
return time % self.repeat
def get(self, time: int) -> dict[Position, str]:
return self.blizzards[time % self.repeat]
@classmethod
def predict_weather(cls, blizzards: BlizTuple, extent: Position) -> Weather:
repeat = lcm(extent[0], extent[1])
weather: list[dict[Position, str]] = [Weather.create_dict(blizzards)]
for _ in range(repeat - 1):
blizzards = Weather.progress_blizzards(blizzards, extent)
weather.append(Weather.create_dict(blizzards))
return Weather(weather, extent, repeat)
@classmethod
def move_east(cls, blizzards: BlizList, extent: Position) -> BlizList:
add = Direction.East.position()
result: BlizList = []
for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1]
if next_pos[0] >= extent[0]:
next_pos = 0, next_pos[1]
result.append(next_pos)
return result
@classmethod
def move_west(cls, blizzards: BlizList, extent: Position) -> BlizList:
add = Direction.West.position()
result: BlizList = []
for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1]
if next_pos[0] < 0:
next_pos = extent[0] - 1, next_pos[1]
result.append(next_pos)
return result
@classmethod
def move_south(cls, blizzards: BlizList, extent: Position) -> BlizList:
add = Direction.South.position()
result: BlizList = []
for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1]
if next_pos[1] >= extent[1]:
next_pos = next_pos[0], 0
result.append(next_pos)
return result
@classmethod
def move_north(cls, blizzards: BlizList, extent: Position) -> BlizList:
add = Direction.North.position()
result: BlizList = []
for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1]
if next_pos[1] < 0:
next_pos = next_pos[0], extent[1] - 1
result.append(next_pos)
return result
@classmethod
def _add_list(cls, map: dict[Position, str], lst: BlizList, char: str) -> dict[Position, str]:
for position in lst:
match map.get(position):
case None: map[position] = char
case '2': map[position] = '3'
case '3': map[position] = '4'
case _: map[position] = '2'
return map
@classmethod
def create_dict(cls, blizzards: BlizTuple) -> dict[Position, str]:
map = Weather._add_list({}, blizzards[0], Direction.East.char)
map = Weather._add_list(map, blizzards[1], Direction.North.char)
map = Weather._add_list(map, blizzards[2], Direction.West.char)
return Weather._add_list(map, blizzards[3], Direction.South.char)
@classmethod
def progress_blizzards(cls, blizzards: BlizTuple, extent: Position) -> BlizTuple:
return (
Weather.move_east(blizzards[0], extent),
Weather.move_north(blizzards[1], extent),
Weather.move_west(blizzards[2], extent),
Weather.move_south(blizzards[3], extent),
)
@dataclass(slots=True, frozen=True)
class Valley:
weather: Weather = field(repr=False)
extent: Position
start: Position
exit: Position
@classmethod
def append(cls, blizzards: BlizTuple, direction: Direction, position: Position) -> BlizTuple:
return tuple(
directed if num != direction else directed + [position]
for num, directed in enumerate(blizzards)
)
@classmethod
def _get_wallbreak(cls, lines: str) -> int:
for col, tile in enumerate(lines[1:]):
if tile == '.':
return col
raise Exception("No break in the wall")
@classmethod
def parse_line(cls, blizzards: BlizTuple, line: str, row: int) -> BlizTuple:
for col, char in enumerate(line[1:]):
match char:
case '#':
return blizzards
case '>' | '^' | '<' | 'v':
blizzards = Valley.append(blizzards, Direction.create(char), (col, row))
case '.':
pass
case _:
raise Exception("Unknown char: {char}")
raise Exception("line not terminated by wall")
@classmethod
def parse(cls, lines: Iterator[str]) -> Valley:
first_line = next(lines)
start_col = Valley._get_wallbreak(first_line)
width = len(first_line) - 2
blizzards: BlizTuple = [], [], [], []
for row, line in enumerate(lines):
if line.startswith("##"):
end_col = Valley._get_wallbreak(line)
extent = width, row
return Valley(Weather.predict_weather(blizzards, extent), extent,
(start_col, -1), (end_col, row))
else:
blizzards = Valley.parse_line(blizzards, line, row)
assert False, "Unreachable"
def __str__(self) -> str:
return '\n'.join(self.print(0))
def print(self, time: int) -> list[str]:
first = '#' + ('#' * self.start[0]) + '.' + ('#' * (self.extent[0] - self.start[0]))
last = '#' + ('#' * self.exit[0]) + '.' + ('#' * (self.extent[0] - self.exit[0]))
lines = self.weather.print(time)
lines = [first] + ['#' + line + '#' for line in lines] + [last]
return lines
def find_way(self, rounds: int) -> int:
queue: PriorityQueue[Step] = PriorityQueue()
queue.put(Step(
position=self.start,
time=0,
round=0,
valley=self,
start=self.start,
target=self.exit))
reached: set[tuple[Position, int, int]] = set()
while not queue.empty():
current = queue.get()
if current.round == rounds:
return current.time
normal_time = self.weather.normal_time(current.time)
if (current.position, normal_time, current.round) in reached:
continue
reached.add((current.position, normal_time, current.round))
for next in current.possible_moves():
queue.put(next)
raise Exception("No path found")
@dataclass(slots=True, kw_only=True)
class Step:
position: Position
time: int
round: int
start: Position
target: Position
valley: Valley
def __lt__(self, other: Step) -> bool:
return self.time < other.time
def __str__(self) -> str:
return '\n'.join(self.print())
def print(self) -> list[str]:
lines = self.valley.print(self.time)
line = lines[self.position[1] + 1]
lines[self.position[1] + 1] = line[:self.position[0] + 1] + \
'E' + line[self.position[0] + 2:]
return lines
def reach_target(self) -> Step:
path = Step(position=self.target,
time=self.time + 1,
round=self.round + 1,
valley=self.valley,
start=self.target,
target=self.start,
)
return path
def move(self, position: Position) -> Step:
return Step(position=position,
time=self.time + 1,
round=self.round,
valley=self.valley,
start=self.start,
target=self.target,
)
def wait(self) -> Step:
return Step(position=self.position,
time=self.time + 1,
round=self.round,
valley=self.valley,
start=self.start,
target=self.target,
)
def possible_moves(self) -> Iterator[Step]:
impassable = self.valley.weather.get(self.time + 1)
if self.position not in impassable:
yield self.wait()
for dir in Direction:
add = dir.position()
next_position = self.position[0] + add[0], self.position[1] + add[1]
if next_position == self.target:
yield self.reach_target()
elif (0 <= next_position[0] < self.valley.extent[0]
and 0 <= next_position[1] < self.valley.extent[1]):
if next_position not in impassable:
yield self.move(next_position)

View file

@ -0,0 +1,49 @@
from advent.common import input
from .solution import BlizTuple, Valley, day_num, part1, part2
def test_part1():
lines = input.read_lines(day_num, 'example01.txt')
expected = 18
result = part1(lines)
assert result == expected
def test_part2():
lines = input.read_lines(day_num, 'example01.txt')
expected = 54
result = part2(lines)
assert result == expected
def test_parse_line():
input = "#>>.<^<#"
expected: BlizTuple = (
[(0, 0), (1, 0)],
[(4, 0)],
[(3, 0), (5, 0)],
[])
result = Valley.parse_line(([], [], [], []), input, 0)
assert result == expected
def test_walk():
lines = input.read_lines(day_num, 'example01.txt')
valley = Valley.parse(lines)
result = valley.find_way(1)
assert result == 18
def test_walk2():
lines = input.read_lines(day_num, 'example01.txt')
valley = Valley.parse(lines)
result = valley.find_way(2)
assert result == (18 + 23)
def test_back_and_forth():
lines = input.read_lines(day_num, 'example01.txt')
valley = Valley.parse(lines)
result = valley.find_way(3)
assert result == 54