use crate::utils; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; use std::fmt::Display; const HEALTH: i32 = 200; pub fn task1() { let input = utils::read_file("input/day15.txt"); let mut game = Game::from_input(&input.lines().collect(), 3, 3); println!("{}", game); let mut round = 0; while game.round() { round += 1; // println!("{}", game); // println!("round was {}", round); } println!("Final full round was {}", round); println!( "Result: {}", game.units.iter().map(|it| it.health).sum::() * round ); } pub fn task2() { let input = utils::read_file("input/day15.txt"); let input = input.lines().collect(); let mut highest_fail = 3; let mut lowest_win = None::; while lowest_win.is_none() || lowest_win.unwrap() - 1 > highest_fail { let attack = match lowest_win { Some(upper) => highest_fail + (upper - highest_fail) / 2, None => 2 * highest_fail, }; let mut game = Game::from_input(&input, 3, attack); let initial_elve_count = game .units .iter() .filter(|unit| unit.warrior_type == WarriorType::Elve) .count(); let mut round = 0; while game.round() { round += 1; } if game .units .iter() .filter(|unit| unit.warrior_type == WarriorType::Elve) .count() == initial_elve_count { lowest_win = Some(attack); } else { highest_fail = attack; } println!( "Result: {}", game.units.iter().map(|it| it.health).sum::() * round ); } println!("Searching stopped with lowest win {:?}", lowest_win); // 7169 too low } #[derive(Clone, PartialEq)] enum Tile { Empty, Wall, } #[derive(Clone, Copy, PartialEq, Debug)] 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() } fn manhattan_distance(&self, other: &Self) -> usize { let a = if self.0 > other.0 { self.0 - other.0 } else { other.0 - self.0 }; let b = if self.1 > other.1 { self.1 - other.1 } else { other.1 - self.1 }; a + b } } 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) } } } #[derive(Debug)] struct Warrior { warrior_type: WarriorType, position: Position, health: i32, attack: 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, from); // if let Some(path) = map.shortest_path(*start, to) { // Some((delta + path.len() * 10, *path.first().unwrap_or(start))) // } else { // None // } // }) // .min_by_key(|(d, _)| *d) // { // Some(best) // } else { // None // } let map = Map::from_game(self, from); map.shortest_path(from, to) .map(|path| *path.get(1).unwrap_or(&from)) } /// Returns true if a full round was played, false if the round aborted because all /// enemies of one party are dead fn round(&mut self) -> bool { self.units.sort_unstable_by_key(|it| it.position); let mut curr = 0; while curr < self.units.len() { if !self .units .iter() .any(|warrior| warrior.warrior_type == self.units[curr].warrior_type.enemy_type()) { println!("There are no enemies anymore!"); return false; } // movement phase if let Some(next_target_position) = self.closest_fighting_position( self.units[curr].position, self.units[curr].warrior_type.enemy_type(), ) { if self.units[curr].position != next_target_position { if let Some(next_position) = self.next_position(self.units[curr].position, next_target_position) { // println!( // "{:?} moves to {:?} via {:?}", // self.units[curr].position, next_target_position, next_position // ); self.units[curr].position = next_position; } else { panic!("We have a reachable target but no path to it! {:?} wants to go to {:?}", self.units[curr], next_target_position); } } } // attack phase let neighbors = self.units[curr].position.neighbors(self.width, self.height); let mut close_enemies: Vec = self .units .iter() .enumerate() .filter(|(_, it)| it.warrior_type == self.units[curr].warrior_type.enemy_type()) .filter(|(_, it)| neighbors.contains(&it.position)) .map(|(i, _)| i) .collect(); close_enemies.sort_unstable_by(|a, b| { let a = &self.units[*a]; let b = &self.units[*b]; if a.health == b.health { a.position.cmp(&b.position) } else { a.health.cmp(&b.health) } }); if let Some(closest_index) = close_enemies.first() { if self.units[*closest_index] .position .manhattan_distance(&self.units[curr].position) > 1 { panic!("Distance WTF") } let attack = self.units[curr].attack; let enemy = &mut self.units[*closest_index]; enemy.health -= attack; if enemy.health <= 0 { let enemy = self.units.remove(*closest_index); if *closest_index < curr { curr -= 1; } self.tiles[enemy.position.0][enemy.position.1] = Tile::Empty; } } curr += 1; } true } fn from_input(input: &Vec<&str>, goblin_attack: i32, elve_attack: i32) -> 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: HEALTH, attack: elve_attack, }); Empty } 'G' => { units.push(Warrior { warrior_type: Goblin, position: Position(x, y), health: HEALTH, attack: goblin_attack, }); Empty } 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 => '#', }); } } 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, Tile::Wall => MapTile::Occupied, }); } fields.push(new); } for warrior in game.units.iter() { fields[warrior.position.0][warrior.position.1] = MapTile::Occupied; } fields[clear.0][clear.1] = MapTile::Empty; Map { 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; } if self.fields[curr_pos.0][curr_pos.1] != MapTile::Empty { continue; } 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; result.push(current); 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 { 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; } if self.fields[curr_pos.0][curr_pos.1] == MapTile::Occupied { 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(); candidates.first().copied() } }