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}