Skip to content

Commit dec96d3

Browse files
committed
2024-15 * ** Gleam
1 parent 47b6657 commit dec96d3

File tree

3 files changed

+394
-5
lines changed

3 files changed

+394
-5
lines changed

src/aoc_2024/day_15.gleam

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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+
}

src/extra.gleam

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,30 @@ pub fn do_if(subject: a, pred pred: Bool, fun fun: fn(a) -> a) -> a {
8181

8282
@external(erlang, "timer", "sleep")
8383
pub fn sleep(ms: Int) -> Nil
84+
85+
pub fn add_line_numbers(str: String) -> String {
86+
str
87+
|> string.split("\n")
88+
|> list.index_map(fn(s, i) {
89+
{
90+
int.to_string(i)
91+
|> string.pad_start(3, with: " ")
92+
}
93+
<> " "
94+
<> s
95+
})
96+
|> string.join("\n")
97+
}
98+
99+
pub fn string_set(str: String, at: Int, value: String) -> String {
100+
str
101+
|> string.to_graphemes
102+
// PERF: fold_until
103+
|> list.index_map(fn(s, i) {
104+
case i == at {
105+
True -> value
106+
False -> s
107+
}
108+
})
109+
|> string.join("")
110+
}

0 commit comments

Comments
 (0)