···11+import fileinput
22+from collections import defaultdict
33+44+from utils import Point, N, NE, E, SE, S, SW, W, NW, min_max_xy
55+66+77+BOARD = {}
88+99+for y, line in enumerate(fileinput.input()):
1010+ for x, c in enumerate(line.strip()):
1111+ BOARD[Point(x, y)] = c
1212+1313+# N and S are switched from the problem statement because of
1414+# the convention of using +y -> N in my utils, which does have
1515+# an effect on the interaction between elves in this problem.
1616+CHECKS = [
1717+ [[S, SE, SW], S],
1818+ [[N, NE, NW], N],
1919+ [[W, NW, SW], W],
2020+ [[E, NE, SE], E],
2121+]
2222+2323+for i in range(100000):
2424+ proposal = defaultdict(list)
2525+ for p, c in BOARD.items():
2626+ if c == '#':
2727+ # If elf has no neighbours in all 8 directions, don't consider moving.
2828+ if not any(BOARD.get(n) == '#' for n in p.neighbours_8()):
2929+ continue
3030+3131+ # Add elf's proposed location to `proposal`.
3232+ for d in range(4):
3333+ checks, move = CHECKS[(i + d) % 4]
3434+ if any(BOARD.get(p + c, ' ') == '#' for c in checks):
3535+ continue
3636+3737+ proposal[p + move].append(p)
3838+ break
3939+4040+ for new_loc in proposal:
4141+ elves = proposal[new_loc]
4242+ # If only one elf wants to move there, they can move (and vacate their old spot).
4343+ if len(elves) == 1:
4444+ BOARD[elves[0]] = '.'
4545+ BOARD[new_loc] = '#'
4646+4747+ # Answer to part 1, compute number of empty squares within the bounds.
4848+ if i == 9:
4949+ elves = [p for p in BOARD if BOARD[p] == '#']
5050+ min_x, max_x, min_y, max_y = min_max_xy(elves)
5151+ blanks = 0
5252+ for y in range(min_y, max_y + 1):
5353+ for x in range(min_x, max_x + 1):
5454+ p = Point(x, y)
5555+ if BOARD.get(p) != '#':
5656+ blanks += 1
5757+5858+ print("Part 1:", blanks)
5959+6060+ # Empty proposal list -> no elves moved, the answer to part 2.
6161+ if len(proposal) == 0:
6262+ print("Part 2:", i + 1)
6363+ break