asterion_core/
minotaur.rs1use crate::{entity::Entity, Direction, Hero, IntoDirection, Maze, PlayerId, Position};
2use itertools::Itertools;
3use rand::{seq::IteratorRandom, RngExt};
4use std::time::Instant;
5
6#[derive(Debug)]
7pub struct Minotaur {
8 id: PlayerId,
9 name: String,
10 chasing: Option<PlayerId>,
11 maze_id: usize,
12 position: Position,
13 direction: Direction,
14 speed: u64,
15 vision: usize,
16 aggression: f64,
17 last_update_time: Instant,
19 pub kills: usize,
20}
21
22impl Minotaur {
23 fn find_closest_hero(&mut self, visible_heros: &Vec<&Hero>) -> Option<PlayerId> {
24 let closest_hero = visible_heros
25 .iter()
26 .min_by_key(|hero| self.position.distance_squared(hero.position()));
27
28 closest_hero.map(|hero| hero.id())
29 }
30
31 pub fn new(
32 name: String,
33 maze_id: usize,
34 position: Position,
35 speed: u64,
36 vision: usize,
37 aggression: f64,
38 ) -> Self {
39 Self {
40 id: PlayerId::new_v4(),
41 name,
42 chasing: None,
43 maze_id,
44 position,
45 direction: Direction::North,
46 speed,
47 vision,
48 aggression,
49 last_update_time: Instant::now(),
50 kills: 0,
51 }
52 }
53
54 pub fn update(&mut self, maze: &mut Maze, visible_heros: Vec<&Hero>) {
55 if let Some(hero_id) = self.chasing {
56 if !visible_heros.iter().map(|h| h.id()).any(|id| id == hero_id) {
57 self.chasing = None;
58 }
59 } else if let Some(hero_id) = self.find_closest_hero(&visible_heros) {
60 self.chasing = Some(hero_id);
61 }
62
63 if self.last_update_time.elapsed() < self.movement_recovery_duration() {
64 return;
65 }
66
67 let rng = &mut rand::rng();
68 let (x, y) = self.position;
69
70 let mut available_directions = [
71 Direction::North,
72 Direction::East,
73 Direction::South,
74 Direction::West,
75 ]
76 .iter()
77 .filter(|d| {
78 let new_position = (
79 (x as isize + d.as_offset().0).max(0) as usize,
80 (y as isize + d.as_offset().1).max(0) as usize,
81 );
82 maze.is_valid_minotaur_position(new_position)
83 })
84 .collect_vec();
85
86 if let Some(hero_id) = self.chasing {
87 if let Some(hero) = visible_heros.iter().find(|h| h.id() == hero_id) {
89 let current_distance = self.position.distance(hero.position());
90 available_directions = available_directions
91 .iter()
92 .filter(|d| {
93 let new_position = (
94 (x as isize + d.as_offset().0).max(0) as usize,
95 (y as isize + d.as_offset().1).max(0) as usize,
96 );
97 new_position.distance(hero.position()) < current_distance
98 }).copied()
99 .collect_vec();
100 }
101 }
102
103 if !available_directions.is_empty() && rng.random_bool(self.aggression) {
104 let direction = available_directions.iter().choose(rng).unwrap();
106 let new_position = (
107 (x as isize + direction.as_offset().0).max(0) as usize,
108 (y as isize + direction.as_offset().1).max(0) as usize,
109 );
110 self.position = new_position;
111 self.last_update_time = Instant::now();
112 }
113 }
114
115 pub fn is_chasing(&self, player_id: PlayerId) -> bool {
116 self.chasing == Some(player_id)
117 }
118
119 pub fn is_chasing_someone(&self) -> bool {
120 self.chasing.is_some()
121 }
122}
123
124impl Entity for Minotaur {
125 fn id(&self) -> PlayerId {
126 self.id
127 }
128
129 fn name(&self) -> &str {
130 &self.name
131 }
132
133 fn vision(&self) -> usize {
134 self.vision
135 }
136
137 fn speed(&self) -> u64 {
138 self.speed
139 }
140
141 fn position(&self) -> super::Position {
142 self.position
143 }
144
145 fn direction(&self) -> Direction {
146 self.direction
147 }
148
149 fn maze_id(&self) -> usize {
150 self.maze_id
151 }
152}