diff --git a/src/day_17/mod.rs b/src/day_17/mod.rs new file mode 100644 index 0000000..0d196b8 --- /dev/null +++ b/src/day_17/mod.rs @@ -0,0 +1,53 @@ +mod model; +mod parsing; + +use aoc_runner_derive::{aoc, aoc_generator}; +pub use model::{Probe, TargetArea}; +pub use parsing::parse_target_area; +use yap::IntoTokens; + +#[aoc_generator(day17)] +pub fn parse_input(input: &str) -> TargetArea { + parse_target_area(&mut input.into_tokens()).unwrap() +} + +#[aoc(day17, part1)] +pub fn part1(input: &TargetArea) -> usize { + let y = input.get_highest_possible_y_velocity().unwrap(); + + let mut ret_val = 0; + for i in 1..=y { + ret_val += i + } + ret_val as usize +} + +#[aoc(day17, part2)] +pub fn part2(input: &TargetArea) -> usize { + input.get_all_possible_velocities().len() +} + +#[cfg(test)] +mod test { + + const EXAMPLE_INPUT: &str = "target area: x=20..30, y=-10..-5"; + const RESULT_PART_1: usize = 45; + const RESULT_PART_2: usize = 112; + + #[test] + fn part1_example() { + let result = super::part1(&super::parse_input(EXAMPLE_INPUT)); + assert_eq!(result, RESULT_PART_1); + } + + #[test] + fn part2_example() { + let result = super::part2(&super::parse_input(EXAMPLE_INPUT)); + assert_eq!(result, RESULT_PART_2); + } + + #[test] + fn parse_example() { + super::parse_input(EXAMPLE_INPUT); + } +} diff --git a/src/day_17/model.rs b/src/day_17/model.rs new file mode 100644 index 0000000..c7246a3 --- /dev/null +++ b/src/day_17/model.rs @@ -0,0 +1,190 @@ +use std::ops::RangeBounds; + +use itertools::Itertools; + +#[derive(PartialEq, Eq, Debug)] +pub struct TargetArea(pub(crate) (isize, isize), pub(crate) (isize, isize)); + +#[derive(PartialEq, Eq)] +pub enum State { + EnRoute, + InTargetArea, + Overshot, +} + +impl TargetArea { + pub fn get_position_state(&self, position: (isize, isize)) -> State { + let x_bounds_en_route = self.get_bounds(true, true); + let y_bounds_en_route = self.get_bounds(false, true); + let x_bounds_target = self.get_bounds(true, false); + let y_bounds_target = self.get_bounds(false, false); + match ( + x_bounds_en_route.contains(&position.0), + y_bounds_en_route.contains(&position.1), + x_bounds_target.contains(&position.0), + y_bounds_target.contains(&position.1), + ) { + (_, _, true, true) => State::InTargetArea, + (true, true, _, _) => State::EnRoute, + _ => State::Overshot, + } + } + + pub fn get_bounds(&self, x: bool, en_route: bool) -> impl RangeBounds { + let mut values = if x { + vec![self.0 .0, self.0 .1] + } else { + vec![self.1 .0, self.1 .1] + }; + if en_route && x { + values.push(0); + } else if en_route && !x { + values.push(isize::MAX); + } + *values.iter().min().unwrap()..=*values.iter().max().unwrap() + } + + pub fn get_lowest_possible_x_velocity(&self) -> Option { + let bounds = self.get_bounds(true, false); + for i in 1..=Ord::max(self.0 .0, self.0 .1) { + if XLoc(0, i).any(|loc| bounds.contains(&loc)) { + return Some(i); + } + } + None + } + + pub fn get_highest_possible_y_velocity(&self) -> Option { + let bounds = self.get_bounds(false, false); + let y_min = Ord::min(self.1 .0, self.1 .1); + (y_min..500) + .rev() + .find(|i| YLoc(0, *i, y_min).any(|loc| bounds.contains(&loc))) + } + + pub fn get_all_possible_velocities(&self) -> Vec<(isize, isize)> { + let x_bounds = self.get_bounds(true, false); + let x_velocities = (1..=Ord::max(self.0 .0, self.0 .1)) + .filter(|i| XLoc(0, *i).any(|loc| x_bounds.contains(&loc))); + let y_bounds = self.get_bounds(false, false); + let y_min = Ord::min(self.1 .0, self.1 .1); + let y_velocities = (y_min..500) + .rev() + .filter(|i| YLoc(0, *i, y_min).any(|loc| y_bounds.contains(&loc))); + + let mut ret_val = Vec::new(); + + x_velocities + .cartesian_product(y_velocities) + .filter(|velocity| Probe::new(*velocity).will_reach_target_area(self)) + .for_each(|velocity| ret_val.push(velocity)); + + ret_val + } +} + +pub struct XLoc(isize, isize); + +impl Iterator for XLoc { + type Item = isize; + + fn next(&mut self) -> Option { + let velocity = self.1; + self.0 += velocity; + let item = self.0; + match velocity { + n if n > 0 => self.1 -= 1, + n if n < 0 => self.1 += 1, + _ => return None, + }; + Some(item) + } +} + +pub struct YLoc(isize, isize, isize); + +impl Iterator for YLoc { + type Item = isize; + + fn next(&mut self) -> Option { + let velocity = self.1; + self.0 += velocity; + self.1 -= 1; + let item = self.0; + if item < self.2 { + None + } else { + Some(item) + } + } +} + +#[derive(Clone, Debug)] +pub struct Probe { + pub position: (isize, isize), + pub velocity: (isize, isize), +} + +impl Probe { + pub fn new(velocity: (isize, isize)) -> Self { + Self { + position: (0, 0), + velocity, + } + } + pub fn step(&mut self) { + self.position.0 += self.velocity.0; + self.position.1 += self.velocity.1; + self.velocity.0 = match self.velocity.0 { + n if n > 0 => self.velocity.0 - 1, + n if n < 0 => self.velocity.0 + 1, + _ => 0, + }; + self.velocity.1 -= 1; + } + pub fn will_reach_target_area(&self, target_area: &TargetArea) -> bool { + let mut probe = self.clone(); + while target_area.get_position_state(probe.position) == State::EnRoute { + probe.step(); + } + matches!( + target_area.get_position_state(probe.position), + State::InTargetArea + ) + } +} + +#[cfg(test)] +mod test { + use crate::day_17::model::Probe; + + use super::TargetArea; + + fn example() -> TargetArea { + TargetArea((20, 30), (-10, -5)) + } + + #[test] + fn examples() { + for velocity in [(7, 2), (6, 3), (9, 0), (6, 9)] { + assert!(Probe::new(velocity).will_reach_target_area(&example())); + } + for velocity in [(17, -4)] { + assert!(!Probe::new(velocity).will_reach_target_area(&example())); + } + + for y_vel in 10..1000 { + assert!(!Probe::new((6, y_vel)).will_reach_target_area(&example())); + } + } + + #[test] + fn lowest_possible_x_velocity() { + assert_eq!(example().get_lowest_possible_x_velocity(), Some(6)); + } + + #[test] + fn highest_possible_y_velocity() { + assert_eq!(example().get_highest_possible_y_velocity(), Some(9)); + } +} diff --git a/src/day_17/parsing.rs b/src/day_17/parsing.rs new file mode 100644 index 0000000..d3b94f1 --- /dev/null +++ b/src/day_17/parsing.rs @@ -0,0 +1,42 @@ +use yap::Tokens; + +use crate::parsing::{parse_n, parse_number}; + +use super::model::TargetArea; + +pub fn parse_target_area(tokens: &mut impl Tokens) -> Option { + tokens.optional(|t| { + if !t.tokens("target area: x=".chars()) { + return None; + } + let x = parse_bounds(t)?; + if !t.tokens(", y=".chars()) { + return None; + } + let y = parse_bounds(t)?; + Some(TargetArea(x, y)) + }) +} + +pub fn parse_bounds(tokens: &mut impl Tokens) -> Option<(isize, isize)> { + tokens.optional(|t| { + let numbers: Option<[isize; 2]> = + parse_n(t, |t| parse_number(t), |t| t.tokens("..".chars())); + numbers.map(|numbers| (numbers[0], numbers[1])) + }) +} + +#[cfg(test)] +mod test { + use yap::IntoTokens; + + use crate::day_17::model::TargetArea; + + const EXAMPLE: &str = "target area: x=20..30, y=-10..-5"; + + #[test] + fn parse_example() { + let example = super::parse_target_area(&mut EXAMPLE.into_tokens()); + assert_eq!(example, Some(TargetArea((20, 30), (-10, -5)))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 79f09a2..c1ccb17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ pub mod day_13; pub mod day_14; pub mod day_15; pub mod day_16; +pub mod day_17; pub mod day_18; aoc_lib! { year = 2021 }