instead of senselessly trying to find the best starting point, just rely on the order of neighbor point generation. Since we use a queue to manage open points and he neighbors are added in the correct order, they neighbor that has precedence due to reading order will automatically be the one to be predecessor on a final path, if he is on a shortest path.
497 lines
15 KiB
Rust
497 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 as i32
|
|
);
|
|
}
|
|
|
|
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 as i32
|
|
);
|
|
}
|
|
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);
|
|
if let Some(path) = map.shortest_path(from, to) {
|
|
// println!("{:?}", path);
|
|
Some(*path.get(1).unwrap_or(&from))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
return 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: 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(¤t) {
|
|
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();
|
|
if let Some(x) = candidates.first() {
|
|
Some(*x)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|