Skip to main content

asterion_core/
hero.rs

1use crate::{entity::Entity, power_up::PowerUp, Direction, PlayerId, Position};
2use std::{
3    collections::{HashMap, HashSet},
4    time::{Duration, Instant},
5};
6use strum_macros::Display;
7
8#[derive(Debug)]
9pub enum GameCommand {
10    Move { direction: Direction },
11    TurnClockwise,
12    TurnCounterClockwise,
13    CycleUiOptions,
14}
15
16#[derive(Debug, Clone, Copy, Display, PartialEq)]
17pub enum HeroState {
18    WaitingToStart,
19    InMaze {
20        instant: Instant,
21    },
22    Dead {
23        duration: Duration,
24        instant: Instant,
25    },
26    Victory {
27        duration: Duration,
28        instant: Instant,
29    },
30}
31#[derive(Debug, Clone, Copy, Display, PartialEq)]
32pub enum UiOptions {
33    Dark,
34    Light,
35}
36
37impl UiOptions {
38    pub fn next(&self) -> Self {
39        match self {
40            Self::Dark => Self::Light,
41            Self::Light => Self::Dark,
42        }
43    }
44}
45
46#[derive(Debug)]
47pub struct Hero {
48    id: PlayerId,
49    name: String,
50    pub state: HeroState,
51    maze_id: usize,
52    position: Position,
53    direction: Direction,
54    vision: usize,
55    speed: u64,
56    memory: u64,
57    past_visible_positions: HashMap<usize, HashMap<Position, Instant>>,
58    last_move_time: Instant,
59    collected_power_ups: HashMap<usize, Vec<Position>>,
60    ui_options: UiOptions,
61}
62
63impl Hero {
64    pub const MAX_SPEED: u64 = 8;
65    pub const MAX_VISION: usize = 8;
66    pub const INITIAL_SPEED: u64 = 4;
67    pub const INITIAL_VISION: usize = 1;
68    pub const INITIAL_MEMORY: u64 = 0;
69    pub fn new(id: PlayerId, name: String, position: Position) -> Self {
70        let state = HeroState::WaitingToStart;
71        Self {
72            id,
73            name,
74            state,
75            maze_id: 0,
76            position,
77            direction: Direction::East,
78            vision: Self::INITIAL_VISION,
79            speed: Self::INITIAL_SPEED,
80            memory: Self::INITIAL_MEMORY,
81            past_visible_positions: HashMap::new(),
82            last_move_time: Instant::now(),
83            collected_power_ups: HashMap::new(),
84            ui_options: UiOptions::Dark,
85        }
86    }
87
88    pub fn reset(&mut self, position: Position) {
89        self.state = HeroState::WaitingToStart;
90        self.maze_id = 0;
91        self.position = position;
92        self.direction = Direction::East;
93        self.vision = Self::INITIAL_VISION;
94        self.speed = Self::INITIAL_SPEED;
95        self.memory = Self::INITIAL_MEMORY;
96        self.past_visible_positions.clear();
97        self.last_move_time = Instant::now();
98        self.collected_power_ups.clear();
99    }
100
101    pub fn cycle_ui_options(&mut self) {
102        self.ui_options = self.ui_options.next();
103    }
104
105    pub fn is_dead(&self) -> bool {
106        matches!(self.state, HeroState::Dead { .. })
107    }
108
109    pub fn has_won(&self) -> Option<Duration> {
110        match self.state {
111            HeroState::Victory { duration, .. } => Some(duration),
112            _ => None,
113        }
114    }
115
116    pub fn memory(&self) -> u64 {
117        self.memory
118    }
119
120    pub fn elapsed_duration_from_start(&self) -> Duration {
121        match self.state {
122            HeroState::WaitingToStart => Duration::from_millis(0),
123            HeroState::InMaze { instant } => instant.elapsed(),
124            HeroState::Dead { duration, .. } => duration,
125            HeroState::Victory { duration, .. } => duration,
126        }
127    }
128
129    pub fn can_move(&self) -> bool {
130        if self.is_dead() {
131            return false;
132        }
133
134        self.last_move_time.elapsed() >= self.movement_recovery_duration()
135    }
136
137    pub fn past_visibility_duration(&self) -> Duration {
138        Duration::from_secs_f32(10.0 + 10.0 * self.memory as f32)
139    }
140
141    pub fn update_past_visible_positions(&mut self, visible_positions: HashSet<Position>) {
142        let duration = self.past_visibility_duration();
143
144        let past_visible_positions = self.past_visible_positions.entry(self.maze_id).or_default();
145
146        for &position in visible_positions.iter() {
147            past_visible_positions.insert(position, Instant::now());
148        }
149        past_visible_positions.retain(|_, instant| instant.elapsed() < duration);
150    }
151
152    pub fn past_visible_positions(&self) -> &HashMap<Position, Instant> {
153        self.past_visible_positions.get(&self.maze_id).unwrap()
154    }
155
156    pub fn set_direction(&mut self, direction: Direction) {
157        self.direction = direction;
158    }
159
160    pub fn set_position(&mut self, position: Position) {
161        self.position = position;
162        self.last_move_time = Instant::now();
163    }
164
165    pub fn set_maze_id(&mut self, maze_id: usize) {
166        self.maze_id = maze_id;
167    }
168
169    pub fn decrease_vision(&mut self) {
170        self.vision -= 1;
171    }
172
173    pub fn apply_random_power_up_at_position(&mut self, position: Position) {
174        let mut available_power_ups = vec![];
175
176        if self.speed < Self::MAX_SPEED {
177            available_power_ups.push(PowerUp::Speed);
178        }
179
180        if self.vision < Self::MAX_VISION {
181            available_power_ups.push(PowerUp::Vision);
182        }
183
184        available_power_ups.push(PowerUp::Memory);
185
186        let idx = rand::random_range(0..available_power_ups.len());
187
188        let power_up = available_power_ups[idx];
189        match power_up {
190            PowerUp::Speed => self.speed = (self.speed + 1).min(Self::MAX_SPEED),
191            PowerUp::Vision => self.vision = (self.vision + 1).min(Self::MAX_VISION),
192            PowerUp::Memory => self.memory += 1,
193        }
194
195        self.collected_power_ups
196            .entry(self.maze_id)
197            .and_modify(|e| e.push(position))
198            .or_insert(vec![position]);
199    }
200
201    pub fn power_ups_collected_in_maze(&self, maze_id: usize) -> usize {
202        self.collected_power_ups
203            .get(&maze_id)
204            .map(|v| v.len())
205            .unwrap_or_default()
206    }
207
208    pub fn power_up_collected_at(&self, maze_id: usize, position: Position) -> bool {
209        self.collected_power_ups
210            .get(&maze_id)
211            .map(|v| v.contains(&position))
212            .unwrap_or_default()
213    }
214}
215
216impl Entity for Hero {
217    fn id(&self) -> PlayerId {
218        self.id
219    }
220
221    fn name(&self) -> &str {
222        &self.name
223    }
224
225    fn vision(&self) -> usize {
226        self.vision
227    }
228
229    fn speed(&self) -> u64 {
230        self.speed
231    }
232
233    fn position(&self) -> super::Position {
234        self.position
235    }
236
237    fn direction(&self) -> Direction {
238        self.direction
239    }
240
241    fn maze_id(&self) -> usize {
242        self.maze_id
243    }
244}