Esempio n. 1
0
def draw_map(seed: int,
             region: Rect,
             *,
             highlighted: Iterable[Pos] = (),
             char_space='·',
             char_wall='#',
             char_highlighted='O') -> None:

    highlighted = set(highlighted)

    def char(pos: Pos) -> str:
        if pos in highlighted:
            return char_highlighted
        elif is_wall(seed, pos):
            return char_wall
        else:
            return char_space

    assert region.width <= 10
    assert region.height <= 10

    x_coor = "".join(str(x)[-1] for x in region.range_x())
    print(f"  {x_coor}")

    for y in region.range_y():
        row = ''.join(char((x, y)) for x in region.range_x())
        print(f'{y} {row}')
Esempio n. 2
0
def draw_coordinates(
    coordinates: Iterable[Pos],
    including_areas: bool = False,
    distance_limit: int = None,
    bounds: Rect = Rect.at_origin(10, 10),
    empty_char: str = '·',
    safe_char: str = '#',
) -> None:
    coordinates_list = list(coordinates)

    # draw points
    canvas = dict(zip(coordinates_list, string.ascii_uppercase))

    if including_areas:
        # draw claims for part 1
        areas = claim_areas(coordinates_list, include_infinite=True)
        canvas.update((area_pos, canvas[pos].lower())
                      for pos, area in areas.items() for area_pos in area
                      if area_pos not in canvas)

    elif distance_limit is not None:
        # draw safe region for part 2
        region = safe_region(coordinates_list, distance_limit)
        canvas.update((pos, safe_char) for pos in region if pos not in canvas)

    for y in bounds.range_y():
        print("".join(
            canvas.get((x, y), empty_char) for x in bounds.range_x()))
Esempio n. 3
0
def drawn(stars: Stars,
          bounds: Rect = None,
          char_star='*',
          char_sky='·') -> str:
    positions = set(tuple(pos) for pos, _ in stars)
    if bounds is None:
        bounds = Rect.with_all((x, y) for x, y in positions)

    return "\n".join(''.join(char_star if (x, y) in positions else char_sky
                             for x in bounds.range_x())
                     for y in bounds.range_y())
Esempio n. 4
0
    def drawn(self,
              collisions: Iterable[Pos] = None,
              coordinates: bool = False) -> str:
        collisions_set = set(collisions or ())

        def char(pos: Pos) -> str:
            if pos in self.carts:
                return str(self.carts[pos])
            elif pos in collisions_set:
                return 'X'
            elif pos in self.tracks:
                return self.tracks[pos]
            else:
                return ' '

        bounds = Rect.with_all(self.tracks.keys())

        def lines() -> Iterable[str]:
            if coordinates:
                for coordinates_row in zip(*(str(x).rjust(2)
                                             for x in bounds.range_x())):
                    yield " " + "".join(coordinates_row)
            for y in bounds.range_y():
                y_coor = str(y) if coordinates else ""
                yield y_coor + "".join(char((x, y))
                                       for x in bounds.range_x()).rstrip()

        return "\n".join(lines())
Esempio n. 5
0
    def from_lines(cls, lines: Iterable[str]):
        height, width = 0, None
        trees: set[Pos] = set()

        for y, line in enumerate(lines):
            line = line.strip()
            assert all(ch in (cls.TREE_CHAR, cls.OPEN_CHAR) for ch in line)

            height += 1
            if width is None:
                # width is determined by the length of the first line
                width = len(line)
            else:
                # all lines must have the equal length
                assert width == len(line)

            trees.update(
                (x, y)
                for x, ch in enumerate(line)
                if ch == cls.TREE_CHAR
            )

        if width is None:
            raise ValueError("no lines")

        return cls(
            trees=trees,
            bounds=Rect.at_origin(width=width, height=height)
        )
Esempio n. 6
0
 def __init__(self, board: Board, sources: Iterable[Pos]):
     self.board = board
     self.sources = list(sources)
     self.scoring_bounds = Rect.with_all(self.board.keys())
     self.drawing_bounds = self.scoring_bounds.grow_to_fit(
         self.sources).grow_by(dx=2)
     self.ticks = 0
Esempio n. 7
0
def draw(cuboids: Iterable[Cuboid], axis_flat: str = 'z') -> None:
    axis_hor, axis_vert = {
        'x': ('y', 'z'),
        'y': ('x', 'z'),
        'z': ('x', 'y')
    }[axis_flat]
    depths = (((h, v), len(cuboid.get_range(axis_flat))) for cuboid in cuboids
              for h in cuboid.get_range(axis_hor)
              for v in cuboid.get_range(axis_vert))
    total_depths: Counter[tuple[int, int]] = Counter()
    for h_v, depth in depths:
        total_depths[h_v] += depth

    def char(pos: tuple[int, int]) -> str:
        val = total_depths[pos]
        if val == 0:
            return '·'
        elif val < 10:
            return str(val)
        else:
            return '#'

    bounds = Rect.with_all(total_depths.keys())
    left_margin = len(str(bounds.top_y)) + 1
    print(' ' * left_margin + f'{bounds.left_x} -> {axis_hor}')  # h_label
    vert_labels = [
        f'{bounds.top_y} ', '↓'.rjust(left_margin - 1) + ' ',
        axis_vert.rjust(left_margin - 1) + ' '
    ]

    for vert in bounds.range_y():
        vert_off = vert - bounds.top_y
        vert_label = vert_labels[vert_off] if vert_off < len(
            vert_labels) else ' ' * left_margin
        print(vert_label + ''.join(char((h, vert)) for h in bounds.range_x()))
Esempio n. 8
0
def highest_y(target: Rect) -> tuple[int, Vector]:
    """
    Finds the initial velocity to hit the target while reaching the highest possibly `y`.
    """

    # find any `vx_0` that will eventually stall to `vx=0` when `x` is in the target x range
    # e.g. target x=12..20
    #   - vx_0=4 (0:4, 4:3, 7:2, 9:1, 10:0) doesn't reach
    #   - vx_0=5 (0:5, 5:4, 9:3, 12:2, 14:1, 15:0) stalls at x=15 ok
    #   - vx_0=6 (0:6, 6:5, 11:4, 15:3, 18:2, 20:1, 21:0) overshoot

    vx_0 = triangular_root(target.left_x) + 1
    if not triangular(vx_0) in target.range_x():
        # not possible to "stall" x in target area -> still possible to shoot into target, but
        # this calculation is not supported
        raise ValueError(f"unable to stall x in {target.range_x()}")

    # one step after the projectile returns to `y=0`, it will hit the bottom of the target y range
    # e.g. target y=-20..-30
    # -> vy_0=28 (0:28, 28:27, 55:26, ..., 405:1, tr(28)=406:0, 406:-1, ..., 0:-29, -29:-30) hit
    # -> vy_0=29 (0:29, 29:28, 57:27, ..., 434:1, tr(29)=435:0, 435:-1, ..., 0:-30, -30:-31) hit
    # -> vy_0=30 (0:30, 30:29, 59:28, ..., 464:1, tr(30)=465:0, 465:-1, ..., 0:-31, -31:-32) miss
    # => max_y=435 using vy_0=29 (tr(29)=435)

    # target.top_y is actually the bottom part of the target because y-axis is inverted
    assert target.top_y < 0
    vy_0 = -target.top_y - 1
    max_y = triangular(vy_0)

    return max_y, (vx_0, vy_0)
Esempio n. 9
0
def watch_for_message(stars: Stars, font: ocr.Font) -> tuple[str, int]:
    return next((font.read_string(drawn(stars, bounds)), tick)
                for tick, stars in tqdm(enumerate(move(stars)),
                                        desc="watching stars",
                                        unit=" ticks",
                                        unit_scale=True,
                                        delay=0.5)
                if (bounds := Rect.with_all(
                    (x, y) for (x, y), _ in stars)).height <= font.char_height)
Esempio n. 10
0
def safe_region(coordinates: Iterable[Pos], distance_limit: int) -> set[Pos]:
    # note that this wouldn't work with distance_limit large enough
    # for the safe region to overflow the bounds
    coordinates_list = list(coordinates)
    bounds = Rect.with_all(coordinates_list)
    return {
        pos
        for pos in bounds
        if sum(manhattan_distance(pos, coor)
               for coor in coordinates_list) < distance_limit
    }
Esempio n. 11
0
    def __init__(self, passages: Iterable[Pos],
                 targets: Iterable[Target] | dict[Pos, str]):
        self.passages = set(passages)
        self.targets: dict[Pos, str] = dict(targets)

        self.bounds = Rect.with_all(self.passages).grow_by(+1, +1)

        assert self.passages
        assert self.targets
        for target_pos, target_code in self.targets.items():
            assert target_pos in self.passages  # target is not in a wall
            assert len(target_code) == 1
Esempio n. 12
0
    def load(cls, fn: str):
        with open(fn) as file:
            lines = [line.rstrip() for line in file]

        height = len(lines)
        assert height > 0
        width = single_value(set(len(line) for line in lines))

        board = {(x, y): c
                 for y, line in enumerate(lines) for x, c in enumerate(line)}

        return cls(board, Rect.at_origin(width, height))
Esempio n. 13
0
def draw_map(vents: Iterable[Vent], allow_diagonal: bool = False) -> None:
    considered_vents = [
        vent for vent in vents
        if allow_diagonal or vent.is_vertical() or vent.is_horizontal()
    ]

    bounds = Rect.with_all(pos for vent in considered_vents
                           for pos in (vent.pos_1, vent.pos_2))
    counts = Counter(p for vent in considered_vents for p in vent.points())
    lines = (''.join(str(counts[(x, y)] or '·') for x in bounds.range_x())
             for y in bounds.range_y())
    print('\n'.join(lines))
Esempio n. 14
0
    def __init__(self, tiles: Iterable[tuple[Pos, str]]):
        self.tiles = {
            pos: ch
            for pos, ch in tiles
            if ch != self.FLOOR
        }
        self.bounds = Rect.with_all(self.tiles.keys())
        self.rounds = 0

        assert all(
            tile in (self.EMPTY_SEAT, self.OCCUPIED_SEAT)
            for tile in self.tiles.values()
        )
Esempio n. 15
0
    def draw(self, z: int) -> None:
        flat_scanners = {(s.pos.x, s.pos.y) for s in self.scanners if s.pos.z == z}
        flat_beacons = {(b.x, b.y) for b in self.all_beacons if b.z == z}
        canvas = Rect.with_all(flat_scanners | flat_beacons)

        def char(pos: tuple[int, int]) -> str:
            if pos in flat_scanners:
                return 'S'
            elif pos in flat_beacons:
                return 'B'
            else:
                return '·'

        for y in canvas.range_y():
            print(''.join(char((x, y)) for x in canvas.range_x()))
Esempio n. 16
0
    def draw(self, z: int) -> None:
        flat_beacons = {(b.x, b.y) for b in self if b.z == z}
        flat_origin = (0, 0)
        canvas = Rect.with_all(flat_beacons | {flat_origin})

        def char(pos: tuple[int, int]) -> str:
            if pos == flat_origin:
                return 'S'
            elif pos in flat_beacons:
                return 'B'
            else:
                return '·'

        for y in canvas.range_y():
            print(''.join(char((x, y)) for x in canvas.range_x()))
Esempio n. 17
0
def draw_region(x: int,
                y: int,
                serial: int,
                square_size: int = 3,
                margin: int = 1) -> None:
    bounds = Rect((x - margin, y - margin),
                  (x + square_size + margin - 1, y + square_size + margin - 1))

    def cell(c_x: int, c_y: int) -> str:
        left, right = " ", " "
        if c_x == bounds.left_x:
            left = ""
        elif c_x == bounds.right_x:
            right = ""
        elif c_y in range(y, y + square_size):
            if c_x == x:
                left = "["
            elif c_x == x + square_size - 1:
                right = "]"

        return f"{left}{power_level(c_x, c_y, serial):2}{right}"

    for dy in bounds.range_y():
        print("".join(cell(dx, dy) for dx in bounds.range_x()))
Esempio n. 18
0
    def __init__(self,
                 width: int,
                 height: int,
                 lights_on: Iterable[Pos],
                 *,
                 lights_stuck_on: Iterable[Pos] = ()):
        assert width > 0
        assert height > 0

        self.bounds = Rect.at_origin(width, height)
        self.lights_stuck_on = set(lights_stuck_on)
        self.lights_on = set(lights_on) | self.lights_stuck_on

        assert all(pos in self.bounds for pos in self.lights_on)
        assert all(pos in self.bounds for pos in self.lights_stuck_on)
Esempio n. 19
0
def draw(dots: set[Pos],
         instruction: Instruction = None,
         full_char='█',
         empty_char='·') -> None:
    def char(pos: Pos) -> str:
        if pos in dots:
            return full_char
        elif instruction and instruction.is_on_fold(pos):
            return '|' if instruction.is_vertical() else '-'
        else:
            return empty_char

    bounds = Rect.with_all(dots)
    for y in bounds.range_y():
        print(''.join(char((x, y)) for x in bounds.range_x()))
Esempio n. 20
0
    def __str__(self):
        def char(pos: Pos) -> str:
            x, y = pos
            if (x + y) % 2 == 1:
                return ' '
            elif pos in self.active_tiles:
                return '#'
            else:
                return '.'

        bounds = Rect.with_all(self.active_tiles)
        return "\n".join(
            "".join(char((x, y)) for x in bounds.range_x())
            for y in bounds.range_y()
        )
Esempio n. 21
0
def hitting_velocities(target: Rect) -> Iterable[Vector]:
    # brute force! we just need to establish some boundaries:

    # min x ... stall x from part 1
    min_vx0 = triangular_root(target.left_x)
    # max x .... shoot directly to target right edge
    max_vx0 = target.right_x
    # min y ... shoot directly to target bottom
    # (note that target.top_y is actually bottom because y-axis is inverted)
    min_vy0 = target.top_y
    # max y ... use know-how from part 1
    max_vy0 = -target.top_y - 1

    velocities = Rect((min_vx0, min_vy0), (max_vx0, max_vy0))

    return (v0 for v0 in velocities if does_hit(v0, target))
Esempio n. 22
0
    def __init__(self, nodes: Iterable[Node], target_data_pos: Pos = None):
        self.nodes = {node.pos: node for node in nodes}
        self.rect = Rect.with_all(self.nodes.keys())
        assert self.rect.area == len(self.nodes)

        if target_data_pos:
            assert target_data_pos in self.rect
            self.target_data_pos = target_data_pos
        else:
            self.target_data_pos = self.rect.top_right

        self.empty_node_pos = single_value(
            pos
            for pos, node in self.nodes.items()
            if node.used == 0
        )

        self._key = self._create_hash_key()
Esempio n. 23
0
def draw_trajectory(initial_velocity: Vector, target: Rect) -> None:
    steps = list(shoot(initial_velocity, target))

    origin = (0, 0)
    bounds = Rect.with_all([origin, target.top_left, target.bottom_right] +
                           steps)

    def char(pos: Pos) -> str:
        if pos == origin:
            return 'S'
        elif pos in steps:
            return '#'
        elif pos in target:
            return 'T'
        else:
            return '·'

    for y in reversed(bounds.range_y()):
        print(''.join(char((x, y)) for x in bounds.range_x()))
Esempio n. 24
0
    def __format__(self, format_spec: str) -> str:
        pad = int(format_spec) if format_spec else 2
        bounds = Rect.with_all(self.grid.nodes.keys()).grow_to_fit([self.pos]).grow_by(pad, pad)

        def char(pos: Pos) -> str:
            x, y = pos
            if pos == self.pos:
                suffix = "]"
            elif (x + 1, y) == self.pos:
                suffix = "["
            else:
                suffix = " "

            return self.grid[pos].char + suffix

        def lines() -> Iterable[str]:
            for y in bounds.range_y():
                status = f" ({self.heading.arrow})" if y == self.pos[1] else ""
                yield ("".join(char((x, y)) for x in bounds.range_x()) + status).rstrip()

        return "\n".join(lines())
Esempio n. 25
0
def claim_areas(coordinates: Iterable[Pos],
                include_infinite: bool = False) -> dict[Pos, set[Pos]]:
    def neighbors(pos: Pos) -> Iterable[Pos]:
        x, y = pos
        yield x + 1, y
        yield x - 1, y
        yield x, y + 1
        yield x, y - 1

    positions = sorted(coordinates)
    # determine boundaries
    bounds = Rect.with_all(positions).grow_by(+3, +3)
    # position -> claimed by which original coordinate
    claimed_by: dict[Pos, Pos | None] = {pos: pos for pos in positions}
    new_claims: dict[Pos, set[Pos]] = {pos: {pos} for pos in positions}

    while any(len(claimants) == 1 for claimants in new_claims.values()):
        # collect all new claims
        new_claims = dgroupby_pairs_set(
            (neighbor, claimant) for pos, claimants in new_claims.items()
            for neighbor in neighbors(pos) if neighbor not in claimed_by
            if neighbor in bounds for claimant in claimants)
        # mark new claims (with single claimant) and any position with draws
        claimed_by.update({
            pos: single_value(claimants) if len(claimants) == 1 else None
            for pos, claimants in new_claims.items()
        })

    # optionally ignore all claims reaching bounds (infinite?)
    if not include_infinite:
        ignored = set(claimant for pos in bounds.border_ps()
                      if (claimant := claimed_by[pos]))
    else:
        ignored = set()

    # return the areas: claimant -> set of their claimed positions
    return dgroupby_pairs_set((claimant, pos)
                              for pos, claimant in claimed_by.items()
                              if claimant if claimant not in ignored)
Esempio n. 26
0
 def __init__(self, values: Iterable[tuple[Pos, int]]):
     self.values = dict(values)
     self.bounds = Rect.with_all(self.values.keys())
     assert len(self.values) == self.bounds.area
Esempio n. 27
0
 def from_str(cls, line: str) -> 'Instruction':
     phrases = ('turn on', 'turn off', 'toggle')
     phrase = next(p for p in phrases if line.startswith(p + ' '))
     rect_str = line[len(phrase):]
     x1, y1, x2, y2 = parse_line(rect_str, '$,$ through $,$')
     return cls(phrase, Rect((int(x1), int(y1)), (int(x2), int(y2))))
Esempio n. 28
0
def target_area_from_text(text: str) -> Rect:
    # target area: x=20..30, y=-10..-5
    x1, x2, y1, y2 = parse_line(text.strip(), 'target area: x=$..$, y=$..$')
    return Rect((int(x1), int(y1)), (int(x2), int(y2)))
Esempio n. 29
0
 def __init__(self, pixels: Iterable[Pixel], others: bool = False):
     self.lit = set(pos for pos, lit in pixels if lit != others)
     self.inverted = others
     self.bounds = Rect.with_all(self.lit)
Esempio n. 30
0
 def __init__(self,
              width: int = 50,
              height: int = 6,
              pixels_on: Iterable[Pos] = None):
     self.bounds = Rect.at_origin(width, height)
     self.pixels_on: set[Pos] = set(pixels_on) if pixels_on else set()