diff --git a/input/day15.txt b/input/day15.txt new file mode 100644 index 0000000..5d6aaaa --- /dev/null +++ b/input/day15.txt @@ -0,0 +1,9 @@ +######### +#G..G..G# +#.......# +#.......# +#G..E..G# +#.......# +#.......# +#G..G..G# +######### \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3a2692b..e1a63f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ fn main() { - // aoc_2018::tasks::day14::task1(); - aoc_2018::tasks::day14::task2(); + aoc_2018::tasks::day15::task1(); + // aoc_2018::tasks::day15::task2(); } diff --git a/src/tasks/day15.rs b/src/tasks/day15.rs new file mode 100644 index 0000000..504d9ef --- /dev/null +++ b/src/tasks/day15.rs @@ -0,0 +1,370 @@ +use crate::utils; +use std::collections::HashMap; +use std::collections::HashSet; +use std::collections::VecDeque; +use std::fmt::Display; + +pub fn task1() { + let input = utils::read_file("input/day15.txt"); + let mut game = Game::from_input(input.lines().collect()); + println!("{}", game); + game.round(); + println!("{}", game); + game.round(); + println!("{}", game); + game.round(); + println!("{}", game); + game.round(); + println!("{}", game); +} + +#[derive(Clone, PartialEq)] +enum Tile { + Empty, + Wall, + Unit(WarriorType), +} + +#[derive(Clone, Copy, PartialEq)] +enum WarriorType { + Elve, + Goblin, +} + +impl WarriorType { + fn enemy_type(&self) -> Self { + use self::WarriorType::*; + match self { + Elve => Goblin, + Goblin => Elve, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +struct Position(usize, usize); + +impl Position { + fn neighbors(&self, width: usize, height: usize) -> Vec { + vec![ + (self.0 as isize, self.1 as isize - 1), + (self.0 as isize - 1, self.1 as isize), + (self.0 as isize + 1, self.1 as isize), + (self.0 as isize, self.1 as isize + 1), + ] + .into_iter() + .filter(|p| p.0 > 0 && p.0 < width as isize - 1 && p.1 > 0 && p.1 < height as isize - 1) + .map(|it| Position(it.0 as usize, it.1 as usize)) + .collect() + } +} + +impl PartialOrd for Position { + fn partial_cmp(&self, other: &Self) -> std::option::Option { + if self.1 == other.1 { + Some(self.0.cmp(&other.0)) + } else { + Some(self.1.cmp(&other.1)) + } + } +} + +impl Ord for Position { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + if self.1 == other.1 { + self.0.cmp(&other.0) + } else { + self.1.cmp(&other.1) + } + } +} + +struct Warrior { + warrior_type: WarriorType, + position: Position, + health: i32, +} + +struct Game { + units: Vec, + /// [x, y] + tiles: Vec>, + width: usize, + height: usize, +} + +impl Game { + fn closest_fighting_position( + &self, + from: Position, + target_type: WarriorType, + ) -> Option { + let mut map = Map::from_game(&self, from); + + for unit in self.units.iter() { + if unit.warrior_type == target_type { + for neighbor in unit.position.neighbors(self.width, self.height) { + if map.fields[neighbor.0][neighbor.1] == MapTile::Empty { + map.fields[neighbor.0][neighbor.1] = MapTile::TargetNonOccupied; + } + } + } + } + + map.find_closest_target(from) + } + + fn next_position(&self, from: Position, to: Position) -> Option { + if from == to { + return Some(from); + } + let input = vec![ + (1, Position(from.0, from.1 - 1)), + (2, Position(from.0 - 1, from.1)), + (3, Position(from.0 + 1, from.1)), + (4, Position(from.0, from.1 + 1)), + ]; + if let Some((_, best)) = input + .iter() + .filter_map(|(delta, start)| { + let map = Map::from_game(&self, *start); + if let Some(path) = map.shortest_path(*start, to) { + //println!("Path from {:?}: {:?}", start, path); + Some((delta + path.len() * 10, *path.first().unwrap_or(start))) + } else { + None + } + }) + .min_by_key(|(d, _)| *d) + { + Some(best) + } else { + None + } + } + + fn round(&mut self) { + self.units.sort_unstable_by_key(|it| it.position); + + for curr in 0..self.units.len() { + if let Some(next_target_position) = self.closest_fighting_position( + self.units[curr].position, + self.units[curr].warrior_type.enemy_type(), + ) { + if let Some(next_position) = + self.next_position(self.units[curr].position, next_target_position) + { + // println!( + // "{:?} -> {:?} ({:?})", + // self.units[curr].position, next_target_position, next_position + // ); + let curr_pos = self.units[curr].position; + self.tiles[curr_pos.0][curr_pos.1] = Tile::Empty; + self.units[curr].position = next_position; + self.tiles[next_position.0][next_position.1] = + Tile::Unit(self.units[curr].warrior_type); + } else { + panic!("We have a reachable target but no path to it!"); + } + } + } + } + + fn from_input(input: Vec<&str>) -> Self { + use self::Tile::*; + use self::WarriorType::*; + let width = input[0].len(); + let height = input.len(); + let mut inner_vec = Vec::new(); + inner_vec.resize(height, Empty); + let mut tiles = Vec::new(); + tiles.resize(width, inner_vec); + + let mut units = Vec::new(); + for (y, line) in input.iter().enumerate() { + for (x, c) in line.chars().enumerate() { + tiles[x][y] = match c { + '.' => Empty, + '#' => Wall, + 'E' => { + units.push(Warrior { + warrior_type: Elve, + position: Position(x, y), + health: 300, + }); + Unit(Elve) + } + 'G' => { + units.push(Warrior { + warrior_type: Goblin, + position: Position(x, y), + health: 300, + }); + Unit(Goblin) + } + c => panic!("Unexpected input character '{}'", c), + } + } + } + + Game { + units, + tiles, + width, + height, + } + } +} + +impl Display for Game { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + use self::Tile::*; + for y in 0..self.height { + let mut line = String::with_capacity(self.width); + for x in 0..self.width { + if let Some(warrior) = self.units.iter().find(|it| it.position == Position(x, y)) { + line.push(match warrior.warrior_type { + WarriorType::Elve => 'E', + WarriorType::Goblin => 'G', + }); + } else { + line.push(match &self.tiles[x][y] { + Empty => '.', + Wall => '#', + Unit(WarriorType::Elve) => 'E', + Unit(WarriorType::Goblin) => 'G', + }); + } + } + f.write_str(&line)?; + f.write_str(&"\n")?; + } + Ok(()) + } +} + +#[derive(Debug)] +struct Map { + fields: Vec>, + width: usize, + height: usize, +} + +#[derive(PartialEq, Debug)] +enum MapTile { + Empty, + TargetNonOccupied, + Occupied, +} + +impl Display for Map { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + use self::MapTile::*; + for y in 0..self.height { + let mut line = String::with_capacity(self.width); + for x in 0..self.width { + line.push(match &self.fields[x][y] { + Empty => '.', + Occupied => 'X', + TargetNonOccupied => 'o', + }); + } + f.write_str(&line)?; + f.write_str(&"\n")?; + } + Ok(()) + } +} + +impl Map { + fn from_game(game: &Game, clear: Position) -> Self { + let mut fields = Vec::with_capacity(game.width); + for x in 0..game.width { + let mut new = Vec::with_capacity(game.height); + for y in 0..game.height { + new.push(match &game.tiles[x][y] { + Tile::Empty => MapTile::Empty, + _ => MapTile::Occupied, + }); + } + fields.push(new); + } + fields[clear.0][clear.1] = MapTile::Empty; + Map { + fields: fields, + width: game.width, + height: game.height, + } + } + + fn shortest_path(&self, from: Position, to: Position) -> Option> { + if to == from { + return Some(vec![]); + } + if self.fields[from.0][from.1] != MapTile::Empty { + return None; + } + let mut open: VecDeque<(Option, Position)> = VecDeque::new(); + open.push_back((None, from)); + let mut predecessors: HashMap> = HashMap::new(); + + while let Some((predecessor, curr_pos)) = open.pop_front() { + predecessors.insert(curr_pos, predecessor); + if curr_pos == to { + break; + } + for pos in curr_pos.neighbors(self.width, self.height) { + if !predecessors.contains_key(&pos) && !open.iter().any(|it| it.1 == pos) { + open.push_back((Some(curr_pos), pos)); + } + } + } + + if let Some(Some(_)) = predecessors.get(&to) { + let mut result: Vec = Vec::new(); + let mut current = to; + while let Some(Some(predecessor)) = predecessors.get(¤t) { + result.push(*predecessor); + current = *predecessor; + } + result.reverse(); + Some(result) + } else { + None + } + } + + fn find_closest_target(&self, from: Position) -> Option { + // println!("{}", self); + let mut open: VecDeque<(usize, Position)> = VecDeque::new(); + open.push_back((0, from)); + let mut visited: HashSet = HashSet::new(); + let mut current_found_distance = usize::max_value(); + let mut candidates: Vec = Vec::new(); + + while let Some((curr_dist, curr_pos)) = open.pop_front() { + if curr_dist > current_found_distance { + break; + } + if self.fields[curr_pos.0][curr_pos.1] == MapTile::TargetNonOccupied { + candidates.push(curr_pos); + current_found_distance = curr_dist; + // all others would have a higher distance and therefore are not relevant + continue; + } + for pos in curr_pos.neighbors(self.width, self.height) { + if !visited.contains(&pos) && !open.iter().any(|it| it.1 == pos) { + open.push_back((curr_dist + 1, pos)); + } + } + visited.insert(curr_pos); + } + + candidates.sort_unstable(); + if let Some(x) = candidates.first() { + Some(*x) + } else { + None + } + } +} diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index f4877db..c4cadc0 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -12,3 +12,4 @@ pub mod day11; pub mod day12; pub mod day13; pub mod day14; +pub mod day15;