Coverage for src\aoc2022\days\day14.py: 100%

86 statements  

« prev     ^ index     » next       coverage.py v7.0.0, created at 2022-12-23 05:19 +0000

1from typing import Tuple 

2 

3import numpy as np 

4 

5from src.aoc2022 import utils 

6 

7 

8def parse_data_to_array( 

9 raw_data: str, starting_point: Tuple[int, int] 

10) -> Tuple[np.ndarray, Tuple[int, int]]: 

11 """ 

12 Parse raw data into a grid of air/stone cells 

13 

14 :param raw_data: Raw data to parse into packets 

15 :type raw_data: str 

16 

17 :param starting_point: Starting point from where sand is dropped 

18 :type starting_point: Tuple[int, int] 

19 

20 :return: the generated map and updated starting point 

21 :rtype: Tuple[np.ndarray, Tuple[int, int]] 

22 """ 

23 min_x_value = starting_point[0] 

24 min_y_value = starting_point[1] 

25 max_x_value = starting_point[0] 

26 max_y_value = starting_point[1] 

27 paths = [] 

28 

29 for line in raw_data.split("\n"): 

30 coords = line.split(" -> ") 

31 path = [] 

32 for i in range(len(coords)): 

33 x, y = coords[i].split(",") 

34 

35 x_val = int(x) 

36 y_val = int(y) 

37 

38 min_x_value = min(x_val, min_x_value) 

39 max_x_value = max(x_val, max_x_value) 

40 min_y_value = min(y_val, min_y_value) 

41 max_y_value = max(y_val, max_y_value) 

42 

43 path.append((x_val, y_val)) 

44 

45 paths.append(path) 

46 

47 shape = (max_x_value - min_x_value + 1, max_y_value - min_y_value + 1) 

48 map_ = np.zeros(shape) 

49 

50 normalized_starting_point = ( 

51 starting_point[0] - min_x_value, 

52 starting_point[1] - min_y_value, 

53 ) 

54 

55 for path in paths: 

56 for i in range(len(path) - 1): 

57 x1, y1 = path[i] 

58 x2, y2 = path[i + 1] 

59 

60 x1 = min(path[i][0], path[i + 1][0]) 

61 y1 = min(path[i][1], path[i + 1][1]) 

62 x2 = max(path[i][0], path[i + 1][0]) 

63 y2 = max(path[i][1], path[i + 1][1]) 

64 

65 x1 = x1 - min_x_value 

66 x2 = x2 - min_x_value 

67 

68 y1 = y1 - min_y_value 

69 y2 = y2 - min_y_value 

70 

71 if x1 != x2: 

72 x2 += 1 

73 map_[x1:x2, y1] = 1 

74 elif y1 != y2: 

75 y2 += 1 

76 map_[x1, y1:y2] = 1 

77 

78 return map_, normalized_starting_point 

79 

80 

81def add_floor_to_map( 

82 map_: np.ndarray, starting_point: Tuple[int, int] 

83) -> Tuple[np.ndarray, Tuple[int, int]]: 

84 """ 

85 Add a floor underneath the given map 

86 

87 :param map_: The map to which the extra floor is added 

88 :type map_: np.ndarray 

89 

90 :param starting_point: Starting point from where sand is dropped 

91 :type starting_point: Tuple[int, int] 

92 

93 :return: the updated map and starting point 

94 :rtype: Tuple[np.ndarray, Tuple[int, int]] 

95 """ 

96 # want starting point to be in middle, need to find size of new map 

97 height = map_.shape[1] + 2 

98 width = 2 * height + map_.shape[0] 

99 

100 copy_left = height 

101 copy_right = height + map_.shape[0] 

102 copy_top = 0 

103 copy_bot = map_.shape[1] 

104 

105 new_map = np.zeros((width, height)) 

106 new_map[copy_left:copy_right, copy_top:copy_bot] = map_ 

107 new_map[:, -1] = 1 

108 

109 new_starting_point = (starting_point[0] + height, 0) 

110 

111 return new_map, new_starting_point 

112 

113 

114def simulate_grain_of_sand( 

115 map_: np.ndarray, starting_point: Tuple[int, int] 

116) -> Tuple[int, int]: 

117 """ 

118 Simulate a grain of sand falling through the map 

119 

120 :param map_: The map through which the grain of sand falls 

121 :type map_: np.ndarray 

122 

123 :param starting_point: Starting point from where sand is dropped 

124 :type starting_point: Tuple[int, int] 

125 

126 :return: The updated cell that the grain of sand falls onto 

127 :rtype: Tuple[int, int] 

128 """ 

129 

130 # TODO: this can be optimized with dynamic programming 

131 x, y = starting_point 

132 max_x, max_y = map_.shape 

133 while (x >= 0) and (x < max_x) and (y >= 0) and (y < max_y - 1): 

134 if map_[x, y] != 0: 

135 return (-1, -1) 

136 if map_[x, y + 1] == 0: 

137 y += 1 

138 else: 

139 if map_[x - 1, y + 1] == 0: 

140 x -= 1 

141 y += 1 

142 elif map_[x + 1, y + 1] == 0: 

143 x += 1 

144 y += 1 

145 else: 

146 return (x, y) 

147 return (-1, -1) 

148 

149 

150def render_map(map_: np.ndarray, start_point: Tuple[int, int]) -> str: 

151 """ 

152 Render the map for visualization 

153 

154 :param map_: The map through which the grain of sand falls 

155 :type map_: np.ndarray 

156 

157 :param starting_point: Starting point from where sand is dropped 

158 :type starting_point: Tuple[int, int] 

159 

160 :return: A rendered representation of the map 

161 :rtype: str 

162 """ 

163 render = np.full(map_.shape, "") 

164 render[map_ == 0] = "." 

165 render[map_ == 1] = "#" 

166 render[map_ == 2] = "o" 

167 render[start_point] = "S" 

168 

169 return "\n".join(["".join(render[:, i]) for i in range(render.shape[1])]) 

170 

171 

172def simulate_sand_falling( 

173 map_: np.ndarray, starting_point: Tuple[int, int] 

174) -> np.ndarray: 

175 """ 

176 Repeatedly drop grains of sand til convergence 

177 

178 :param map_: The map through which the grain of sand falls 

179 :type map_: np.ndarray 

180 

181 :param starting_point: Starting point from where sand is dropped 

182 :type starting_point: Tuple[int, int] 

183 

184 :return: converged map state after grain of sand fall off edge of map 

185 :rtype: np.ndarray 

186 """ 

187 map_ = map_.copy() 

188 updated_point = simulate_grain_of_sand(map_, starting_point) 

189 

190 while updated_point != (-1, -1): 

191 map_[updated_point] = 2 

192 updated_point = simulate_grain_of_sand(map_, starting_point) 

193 return map_ 

194 

195 

196if __name__ == "__main__": # pragma: no cover 

197 

198 starting_point = (500, 0) 

199 verbose = False 

200 

201 raw_data = utils.get_raw_data("./src/aoc2022/data/day14.txt") 

202 map_, new_start = parse_data_to_array(raw_data, starting_point) 

203 

204 sand_filled_map = simulate_sand_falling(map_, new_start) 

205 

206 print(f"{sand_filled_map[sand_filled_map == 2].shape[0]} grains fell") 

207 

208 if verbose: 

209 print("Before :") 

210 print(render_map(map_, new_start)) 

211 print("After :") 

212 print(render_map(sand_filled_map, new_start)) 

213 

214 map_with_floor, start_with_floor = add_floor_to_map(map_, new_start) 

215 sand_filled_map = simulate_sand_falling(map_with_floor, start_with_floor) 

216 

217 print(f"{sand_filled_map[sand_filled_map == 2].shape[0]} grains fell") 

218 

219 if verbose: 

220 print("Before :") 

221 print(render_map(map_with_floor, start_with_floor)) 

222 print("After :") 

223 print(render_map(sand_filled_map, start_with_floor))