import matrix import shared from pprint import pprint from dataclasses import dataclass from typing import Set, Tuple from functools import cached_property from mpl_toolkits.mplot3d import Axes3D import numpy as np import matplotlib.pyplot as plt @dataclass class Cube: x: int y: int z: int neighbor_coords: Set[Tuple[int,int,int]] = None @cached_property def xyz(self): return (self.x,self.y,self.z) def set_neighbor_coords(self): self.neighbor_coords = get_neighbors(self.x,self.y,self.z) def get_neighbors(x,y,z): # Generate the six neighbor_coords # Look at a die 6 to the left, 2 on top, one on right offsets = ( (1,0,0), # 1 (0,0,1), # 2 (0,-1,0),# 3 (0,1,0), # 4 (0,0,-1),# 5 (-1,0,0) # 6 ) return set([(x+o[0], y+o[1], z+o[2]) for o in offsets]) def get_flat_neighbors(x,y,z): # Generate the six neighbor_coords # Look at a die 6 to the left, 2 on top, one on right # ignore top and bottom offsets = ( (1,0,0), # 1 #(0,0,1), # 2 (0,-1,0),# 3 (0,1,0), # 4 #(0,0,-1),# 5 (-1,0,0) # 6 ) return set([(x+o[0], y+o[1], z+o[2]) for o in offsets]) def get_flat_neighbors_from(x,y,z, f): return get_flat_neighbors(x,y,z).intersection(f) # @shared.profile def part1(rows): cubes = {} for idx, (x,y,z) in enumerate(rows): cubes[(x,y,z)] = {'n':get_neighbors(x,y,z), 'not_me': tuple(map(tuple,rows[:idx] + rows[idx+1:]))} potential = len(cubes) * 6 for coords, cube in cubes.items(): for other in cube['not_me']: if other in cube['n']: potential -= 1 print(potential) def surface_area(rows): cubes = {} maxX,maxY,maxZ = 0,0,0 for idx, (x,y,z) in enumerate(rows): cubes[(x,y,z)] = {'n':get_neighbors(x,y,z), 'not_me': set(map(tuple,rows[:idx] + rows[idx+1:]))} maxX = max(maxX,x) maxY = max(maxY,y) maxZ = max(maxZ,z) _cubes = frozenset(cubes.keys()) potential = len(cubes) * 6 for coords, cube in cubes.items(): for other in cube['not_me']: if other in cube['n']: potential -= 1 return potential, maxX,maxY,maxZ, cubes, _cubes def part2(rows): potential, maxX, maxY,maxZ,cubes,_cubes = surface_area(rows) air = set() for x in range(0,maxX+1): for y in range(0,maxY+1): for z in range(0,maxZ+1): if (x,y,z) not in _cubes: air.add((x,y,z)) air -= _cubes # Remove all lava from air all_air_count = len(air) all_lava_count = len(_cubes) possible_count = (maxX+1)*(maxY+1)*(maxZ+1) print("all_air: ", all_air_count) print("all_lava:", all_lava_count) print("possible:", possible_count) print(maxZ,maxY,maxX) print() #shared.render_cubes(maxX+2,maxY+2,maxZ+1, [x for x in _cubes]) air_nx = {} for a in air: neighbors = get_neighbors(*a) air_nx[a] = { 'lava':neighbors & _cubes or None, 'air': neighbors & air or None } # loop row by row inside = [] for z in range(0,maxZ+2): seen = set() seen.add((0,0,z)) mx = matrix.matrix_of_size(maxX+2,maxY+2) for x,y in matrix.spiral_generator(maxX+2, maxY+2): xyz = x,y,z mx[y][x] = "#" if xyz in _cubes: continue ns = get_flat_neighbors(x,y,z) for neigh in ns: if neigh in seen: seen.add(xyz) this_level = set(x for x in air_in_row(air, z)) for (x,y,z) in seen: mx[y][x] = 0 for x,y,z in this_level: if (x,y,z) not in seen: mx[y][x] = "o" print(matrix.ppmx(mx, pad=False,space=False)) print() #print(len(this_level), len(seen)) #inside.extend([x for x in this_level if x not in seen]) #print(seen) #shared.render_cubes(maxX+2,maxY+2,maxZ+1, [x for x in seen]) #shared.render_cubes(maxX+2,maxY+2,maxZ+1, [x for x in this_level-seen]) ##print() ##print(inside) ##print() #actually_inside = [] #for i in inside: # ns = get_neighbors(*i) # # check for surrounded 100% by rock # rock_count = 0 # for n in ns: # if n in _cubes: # rock_count +=1 # if rock_count == 6: # print("in rock") # actually_inside.append(i) # continue # #check for surrounded 100% by air # air_count = 0 # for n in ns: # if n in air: # air_count +=1 # print("in air") # break # actually_inside.append(i) ##print(actually_inside) #tot, _, _, _, _, _ = surface_area(actually_inside) #print(tot) #print(potential - tot) def air_in_row(air, z): x = [] for a in air: if a[2] == z: x.append(a) return x def main(): rows = [list(map(int,row.split(","))) for row in shared.load_rows(18)] with shared.elapsed_timer() as elapsed: part1(rows) print("🕒", elapsed()) #rows = [map(int,row.split(",")) for row in shared.load_rows(18)] with shared.elapsed_timer() as elapsed: part2(rows) print("🕒", elapsed()) if __name__ == "__main__": main()