adventofcode22/src/day9.rs
2022-12-09 15:13:38 +01:00

258 lines
6.2 KiB
Rust

//! Day 9: Rope Bridge
use std::{collections::HashSet, fmt::Debug};
use itertools::Itertools;
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
struct Position {
x: i32,
y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
U,
D,
R,
L,
}
impl Position {
fn move_dir(self, dir: Direction) -> Self {
match dir {
Direction::U => Self {
x: self.x,
y: self.y + 1,
},
Direction::D => Self {
x: self.x,
y: self.y - 1,
},
Direction::R => Self {
x: self.x + 1,
y: self.y,
},
Direction::L => Self {
x: self.x - 1,
y: self.y,
},
}
}
fn dx(self, p2: Position) -> i32 {
self.x - p2.x
}
fn dy(self, p2: Position) -> i32 {
self.y - p2.y
}
}
impl Debug for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}|{}", self.x, self.y))
}
}
fn update_rope_pos(hpos: Position, tpos: Position) -> (Position, Position) {
let dx = hpos.dx(tpos);
let dy = hpos.dy(tpos);
match (dx.abs() < 2, dy.abs() < 2) {
(true, true) => (hpos, tpos),
(true, false) => (
hpos,
Position {
x: hpos.x,
y: hpos.y - dy + dy.signum(),
},
),
(false, true) => (
hpos,
Position {
x: hpos.x - dx + dx.signum(),
y: hpos.y,
},
),
(false, false) => (
hpos,
Position {
x: hpos.x - dx + dx.signum(),
y: hpos.y - dy + dy.signum(),
},
),
}
}
fn parse_line(line: &str) -> (Direction, i32) {
let (d_str, n_str) = line.split_once(' ').unwrap();
let d = match d_str {
"U" => Direction::U,
"D" => Direction::D,
"R" => Direction::R,
"L" => Direction::L,
_ => panic!("invalid dir: {}", d_str),
};
(d, n_str.parse().unwrap())
}
fn print_rope(rope: &[Position]) {
let (min_x, max_x) = match rope.iter().map(|p| p.x).minmax() {
itertools::MinMaxResult::NoElements => return,
itertools::MinMaxResult::OneElement(x) => (x, x),
itertools::MinMaxResult::MinMax(min_x, max_x) => (min_x, max_x),
};
let (min_y, max_y) = match rope.iter().map(|p| p.y).minmax() {
itertools::MinMaxResult::NoElements => return,
itertools::MinMaxResult::OneElement(y) => (y, y),
itertools::MinMaxResult::MinMax(min_y, max_y) => (min_y, max_y),
};
for y in (min_y..=max_y).rev() {
for x in min_x..=max_x {
if let Some((n, _)) = rope.iter().find_position(|p| p == &&Position { x, y }) {
print!("{}", n)
} else {
print!(".");
}
}
println!();
}
println!();
}
pub fn part1(input: Vec<String>) -> String {
let mut visited = HashSet::new();
let mut hpos = Position::default();
let mut tpos = Position::default();
visited.insert(tpos);
for (dir, n) in input.iter().map(|line| parse_line(line)) {
for _ in 0..n {
(hpos, tpos) = update_rope_pos(hpos.move_dir(dir), tpos);
visited.insert(tpos);
}
}
visited.len().to_string()
}
pub fn part2(input: Vec<String>) -> String {
let mut visited = HashSet::new();
let rope_len = 10;
// head of the rope first
let mut rope = (0..rope_len)
.map(|_| Position::default())
.collect::<Vec<_>>();
visited.insert(rope[rope_len - 1]);
for (dir, n) in input.iter().map(|line| parse_line(line)) {
for _ in 0..n {
let mut new_rope = Vec::with_capacity(rope_len);
let mut rope_iter = rope.iter();
let mut hpos = rope_iter.next().copied().unwrap().move_dir(dir);
for tpos in rope_iter {
let (n_hpos, n_tpos) = update_rope_pos(hpos, *tpos);
new_rope.push(n_hpos);
// Tail pos of this section will be head pos of next section
hpos = n_tpos;
}
new_rope.push(hpos);
rope = new_rope;
visited.insert(rope[rope_len - 1]);
// print_rope(&rope);
}
}
print_rope(&rope);
visited.len().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
const DAY: u8 = 9;
#[rstest]
#[case(
Position {x: 0, y: 0},
Position {x: 0, y: 0},
Direction::R,
Position {x: 0, y: 0},
)]
#[case(
Position {x: 1, y: 0},
Position {x: 0, y: 0},
Direction::R,
Position {x: 1, y: 0},
)]
#[case(
Position {x: 4, y: 1},
Position {x: 3, y: 0},
Direction::U,
Position {x: 4, y: 1},
)]
#[case(
Position {x: 4, y: 4},
Position {x: 4, y: 3},
Direction::L,
Position {x: 4, y: 3},
)]
#[case(
Position {x: 3, y: 4},
Position {x: 4, y: 3},
Direction::L,
Position {x: 3, y: 4},
)]
fn rope_pos(
#[case] hpos: Position,
#[case] tpos: Position,
#[case] dir: Direction,
#[case] exp: Position,
) {
let (_, tpos) = update_rope_pos(hpos.move_dir(dir), tpos);
assert_eq!(tpos, exp);
}
#[test]
fn t_example1() {
assert_eq!(part1(crate::read_example(DAY)), "13");
}
#[test]
fn t_example2() {
assert_eq!(part2(crate::read_example(DAY)), "1");
}
#[test]
fn t_example2b() {
let input = vec![
"R 5".to_owned(),
"U 8".to_owned(),
"L 8".to_owned(),
"D 3".to_owned(),
"R 17".to_owned(),
"D 10".to_owned(),
"L 25".to_owned(),
"U 20".to_owned(),
];
assert_eq!(part2(input), "36");
}
#[test]
fn t_part1() {
assert_eq!(part1(crate::read_input(DAY)), "6332");
}
#[test]
fn t_part2() {
assert_eq!(part2(crate::read_input(DAY)), "2511");
}
}