|
| 1 | +import extra |
| 2 | +import gleam/bool |
| 3 | +import gleam/int |
| 4 | +import gleam/io |
| 5 | +import gleam/list |
| 6 | +import gleam/string |
| 7 | +import grid.{type Dir, type Grid, type XY} |
| 8 | + |
| 9 | +pub type Input { |
| 10 | + Input(grid: Grid(Entity), robot: XY, moves: List(Dir)) |
| 11 | +} |
| 12 | + |
| 13 | +pub type Entity { |
| 14 | + Wall |
| 15 | + Box |
| 16 | +} |
| 17 | + |
| 18 | +pub fn parse(input: String) -> Input { |
| 19 | + case string.split(input, "\n\n") { |
| 20 | + [map, moves] -> { |
| 21 | + let char_grid = grid.from_string(map) |
| 22 | + Input( |
| 23 | + grid: char_grid |
| 24 | + |> grid.filter_map(fn(_, c) { |
| 25 | + case c { |
| 26 | + "#" -> Ok(Wall) |
| 27 | + "O" -> Ok(Box) |
| 28 | + _ -> Error(Nil) |
| 29 | + } |
| 30 | + }), |
| 31 | + robot: case grid.find_exact(char_grid, "@") { |
| 32 | + Ok(xy) -> xy |
| 33 | + Error(Nil) -> panic as "Bad input? #3" |
| 34 | + }, |
| 35 | + moves: moves |
| 36 | + |> string.replace(each: "\n", with: "") |
| 37 | + |> string.to_graphemes |
| 38 | + |> list.map(fn(c) { |
| 39 | + case c { |
| 40 | + "<" -> grid.Left |
| 41 | + ">" -> grid.Right |
| 42 | + "^" -> grid.Top |
| 43 | + "v" -> grid.Bottom |
| 44 | + _ -> panic as "Bad input? #2" |
| 45 | + } |
| 46 | + }), |
| 47 | + ) |
| 48 | + } |
| 49 | + _ -> panic as "Bad input? #1" |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +pub fn pt_1(input: Input) { |
| 54 | + input.moves |
| 55 | + |> list.fold(from: #(input.grid, input.robot), with: step) |
| 56 | + |> fn(state) { state.0 } |
| 57 | + |> grid.filter(fn(_, e) { e == Box }) |
| 58 | + |> grid.keys |
| 59 | + |> list.map(gps) |
| 60 | + |> int.sum |
| 61 | +} |
| 62 | + |
| 63 | +pub fn pt_2(input: Input) { |
| 64 | + let wider_grid = widen(input.grid) |
| 65 | + let wider_robot = grid.xy_mul(input.robot, #(2, 1)) |
| 66 | + let #(final_grid, final_robot) = |
| 67 | + input.moves |
| 68 | + |> list.fold(from: #(wider_grid, wider_robot), with: wider_step) |
| 69 | + //io.println_error("============== FINAL =============") |
| 70 | + //show2(final_grid, final_robot) |
| 71 | + final_grid |
| 72 | + |> grid.filter(fn(_, e) { e == BoxLeft }) |
| 73 | + |> grid.keys |
| 74 | + |> list.map(gps) |
| 75 | + |> int.sum |
| 76 | +} |
| 77 | + |
| 78 | +fn gps(xy: XY) -> Int { |
| 79 | + 100 * xy.1 + xy.0 |
| 80 | +} |
| 81 | + |
| 82 | +// Pt 1: |
| 83 | +// when next to robot: |
| 84 | +// nothing -> just move the robot |
| 85 | +// wall -> do nothing |
| 86 | +// box -> find the next item |
| 87 | +// nothing -> move the first box to the free space, |
| 88 | +// move the robot to the box position |
| 89 | +// wall -> do nothing |
| 90 | +// box -> recurse (but remember the first box XY) |
| 91 | + |
| 92 | +fn step(state: #(Grid(Entity), XY), dir: Dir) -> #(Grid(Entity), XY) { |
| 93 | + let #(grid, robot) = state |
| 94 | + let next_xy = grid.step(robot, dir, 1) |
| 95 | + case grid.get(grid, next_xy) { |
| 96 | + Error(Nil) -> #(grid, next_xy) |
| 97 | + Ok(Wall) -> state |
| 98 | + Ok(Box) -> step_find_nonbox(grid, robot, next_xy, next_xy, dir) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +fn step_find_nonbox( |
| 103 | + grid: Grid(Entity), |
| 104 | + robot: XY, |
| 105 | + first_box_xy: XY, |
| 106 | + current_xy: XY, |
| 107 | + dir: Dir, |
| 108 | +) -> #(Grid(Entity), XY) { |
| 109 | + let next_xy = grid.step(current_xy, dir, 1) |
| 110 | + case grid.get(grid, next_xy) { |
| 111 | + Error(Nil) -> #( |
| 112 | + grid.move(grid, from: first_box_xy, to: next_xy), |
| 113 | + first_box_xy, |
| 114 | + ) |
| 115 | + Ok(Wall) -> #(grid, robot) |
| 116 | + Ok(Box) -> step_find_nonbox(grid, robot, first_box_xy, next_xy, dir) |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +pub type Entity2 { |
| 121 | + Wall2 |
| 122 | + BoxLeft |
| 123 | + BoxRight |
| 124 | +} |
| 125 | + |
| 126 | +fn widen(grid: Grid(Entity)) -> Grid(Entity2) { |
| 127 | + grid |
| 128 | + |> grid.to_list |
| 129 | + |> list.flat_map(fn(kv) { |
| 130 | + let #(xy, e) = kv |
| 131 | + let xy1 = grid.xy_mul(xy, #(2, 1)) |
| 132 | + let xy2 = grid.xy_add(xy1, #(1, 0)) |
| 133 | + let #(first, second) = case e { |
| 134 | + Wall -> #(Wall2, Wall2) |
| 135 | + Box -> #(BoxLeft, BoxRight) |
| 136 | + } |
| 137 | + [#(xy1, first), #(xy2, second)] |
| 138 | + }) |
| 139 | + |> grid.from_list |
| 140 | +} |
| 141 | + |
| 142 | +fn wider_step(state: #(Grid(Entity2), XY), dir: Dir) -> #(Grid(Entity2), XY) { |
| 143 | + let #(grid, robot) = state |
| 144 | + //show2(grid, robot) |
| 145 | + let next_xy = grid.step(robot, dir, 1) |
| 146 | + case grid.get(grid, next_xy) { |
| 147 | + Error(Nil) -> #(grid, next_xy) |
| 148 | + Ok(Wall2) -> state |
| 149 | + Ok(e) -> { |
| 150 | + let left = left_in_dir(e, next_xy, dir) |
| 151 | + wider_step_find_nonbox(grid, robot, [left], [left], dir) |
| 152 | + } |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +fn right_for_left(left: XY) { |
| 157 | + grid.step(left, grid.Right, 1) |
| 158 | +} |
| 159 | + |
| 160 | +fn left_for_right(right: XY) { |
| 161 | + grid.step(right, grid.Left, 1) |
| 162 | +} |
| 163 | + |
| 164 | +fn wider_step_find_nonbox( |
| 165 | + grid: Grid(Entity2), |
| 166 | + robot: XY, |
| 167 | + moved_lefts: List(XY), |
| 168 | + frontier_lefts: List(XY), |
| 169 | + dir: Dir, |
| 170 | +) -> #(Grid(Entity2), XY) { |
| 171 | + case list.is_empty(frontier_lefts) { |
| 172 | + True -> { |
| 173 | + let box_parts = |
| 174 | + moved_lefts |
| 175 | + |> list.flat_map(fn(left) { |
| 176 | + [#(left, BoxLeft), #(right_for_left(left), BoxRight)] |
| 177 | + }) |
| 178 | + // can move |
| 179 | + #( |
| 180 | + grid |
| 181 | + |> grid.delete_all(box_parts |> list.map(fn(kv) { kv.0 })) |
| 182 | + |> grid.insert_all( |
| 183 | + box_parts |
| 184 | + |> list.map(fn(kv) { #(grid.step(kv.0, dir, 1), kv.1) }), |
| 185 | + ), |
| 186 | + grid.step(robot, dir, 1), |
| 187 | + ) |
| 188 | + } |
| 189 | + False -> { |
| 190 | + let frontier_lefts_next: List(#(XY, Entity2)) = |
| 191 | + frontier_lefts |
| 192 | + |> list.flat_map(fn(left) { |
| 193 | + case grid.get(grid, left) { |
| 194 | + Ok(Wall2) -> panic as "wall as a frontier?!" |
| 195 | + Ok(BoxLeft) -> { |
| 196 | + let right = right_for_left(left) |
| 197 | + let frontmost: List(XY) = case dir { |
| 198 | + grid.Left -> [left] |
| 199 | + grid.Right -> [right] |
| 200 | + grid.Top -> [left, right] |
| 201 | + grid.Bottom -> [left, right] |
| 202 | + _ -> panic as "Diagonals?? In my program??" |
| 203 | + } |
| 204 | + let frontmost_touches: List(XY) = |
| 205 | + frontmost |
| 206 | + |> list.map(grid.step(_, dir, 1)) |
| 207 | + case |
| 208 | + dir, |
| 209 | + frontmost_touches, |
| 210 | + list.map(frontmost_touches, grid.get(grid, _)) |
| 211 | + { |
| 212 | + // Left |
| 213 | + grid.Left, [l], [Ok(Wall2)] -> [#(l, Wall2)] |
| 214 | + grid.Left, [_], [Ok(BoxLeft)] -> panic as "split box #1" |
| 215 | + grid.Left, [l], [Ok(BoxRight)] -> [ |
| 216 | + #(left_for_right(l), BoxLeft), |
| 217 | + ] |
| 218 | + grid.Left, [_], [Error(Nil)] -> [] |
| 219 | + |
| 220 | + // Right |
| 221 | + grid.Right, [r], [Ok(Wall2)] -> [#(r, Wall2)] |
| 222 | + grid.Right, [r], [Ok(BoxLeft)] -> [#(r, BoxLeft)] |
| 223 | + grid.Right, [_], [Ok(BoxRight)] -> panic as "split box #2" |
| 224 | + grid.Right, [_], [Error(Nil)] -> [] |
| 225 | + |
| 226 | + // Top |
| 227 | + grid.Top, [l, r], [Ok(Wall2), Ok(Wall2)] -> [ |
| 228 | + #(l, Wall2), |
| 229 | + #(r, Wall2), |
| 230 | + ] |
| 231 | + grid.Top, [l, _], [Ok(Wall2), _] -> [#(l, Wall2)] |
| 232 | + grid.Top, [_, r], [_, Ok(Wall2)] -> [#(r, Wall2)] |
| 233 | + grid.Top, [l, r], [Ok(BoxRight), Ok(BoxLeft)] -> [ |
| 234 | + #(left_for_right(l), BoxLeft), |
| 235 | + #(r, BoxLeft), |
| 236 | + ] |
| 237 | + grid.Top, [l, _], [Ok(BoxLeft), Ok(BoxRight)] -> [#(l, BoxLeft)] |
| 238 | + grid.Top, [_, _], [Error(Nil), Error(Nil)] -> [] |
| 239 | + grid.Top, [_, r], [Error(Nil), Ok(BoxLeft)] -> [#(r, BoxLeft)] |
| 240 | + grid.Top, [l, _], [Ok(BoxRight), Error(Nil)] -> [ |
| 241 | + #(left_for_right(l), BoxLeft), |
| 242 | + ] |
| 243 | + |
| 244 | + // Bottom |
| 245 | + grid.Bottom, [l, r], [Ok(Wall2), Ok(Wall2)] -> [ |
| 246 | + #(l, Wall2), |
| 247 | + #(r, Wall2), |
| 248 | + ] |
| 249 | + grid.Bottom, [l, _], [Ok(Wall2), _] -> [#(l, Wall2)] |
| 250 | + grid.Bottom, [_, r], [_, Ok(Wall2)] -> [#(r, Wall2)] |
| 251 | + grid.Bottom, [l, r], [Ok(BoxRight), Ok(BoxLeft)] -> [ |
| 252 | + #(left_for_right(l), BoxLeft), |
| 253 | + #(r, BoxLeft), |
| 254 | + ] |
| 255 | + grid.Bottom, [l, _], [Ok(BoxLeft), Ok(BoxRight)] -> [ |
| 256 | + #(l, BoxLeft), |
| 257 | + ] |
| 258 | + grid.Bottom, [_, _], [Error(Nil), Error(Nil)] -> [] |
| 259 | + grid.Bottom, [_, r], [Error(Nil), Ok(BoxLeft)] -> [ |
| 260 | + #(r, BoxLeft), |
| 261 | + ] |
| 262 | + grid.Bottom, [l, _], [Ok(BoxRight), Error(Nil)] -> [ |
| 263 | + #(left_for_right(l), BoxLeft), |
| 264 | + ] |
| 265 | + |
| 266 | + // Unfinished cases |
| 267 | + _, _, _ -> { |
| 268 | + io.debug(#( |
| 269 | + dir, |
| 270 | + frontmost_touches, |
| 271 | + list.map(frontmost_touches, grid.get(grid, _)), |
| 272 | + )) |
| 273 | + todo |
| 274 | + } |
| 275 | + } |
| 276 | + } |
| 277 | + Ok(BoxRight) -> panic as "right as a frontier?!" |
| 278 | + Error(Nil) -> [] |
| 279 | + } |
| 280 | + }) |
| 281 | + use <- bool.guard( |
| 282 | + when: list.any(frontier_lefts_next, fn(kv) { kv.1 == Wall2 }), |
| 283 | + return: #(grid, robot), |
| 284 | + ) |
| 285 | + let frontier_lefts_next_xys = |
| 286 | + frontier_lefts_next |> list.map(fn(kv) { kv.0 }) |
| 287 | + wider_step_find_nonbox( |
| 288 | + grid, |
| 289 | + robot, |
| 290 | + list.append(frontier_lefts_next_xys, moved_lefts), |
| 291 | + frontier_lefts_next_xys, |
| 292 | + dir, |
| 293 | + ) |
| 294 | + } |
| 295 | + } |
| 296 | +} |
| 297 | + |
| 298 | +fn left_in_dir(e: Entity2, xy: XY, dir: Dir) -> XY { |
| 299 | + case dir, e { |
| 300 | + _, Wall2 -> panic as "We shouldn't call this with Wall2" |
| 301 | + grid.Left, BoxLeft -> panic as "huh? #1" |
| 302 | + grid.Left, BoxRight -> left_for_right(xy) |
| 303 | + grid.Right, BoxLeft -> xy |
| 304 | + grid.Right, BoxRight -> panic as "huh? #2" |
| 305 | + grid.Top, BoxLeft -> xy |
| 306 | + grid.Top, BoxRight -> left_for_right(xy) |
| 307 | + grid.Bottom, BoxLeft -> xy |
| 308 | + grid.Bottom, BoxRight -> left_for_right(xy) |
| 309 | + _, _ -> panic as "diagonals? huh?" |
| 310 | + } |
| 311 | +} |
| 312 | + |
| 313 | +fn show2(grid: Grid(Entity2), robot: XY) -> Nil { |
| 314 | + let without_robot = |
| 315 | + grid.to_string( |
| 316 | + grid, |
| 317 | + fn(e) { |
| 318 | + case e { |
| 319 | + Wall2 -> #("#", Error(Nil)) |
| 320 | + BoxLeft -> #("[", Error(Nil)) |
| 321 | + BoxRight -> #("]", Error(Nil)) |
| 322 | + } |
| 323 | + }, |
| 324 | + ".", |
| 325 | + ) |
| 326 | + let with_robot = |
| 327 | + without_robot |
| 328 | + |> string.split("\n") |
| 329 | + |> list.index_map(fn(s, i) { |
| 330 | + case i == robot.1 { |
| 331 | + True -> s |> extra.string_set(robot.0, "@") |
| 332 | + False -> s |
| 333 | + } |
| 334 | + }) |
| 335 | + |> string.join("\n") |
| 336 | + io.println_error(" 0123456789") |
| 337 | + io.println_error( |
| 338 | + with_robot |
| 339 | + |> extra.add_line_numbers, |
| 340 | + ) |
| 341 | +} |
0 commit comments