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 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 __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 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 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 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 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, 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 __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(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 __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, heights: Iterable[tuple[Pos, int]]): self.heights = dict(heights) self.bounds = Rect.with_all(self.heights.keys())
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 __str__(self): rect = Rect.with_all(self.keys) return "\n".join( " ".join(self.get((x, y), " ") for x in rect.range_x()).rstrip() for y in rect.range_y() )
if claim.id_ not in claim_ids_with_overlaps) def print_claims(*claims: Claim, empty_color: str = '·', overlapping_color: str = 'X', long_id_color: str = '#', bounds: Rect | None = None): canvas: dict[Pos, str] = {} for claim in claims: color = long_id_color if len(id_str := str(claim.id_)) > 1 else id_str canvas.update((pos, overlapping_color if pos in canvas else color) for pos in claim.rect) if bounds is None: bounds = Rect.with_all(canvas).grow_by(1, 1) for y in bounds.range_y(): print(''.join( canvas.get((x, y), empty_color) for x in bounds.range_x())) def claims_from_text(text: str) -> list[Claim]: return list(claims_from_lines(text.strip().splitlines())) def claims_from_file(fn: str) -> list[Claim]: return list(claims_from_lines(open(relative_path(__file__, fn)))) def claims_from_lines(lines: Iterable[str]) -> Iterable[Claim]:
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 assemble(cls, tiles: Iterable[Tile]) -> Optional['Image']: tiles = list(tiles) # matrix of continuously placed tiles; bordering ones must match placed: dict[Pos, Tile] = {} # pool of unplaced tiles -> needs to be emptied by the end of the algorithm unplaced_tiles: dict[int, Tile] = {tile.tile_id: tile for tile in tiles} # empty positions bordering any placed tiles -> needs to be updated continuously # start with a single position where the first tile will be placed immediately fringe_positions: set[Pos] = {(0, 0)} # any further tiles will have to be checked for match with their neighbors def is_matching(tile: Tile, pos: Pos) -> bool: assert pos not in placed top, right, bottom, left = neighbors(pos) return (top not in placed or tile.border_top == placed[top].border_bottom) \ and (right not in placed or tile.border_right == placed[right].border_left) \ and (bottom not in placed or tile.border_bottom == placed[bottom].border_top) \ and (left not in placed or tile.border_left == placed[left].border_right) # four adjacent positions def neighbors(pos: Pos) -> tuple[Pos, Pos, Pos, Pos]: x, y = pos return ( (x, y - 1), # top (x + 1, y), # right (x, y + 1), # bottom (x - 1, y) # left ) # as long as there are any tiles unplaced ... while unplaced_tiles: try: # ... try placing one of them into any empty bordering position matching_tile, placed_pos = next( (candidate_orientation, fringe_pos) for candidate_tile in unplaced_tiles.values() for candidate_orientation in candidate_tile.orientations() for fringe_pos in fringe_positions if is_matching(candidate_orientation, fringe_pos) ) except StopIteration: # no matching tile found return None # found a matching tile! placed[placed_pos] = matching_tile del unplaced_tiles[matching_tile.tile_id] # update bordering positions fringe_positions.remove(placed_pos) fringe_positions.update( npos for npos in neighbors(placed_pos) if npos not in placed ) # draw # bounds = Rect.with_all(set(placed.keys()) | (fringe_positions)) # def chr(pos): # if pos == placed_pos: # return "+" # elif pos in fringe_positions: # return "." # elif pos in placed: # return "O" if pos == (0, 0) else "#" # else: # return " " # eprint(f"{len(placed)}/{len(tiles)}") # eprint("\n".join( # "".join(chr((x, y)) for x in bounds.range_x()) # for y in bounds.range_y() # )) # eprint() # everything placed! # make sure all tiles fit into a full rectangle without any gaps bounds = Rect.with_all(placed.keys()) if bounds.area != len(tiles): raise ValueError("tiles placed into non-rectangular area") # return the assembled rectangle as Image return cls( [placed[(x, y)] for x in bounds.range_x()] for y in bounds.range_y() )