Files
aoc_2018/src/tasks/day15.rs
2023-05-26 10:02:22 +02:00

489 lines
15 KiB
Rust

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::<i32>() * 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::<i32>;
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::<i32>() * 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<Position> {
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<std::cmp::Ordering> {
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<Warrior>,
/// [x, y]
tiles: Vec<Vec<Tile>>,
width: usize,
height: usize,
}
impl Game {
fn closest_fighting_position(
&self,
from: Position,
target_type: WarriorType,
) -> Option<Position> {
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<Position> {
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<usize> = 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<Vec<MapTile>>,
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<Vec<Position>> {
if to == from {
return Some(vec![]);
}
if self.fields[from.0][from.1] != MapTile::Empty {
return None;
}
let mut open: VecDeque<(Option<Position>, Position)> = VecDeque::new();
open.push_back((None, from));
let mut predecessors: HashMap<Position, Option<Position>> = 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<Position> = Vec::new();
let mut current = to;
result.push(current);
while let Some(Some(predecessor)) = predecessors.get(&current) {
result.push(*predecessor);
current = *predecessor;
}
result.reverse();
Some(result)
} else {
None
}
}
fn find_closest_target(&self, from: Position) -> Option<Position> {
let mut open: VecDeque<(usize, Position)> = VecDeque::new();
open.push_back((0, from));
let mut visited: HashSet<Position> = HashSet::new();
let mut current_found_distance = usize::max_value();
let mut candidates: Vec<Position> = 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()
}
}