1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/* Copyright 2022-2023 Mario Finelli
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Advent of Code 2022 Day 14: <https://adventofcode.com/2022/day/14>
//!
//! Today's puzzle is not too challenging. Essentially, we need to maintain a
//! list of spaces that are occupied by rocks or sand and then simulate pieces
//! of sand that fall down until they can't anymore. As each piece of sand
//! comes to rest it gets added to our list of occupied spaces. In part one
//! when the sand would fall forever (it is below the lowest rock that we
//! recorded from the puzzle input) we're done. In part two there is a floor
//! that extends forever in the "X" direction two spaces below the lowest
//! recorded rock and so when there is no more space for the sand to fall as
//! it's reached the top we're done.

use std::collections::{BinaryHeap, HashSet};

/// The solution for the day fourteen challenge.
///
/// Given the input as a string and an integer to determine if we should run
/// part `1`, or part `2` of the puzzle, we start by building a
/// [`std::collections::HashSet`] of the spaces that are currently occupied by
/// rocks based on parsing the puzzle input. The set contains the coordinates
/// that are currently occupied (right now by rocks, but also by sand once we
/// start the simulation). While doing this we also need to record the lowest
/// "Y" coordinate which we can treat as the floor. We do this using our usual
/// [`std::collections::BinaryHeap`]. If we're in part two we take this floor
/// and add two more spaces below as the prompt tells us there's an actual
/// floor two spaces below the lowest rock.
///
/// Now we can start the simulation. We let sand fall until we've met the
/// condition to stop letting it fall: in part one when the sand would fall
/// forever, and in part two once it reaches the starting point of the sand
/// (coordinate `500`,`0`). For each granule of sand we start at the entry
/// point. We then check the coordinate below (this is slightly tricky as the
/// "Y" coordinates actually count up) to see if there is a space either
/// directly below or below to the left or right where the sand can move. If
/// it _can't_ then we stop, add the sand into our occupied spaces set and move
/// on to the next sand granule. If we're in part two and the sand didn't move
/// from its starting position then we're done. Otherwise on the next (and on
/// every) iteration of the sands movement we check to see if the sand is
/// _below_ the floor which means that it would keep falling forever, so we're
/// done. All that remains is to return the number of units of sand that fell.
///
/// # Example
/// ```rust
/// # use aoc::y22d14::y22d14;
/// // probably read this from the input file...
/// let input = "499,4 -> 499,5 -> 496,5\n503,4 -> 502,4 -> 502,7 -> 494,7";
/// assert_eq!(y22d14(input, 1), 19);
/// assert_eq!(y22d14(input, 2), 54);
/// ```
pub fn y22d14(input: &str, part: u32) -> u32 {
    let lines: Vec<_> = input.lines().collect();
    let mut occupied = HashSet::new();
    let mut lowest = BinaryHeap::new();

    for line in lines {
        let parts: Vec<_> = line.split_whitespace().collect();

        let coordinates: Vec<_> = parts[0].split(',').collect();
        let mut start_x: usize = coordinates[0].parse().unwrap();
        let mut start_y: usize = coordinates[1].parse().unwrap();
        lowest.push(start_y);

        for part in parts.iter().skip(1) {
            if part == &"->" {
                continue;
            }

            let coordinates: Vec<_> = part.split(',').collect();
            let end_x: usize = coordinates[0].parse().unwrap();
            let end_y: usize = coordinates[1].parse().unwrap();
            lowest.push(end_y);

            let mut sorted_x: [usize; 2] = [start_x, end_x];
            let mut sorted_y: [usize; 2] = [start_y, end_y];
            sorted_x.sort();
            sorted_y.sort();

            for x in sorted_x[0]..sorted_x[1] + 1 {
                for y in sorted_y[0]..sorted_y[1] + 1 {
                    occupied.insert((x, y));
                }
            }

            start_x = end_x;
            start_y = end_y;
        }
    }

    let floor = if part == 1 {
        lowest.pop().unwrap()
    } else {
        lowest.pop().unwrap() + 2
    };

    let mut stop = false;
    let mut total = 0;
    while !stop {
        let mut sand: (usize, usize) = (500, 0);

        loop {
            let (x, y) = sand;

            if y > floor {
                occupied.insert((x, floor));
                occupied.insert((x - 1, floor));
                occupied.insert((x + 1, floor));
                total -= 1;
                break;
            }

            if part == 1
                && !occupied.iter().any(|o| {
                    let (ox, oy) = o;
                    ox == &x && oy > &y
                })
            {
                // sand falls forever; we're done
                stop = true;
                break;
            }

            if !occupied.contains(&(x, y + 1)) {
                sand = (x, y + 1);
            } else if !occupied.contains(&(x - 1, y + 1)) {
                sand = (x - 1, y + 1);
            } else if !occupied.contains(&(x + 1, y + 1)) {
                sand = (x + 1, y + 1);
            } else {
                break;
            }
        }

        if part == 2 && sand == (500, 0) {
            // the sand didn't move; we're done
            break;
        }

        occupied.insert(sand);
        total += 1
    }

    if part == 1 {
        total - 1
    } else {
        total + 1
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn it_works() {
        let input = concat!(
            "498,4 -> 498,6 -> 496,6\n",
            "503,4 -> 502,4 -> 502,9 -> 494,9\n"
        );

        assert_eq!(y22d14(input, 1), 24);
        assert_eq!(y22d14(input, 2), 93);
    }

    #[test]
    fn the_solution() {
        let contents = fs::read_to_string("input/2022/day14.txt").unwrap();

        assert_eq!(y22d14(&contents, 1), 698);
        assert_eq!(y22d14(&contents, 2), 28594);
    }
}