day23 better way to check moveable elves

This commit is contained in:
Ruediger Ludwig 2023-01-23 19:07:12 +01:00
parent 840e7abc4d
commit 867a476c44
2 changed files with 97 additions and 75 deletions

View file

@ -76,6 +76,16 @@ class Position:
yield self.left() yield self.left()
yield self.down() yield self.down()
def all_neighbors(self) -> Iterator[Position]:
yield Position(self.x + 1, self.y)
yield Position(self.x + 1, self.y - 1)
yield Position(self.x, self.y - 1)
yield Position(self.x - 1, self.y - 1)
yield Position(self.x - 1, self.y)
yield Position(self.x - 1, self.y + 1)
yield Position(self.x, self.y + 1)
yield Position(self.x + 1, self.y + 1)
def is_within(self, top_left: Position, bottom_right: Position) -> bool: def is_within(self, top_left: Position, bottom_right: Position) -> bool:
""" """
Checks if this point is within the rectangle spanned by the given positions. Checks if this point is within the rectangle spanned by the given positions.

View file

@ -3,7 +3,7 @@ from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum
from itertools import count, cycle from itertools import count, cycle
from typing import Iterable, Iterator from typing import Iterator
from advent.common.position import Position from advent.common.position import Position
@ -58,52 +58,33 @@ class Ground:
return result[:-1] return result[:-1]
@classmethod @classmethod
def check_adjacent(cls, elves: Iterable[Position], def has_neighbor(cls, elves: dict[Position, int],
position: Position) -> list[Direction] | None: position: Position, direction: Direction) -> bool:
north = False match direction:
south = False case Direction.North:
west = False return (
east = False Position(position.x - 1, position.y - 1) in elves
or Position(position.x, position.y - 1) in elves
if (position + Position(-1, -1)) in elves: or Position(position.x + 1, position.y - 1) in elves
north = True )
west = True case Direction.South:
if (position + Position(1, 1)) in elves: return (
south = True Position(position.x - 1, position.y + 1) in elves
east = True or Position(position.x, position.y + 1) in elves
or Position(position.x + 1, position.y + 1) in elves
if north is False: )
north = (position + Position(0, -1)) in elves case Direction.West:
if south is False: return (
south = (position + Position(0, 1)) in elves Position(position.x - 1, position.y - 1) in elves
if west is False: or Position(position.x - 1, position.y) in elves
west = (position + Position(-1, 0)) in elves or Position(position.x - 1, position.y + 1) in elves
if east is False: )
east = (position + Position(1, 0)) in elves case Direction.East:
return (
if north is False or east is False: Position(position.x + 1, position.y - 1) in elves
if (position + Position(1, -1)) in elves: or Position(position.x + 1, position.y) in elves
north = True or Position(position.x + 1, position.y + 1) in elves
east = True )
if south is False or west is False:
if (position + Position(-1, 1)) in elves:
south = True
west = True
if north == south == east == west:
return None
adjacent: list[Direction] = []
if north:
adjacent.append(Direction.North)
if south:
adjacent.append(Direction.South)
if west:
adjacent.append(Direction.West)
if east:
adjacent.append(Direction.East)
return adjacent
def count_empty(self) -> int: def count_empty(self) -> int:
min_pos, max_pos = self.extent() min_pos, max_pos = self.extent()
@ -121,6 +102,33 @@ class Ground:
def extent(self) -> tuple[Position, Position]: def extent(self) -> tuple[Position, Position]:
return Position.component_min(*self.map), Position.component_max(*self.map) return Position.component_min(*self.map), Position.component_max(*self.map)
@classmethod
def minmax(cls, first: int, second: int) -> tuple[int, int]:
return (first, second) if first <= second else (second, first)
@classmethod
def pair_neighbors(cls, from_pos: Position, to_pos: Position) -> Iterator[Position]:
if from_pos.x == to_pos.x:
mn, mx = Ground.minmax(from_pos.y, to_pos.y)
yield Position(from_pos.x - 1, mn - 1)
yield Position(from_pos.x, mn - 1)
yield Position(from_pos.x + 1, mn - 1)
yield Position(from_pos.x - 1, from_pos.y)
yield Position(from_pos.x + 1, from_pos.y)
yield Position(from_pos.x - 1, mx + 1)
yield Position(from_pos.x, mx + 1)
yield Position(from_pos.x + 1, mx + 1)
else:
mn, mx = Ground.minmax(from_pos.x, to_pos.x)
yield Position(mn - 1, from_pos.y - 1)
yield Position(mn - 1, from_pos.y)
yield Position(mn - 1, from_pos.y + 1)
yield Position(from_pos.x, from_pos.y - 1)
yield Position(from_pos.x, from_pos.y + 1)
yield Position(mx + 1, from_pos.y - 1)
yield Position(mx + 1, from_pos.y)
yield Position(mx + 1, from_pos.y + 1)
def rounds(self, max_rounds: int | None) -> int | None: def rounds(self, max_rounds: int | None) -> int | None:
start_dispenser = cycle(iter(Direction)) start_dispenser = cycle(iter(Direction))
if max_rounds is None: if max_rounds is None:
@ -130,50 +138,54 @@ class Ground:
elves = {position: 0 for position in self.map} elves = {position: 0 for position in self.map}
min_position, max_position = self.extent()
for round in it: for round in it:
min_position = min_position + Position(-1, -1)
max_position = max_position + Position(1, 1)
start = next(start_dispenser) start = next(start_dispenser)
proposals: dict[Position, Position] = {} proposals: dict[Position, Position] = {}
for from_pos, last_moved in elves.items(): touched: set[Position] = set()
if last_moved + 4 < round: for from_pos, last_touched in elves.items():
if not from_pos.is_within(min_position, max_position): if last_touched + 4 < round:
continue continue
adjacent = self.check_adjacent(elves, from_pos) found = False
if adjacent is None: for neighbor in from_pos.all_neighbors():
if neighbor in elves:
found = True
break
if not found:
continue continue
next_direction = start next_direction = start
while True: found = True
if next_direction in adjacent: while Ground.has_neighbor(elves, from_pos, next_direction):
next_direction = next_direction.next() next_direction = next_direction.next()
else: if next_direction == start:
to_pos = next_direction.walk(from_pos) found = False
if to_pos not in proposals:
proposals[to_pos] = from_pos
else:
del proposals[to_pos]
break break
if found:
to_pos = next_direction.walk(from_pos)
old_from = proposals.pop(to_pos, None)
if old_from is None:
proposals[to_pos] = from_pos
else:
touched.add(from_pos)
touched.add(old_from)
if not proposals: if not proposals:
self.map = set(elves) self.map = set(elves)
return round return round
first = True
for to_pos, from_pos in proposals.items(): for to_pos, from_pos in proposals.items():
del elves[from_pos]
elves[to_pos] = round elves[to_pos] = round
del elves[from_pos]
if first: for neighbor in Ground.pair_neighbors(from_pos, to_pos):
max_position = Position.component_max(to_pos, from_pos) if neighbor in elves:
min_position = Position.component_min(to_pos, from_pos) elves[neighbor] = round
first = False
else: for touched_pos in touched:
max_position = Position.component_max(max_position, to_pos, from_pos) elves[touched_pos] = round
min_position = Position.component_min(min_position, to_pos, from_pos)
self.map = set(elves) self.map = set(elves)
return None return None