101 lines
3.2 KiB
Python
101 lines
3.2 KiB
Python
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
|
|
from typing import ClassVar, Iterator, Self
|
|
|
|
from advent.parser.parser import P
|
|
|
|
day_num = 5
|
|
|
|
|
|
def part1(lines: Iterator[str]) -> str:
|
|
crane = Crane.parse(lines, False)
|
|
crates = crane.perform_all_moves()
|
|
return Crane.top(crates)
|
|
|
|
|
|
def part2(lines: Iterator[str]) -> str:
|
|
crane = Crane.parse(lines, True)
|
|
crates = crane.perform_all_moves()
|
|
return Crane.top(crates)
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class Move:
|
|
amount: int
|
|
frm: int
|
|
to: int
|
|
|
|
amount_parser: ClassVar[P[int]] = P.second(P.string("move "), P.unsigned())
|
|
from_parser: ClassVar[P[int]] = P.second(P.string(" from "), P.unsigned())
|
|
to_parser: ClassVar[P[int]] = P.second(P.string(" to "), P.unsigned())
|
|
move_parser: ClassVar[P[tuple[int, int, int]]] = P.seq(amount_parser, from_parser, to_parser)
|
|
|
|
@classmethod
|
|
def parse(cls, line: str) -> Self | None:
|
|
parsed = cls.move_parser.parse(line)
|
|
if parsed.is_fail():
|
|
return None
|
|
amount, frm, to = parsed.get()
|
|
return cls(amount, frm - 1, to - 1)
|
|
|
|
def do_move(self, crates: list[str], as_9001: bool) -> list[str]:
|
|
"""
|
|
Moves the given crates by the provided move. Will fail if there are not enough crates
|
|
in the stack to take crates off
|
|
"""
|
|
if as_9001:
|
|
crates[self.to] += crates[self.frm][-self.amount:]
|
|
else:
|
|
crates[self.to] += crates[self.frm][-self.amount:][::-1]
|
|
crates[self.frm] = crates[self.frm][:-self.amount]
|
|
return crates
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class Crane:
|
|
stacks: list[str]
|
|
moves: list[Move]
|
|
is_9001: bool
|
|
|
|
crate_parser: ClassVar[P[str | None]] = P.either(
|
|
P.any_char().in_brackets(), P.string(" ").replace(None))
|
|
crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.char(' '))
|
|
|
|
@classmethod
|
|
def parse_crate_row(cls, line: str) -> list[None | str] | None:
|
|
return Crane.crate_row_parser.parse(line).get_or(None)
|
|
|
|
@classmethod
|
|
def parse_stacks(cls, lines: Iterator[str]) -> list[str]:
|
|
stacks: list[str] = []
|
|
for line in lines:
|
|
crate_row = Crane.parse_crate_row(line)
|
|
if crate_row is None:
|
|
return stacks
|
|
|
|
if len(stacks) < len(crate_row):
|
|
stacks += [""] * (len(crate_row) - len(stacks))
|
|
|
|
for stack_num, crate in enumerate(crate_row):
|
|
if crate is not None:
|
|
stacks[stack_num] = crate + stacks[stack_num]
|
|
|
|
raise Exception("Can never happen")
|
|
|
|
@classmethod
|
|
def parse(cls, lines: Iterator[str], is_9001: bool) -> Self:
|
|
drawing = cls.parse_stacks(lines)
|
|
moves = [p for p in (Move.parse(line) for line in lines) if p is not None]
|
|
return cls(drawing, moves, is_9001)
|
|
|
|
@classmethod
|
|
def top(cls, crates: list[str]) -> str:
|
|
""" Lists the last item in the given stacks. Fails if any stack is empty """
|
|
return ''.join(stack[-1] for stack in crates)
|
|
|
|
def perform_all_moves(self) -> list[str]:
|
|
stacks = self.stacks
|
|
for move in self.moves:
|
|
stacks = move.do_move(stacks, self.is_9001)
|
|
return stacks
|