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}')
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()))
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())
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())
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) )
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
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()))
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)
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)
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 }
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
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))
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))
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() )
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()))
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()))
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()))
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)
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()))
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() )
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))
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()
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()))
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())
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)
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
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))))
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)))
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)
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()