1use crate::{
2 entity::Entity,
3 hero::{GameCommand, HeroState},
4 minotaur::Minotaur,
5 utils::{is_transparent, random_minotaur_name, to_player_name},
6 AlarmLevel, GameColors, Hero, IntoDirection, Maze, PlayerId,
7};
8use anyhow::{anyhow, Result as AppResult};
9use image::{Rgba, RgbaImage};
10use itertools::Itertools;
11use std::{
12 collections::{HashMap, HashSet},
13 time::{Duration, Instant},
14};
15
16pub const MAX_MAZE_ID: usize = 10;
17pub const POWER_UPS_PER_ROOM: usize = 3;
18
19pub struct Game {
20 mazes: [Maze; MAX_MAZE_ID],
21 taken_names: HashSet<String>,
22 heros: HashMap<PlayerId, Hero>,
23 hero_rooms: [Vec<PlayerId>; MAX_MAZE_ID],
24 top_heros_map: HashMap<PlayerId, (String, usize, Duration)>,
25 top_heros: Vec<(PlayerId, String, usize, Duration)>,
26 minotaurs: HashMap<PlayerId, Minotaur>,
27 minotaur_rooms: [Vec<PlayerId>; MAX_MAZE_ID],
28 top_minotaurs_map: HashMap<PlayerId, (String, usize, usize)>,
29 top_minotaurs: Vec<(PlayerId, String, usize, usize)>,
30}
31
32impl Game {
33 const RESPAWN_INTERVAL: Duration = Duration::from_millis(1500);
34
35 fn should_update_hero_record(&self, hero_id: PlayerId) -> bool {
36 let hero = if let Some(hero) = self.get_hero(&hero_id) {
37 hero
38 } else {
39 return false;
40 };
41
42 let &(_, record_maze_id, record_timer) =
43 if let Some(record) = self.top_heros_map.get(&hero_id) {
44 record
45 } else {
46 return true;
47 };
48
49 if let Some(duration) = hero.has_won() {
50 if record_maze_id < MAX_MAZE_ID {
51 return true;
52 }
53 return record_timer > duration;
54 }
55
56 if record_maze_id < hero.maze_id() {
57 return true;
58 } else if record_maze_id > hero.maze_id() {
59 return false;
60 }
61
62 record_timer > hero.elapsed_duration_from_start()
64 }
65
66 fn update_hero_record(&mut self, hero_id: PlayerId) {
67 if self.should_update_hero_record(hero_id) {
68 let hero = if let Some(hero) = self.get_hero(&hero_id) {
69 hero
70 } else {
71 return;
72 };
73
74 let record = if let Some(duration) = hero.has_won() {
75 (hero.name().to_string(), MAX_MAZE_ID, duration)
76 } else {
77 (
78 hero.name().to_string(),
79 hero.maze_id(),
80 hero.elapsed_duration_from_start(),
81 )
82 };
83 self.top_heros_map.insert(hero_id, record);
84 self.update_top_heros();
85 }
86 }
87
88 fn update_top_heros(&mut self) {
89 self.top_heros = self
90 .top_heros_map
91 .iter()
92 .map(|(&id, (name, record_maze_id, duration))| {
93 (id, name.clone(), *record_maze_id, *duration)
94 })
95 .sorted_by(|a, b| {
96 if b.2 == a.2 {
97 a.3.cmp(&b.3)
98 } else {
99 b.2.cmp(&a.2)
100 }
101 })
102 .collect_vec();
103 }
104
105 fn update_top_minotaurs(&mut self) {
106 self.top_minotaurs = self
107 .minotaurs
108 .values()
109 .sorted_by(|a, b| {
110 if b.kills == a.kills {
111 b.maze_id().cmp(&a.maze_id())
112 } else {
113 b.kills.cmp(&a.kills)
114 }
115 })
116 .map(|minotaur| {
117 (
118 minotaur.id(),
119 minotaur.name().to_string(),
120 minotaur.maze_id(),
121 minotaur.kills,
122 )
123 })
124 .collect_vec();
125 }
126
127 pub fn update_time_step() -> Duration {
128 Duration::from_millis(25)
129 }
130
131 pub fn draw_time_step() -> Duration {
132 Duration::from_millis(50)
133 }
134
135 pub fn new() -> AppResult<Self> {
136 let mut mazes: [Maze; MAX_MAZE_ID] = (0..MAX_MAZE_ID)
137 .map(|maze_id| Maze::new(maze_id).build())
138 .collect::<AppResult<Vec<Maze>>>()?
139 .try_into()
140 .expect("MAX_MAZE_ID mismatch");
141
142 let mut minotaurs = HashMap::new();
143 let mut minotaur_rooms = [const { Vec::new() }; MAX_MAZE_ID];
144
145 for maze in mazes.iter_mut() {
146 let mut maze_minotaurs = vec![];
147 for index in 0..maze.id() {
148 let name = format!("{}#{}{}", random_minotaur_name(), maze.id(), index);
149 let minotaur = maze.spawn_minotaur(name);
150 maze_minotaurs.push(minotaur.id());
151 minotaurs.insert(minotaur.id(), minotaur);
152 }
153 minotaur_rooms[maze.id()] = maze_minotaurs;
154 }
155
156 Ok(Self {
157 mazes,
158 heros: HashMap::new(),
159 hero_rooms: [const { Vec::new() }; MAX_MAZE_ID],
160 taken_names: HashSet::new(),
161 top_heros_map: HashMap::new(),
162 top_heros: vec![],
163 minotaurs,
164 minotaur_rooms,
165 top_minotaurs_map: HashMap::new(),
166 top_minotaurs: vec![],
167 })
168 }
169
170 pub fn top_heros(&self) -> &Vec<(PlayerId, String, usize, Duration)> {
171 &self.top_heros
172 }
173
174 pub fn top_minotaurs(&self) -> &Vec<(PlayerId, String, usize, usize)> {
175 &self.top_minotaurs
176 }
177
178 pub fn minotaurs_in_maze(&self, maze_id: usize) -> usize {
179 self.minotaur_rooms[maze_id].len()
180 }
181
182 pub fn alarm_level(&self, hero_id: &PlayerId) -> (AlarmLevel, usize) {
183 if let Some(hero) = self.get_hero(hero_id) {
184 let maze_minotaurs = &self.minotaur_rooms[hero.maze_id()];
185 if !maze_minotaurs.is_empty() {
186 let mut alarm_level = AlarmLevel::NotChasing;
187 let mut min_distance = usize::MAX;
188
189 for minotaur_id in maze_minotaurs.iter() {
190 let minotaur = self.get_minotaur(minotaur_id).unwrap();
191 let distance = minotaur
192 .position()
193 .distance_squared(hero.position().into_direction(&hero.direction()));
194
195 if distance < min_distance {
196 min_distance = distance;
197 }
198
199 if minotaur.is_chasing(*hero_id) {
200 alarm_level = AlarmLevel::ChasingHero;
201 } else if minotaur.is_chasing_someone() && alarm_level < AlarmLevel::ChasingHero
202 {
203 alarm_level = AlarmLevel::ChasingOtherHero;
204 }
205 }
206
207 return (alarm_level, min_distance);
208 }
209 }
210 (AlarmLevel::NoMinotaurs, usize::MAX)
211 }
212
213 pub fn add_player(&mut self, player_id: PlayerId, name: &str) {
214 let rng = &mut rand::rng();
215 let mut player_name = to_player_name(rng, name);
216 while self.taken_names.contains(&player_name) {
217 player_name = to_player_name(rng, name);
218 }
219 self.taken_names.insert(player_name.clone());
220
221 let maze = &mut self.mazes[0];
222 let mut hero = Hero::new(player_id, player_name, maze.hero_starting_position());
223 maze.increase_attempted();
224
225 let visible_positions =
226 maze.get_and_cache_visible_positions(hero.position(), hero.direction(), hero.view());
227 hero.update_past_visible_positions(visible_positions);
228
229 self.hero_rooms[maze.id()].push(hero.id());
230
231 self.top_heros_map.insert(
232 hero.id(),
233 (
234 hero.name().to_string(),
235 0,
236 hero.elapsed_duration_from_start(),
237 ),
238 );
239
240 self.update_top_heros();
241
242 self.heros.insert(player_id, hero);
243 }
244
245 pub fn remove_player(&mut self, player_id: &PlayerId) {
246 self.heros.remove(player_id);
247 }
248
249 pub fn get_hero(&self, id: &PlayerId) -> Option<&Hero> {
250 self.heros.get(id)
251 }
252
253 pub fn get_minotaur(&self, id: &PlayerId) -> Option<&Minotaur> {
254 self.minotaurs.get(id)
255 }
256
257 pub fn get_maze(&self, id: usize) -> &Maze {
258 &self.mazes[id]
259 }
260
261 pub fn number_of_players(&self) -> usize {
262 self.heros.len()
263 }
264
265 pub fn update(&mut self) {
266 for hero in self.heros.values_mut() {
268 match hero.state {
269 HeroState::WaitingToStart | HeroState::InMaze { .. } => {}
270 HeroState::Dead { instant, .. } => {
271 if instant.elapsed() > Self::RESPAWN_INTERVAL {
272 self.hero_rooms[hero.maze_id()].retain(|id| *id != hero.id());
274 self.hero_rooms[0].push(hero.id());
275
276 let maze = &mut self.mazes[0];
277 hero.reset(maze.hero_starting_position());
278 let visible_positions = maze.get_and_cache_visible_positions(
279 hero.position(),
280 hero.direction(),
281 hero.view(),
282 );
283 hero.update_past_visible_positions(visible_positions);
284 }
285 }
286
287 HeroState::Victory { instant, .. } => {
288 if instant.elapsed() > Self::RESPAWN_INTERVAL {
289 self.hero_rooms[hero.maze_id()].retain(|id| *id != hero.id());
291 self.hero_rooms[0].push(hero.id());
292
293 let maze = &mut self.mazes[0];
294 hero.reset(maze.hero_starting_position());
295 let visible_positions = maze.get_and_cache_visible_positions(
296 hero.position(),
297 hero.direction(),
298 hero.view(),
299 );
300 hero.update_past_visible_positions(visible_positions);
301 }
302 }
303 }
304 }
305
306 let mut should_update_top_minotaurs = false;
308
309 for minotaur in self.minotaurs.values_mut() {
310 let maze_id = minotaur.maze_id();
311 let maze = &mut self.mazes[maze_id];
312
313 let visible_positions = maze.get_and_cache_visible_positions(
314 minotaur.position(),
315 minotaur.direction(),
316 minotaur.view(),
317 );
318
319 let visible_heros = self
320 .heros
321 .values()
322 .filter(|hero| {
323 !hero.is_dead()
324 && hero.maze_id() == maze_id
325 && visible_positions.contains(&hero.position())
326 })
327 .collect_vec();
328
329 minotaur.update(maze, visible_heros);
330
331 let catched_heros = self
332 .heros
333 .values()
334 .filter(|hero| {
335 hero.maze_id() == maze_id
336 && hero.position() == minotaur.position()
337 && !hero.is_dead()
338 })
339 .map(|hero| hero.id())
340 .collect_vec();
341
342 for hero_id in catched_heros.iter() {
343 if let Some(hero) = self.heros.get_mut(hero_id) {
344 if let HeroState::InMaze { instant } = hero.state {
345 hero.state = HeroState::Dead {
346 duration: instant.elapsed(),
347 instant: Instant::now(),
348 }
349 }
350 }
351 }
352
353 minotaur.kills += catched_heros.len();
354 self.top_minotaurs_map.insert(
355 minotaur.id(),
356 (
357 minotaur.name().to_string(),
358 minotaur.maze_id(),
359 minotaur.kills,
360 ),
361 );
362 should_update_top_minotaurs = true;
363 }
364
365 if should_update_top_minotaurs {
366 self.update_top_minotaurs();
367 }
368 }
369
370 pub fn image_char_overrides(
371 &self,
372 player_id: PlayerId,
373 image: &RgbaImage,
374 ) -> AppResult<HashMap<(u32, u32), char>> {
375 let hero = if let Some(hero) = self.get_hero(&player_id) {
376 hero
377 } else {
378 return Err(anyhow!("Missing hero {player_id}"));
379 };
380
381 let maze = &self.mazes[hero.maze_id()];
382
383 let visible_positions =
385 maze.get_cached_visible_positions(hero.position(), hero.direction(), hero.view());
386 let mut override_positions = visible_positions
387 .iter()
388 .filter(|(x, y)| {
389 is_transparent(
390 image.get_pixel(*x as u32, *y as u32),
391 &Maze::background_color(),
392 )
393 })
394 .map(|&(x, y)| ((x as u32, y as u32), '·'))
395 .collect::<HashMap<(u32, u32), char>>();
396
397 for &(x, y) in maze.entrance_positions().iter() {
398 if !visible_positions.contains(&(x, y)) {
399 continue;
400 }
401
402 if maze.id() > 0 {
403 for (idx, c) in (maze.id() + 1 - 1).to_string().chars().enumerate() {
404 override_positions.insert((x as u32 + idx as u32 + 1, y as u32), c);
405 }
406 override_positions.insert((x as u32, y as u32), '←');
407 }
408 }
409
410 for &(x, y) in maze.exit_positions().iter() {
411 if !visible_positions.contains(&(x, y)) {
412 continue;
413 }
414
415 for (idx, c) in (maze.id() + 1 + 1).to_string().chars().rev().enumerate() {
416 override_positions.insert((x as u32 - idx as u32 - 1, y as u32), c);
417 }
418 override_positions.insert((x as u32, y as u32), '→');
419 }
420
421 Ok(override_positions)
422 }
423
424 pub fn draw(&self, player_id: PlayerId) -> AppResult<RgbaImage> {
425 if let Some(hero) = self.heros.get(&player_id) {
426 let (x, y) = hero.position();
427 let maze_id = hero.maze_id();
428 let maze = &self.mazes[maze_id];
429
430 let maze_image = maze.image();
431
432 let mut player_image =
434 RgbaImage::from_pixel(maze_image.width(), maze_image.height(), Rgba([0; 4]));
435
436 let visible_positions =
437 maze.get_cached_visible_positions(hero.position(), hero.direction(), hero.view());
438
439 for (&(dx, dy), instant) in hero.past_visible_positions().iter() {
440 let base_color = maze_image.get_pixel(dx as u32, dy as u32);
442
443 let is_valid = maze.is_valid_position((dx, dy));
444
445 let base_alpha = if is_valid { 0 } else { 125 };
446 let mut alpha = if instant.elapsed() < hero.past_visibility_duration() {
447 base_alpha
448 - (base_alpha as f64 * instant.elapsed().as_millis() as f64
449 / hero.past_visibility_duration().as_millis() as f64)
450 as u8
451 } else {
452 0
453 };
454
455 if visible_positions.contains(&(dx, dy)) {
456 let distance = hero.position().distance((dx, dy));
458 alpha += ((255.0 - alpha as f64)
459 * (1.0 - distance / hero.view().radius() as f64))
460 as u8;
461 }
462
463 let pixel = Rgba([base_color[0], base_color[1], base_color[2], alpha]);
464 player_image.put_pixel(dx as u32, dy as u32, pixel);
465 }
466
467 for &(x, y) in maze.power_up_positions.iter() {
469 if !hero.power_up_collected_at(maze_id, (x, y))
470 && visible_positions.contains(&(x, y))
471 {
472 player_image.put_pixel(x as u32, y as u32, GameColors::POWER_UP);
473 }
474 }
475
476 for (p_id, any_hero) in self.heros.iter() {
478 if *p_id != player_id && any_hero.maze_id() == hero.maze_id() {
479 let (ax, ay) = any_hero.position();
480 if visible_positions.contains(&(ax, ay)) {
481 player_image.put_pixel(ax as u32, ay as u32, GameColors::OTHER_HERO);
482 }
483 }
484 }
485
486 let maze_minotaurs = &self.minotaur_rooms[hero.maze_id()];
488 for minotaur_id in maze_minotaurs.iter() {
489 if let Some(minotaur) = self.get_minotaur(minotaur_id) {
490 let (mx, my) = minotaur.position();
491 if visible_positions.contains(&(mx, my)) {
492 if minotaur.is_chasing(hero.id()) {
493 player_image.put_pixel(
494 mx as u32,
495 my as u32,
496 GameColors::CHASING_MINOTAUR,
497 );
498 } else {
499 player_image.put_pixel(mx as u32, my as u32, GameColors::MINOTAUR);
500 }
501 }
502 }
503 }
504
505 player_image.put_pixel(x as u32, y as u32, GameColors::HERO);
507
508 return Ok(player_image);
509 }
510 Err(anyhow!("No hero with id {player_id}"))
511 }
512
513 pub fn handle_command(&mut self, command: &GameCommand, hero_id: PlayerId) {
514 let hero = if let Some(hero) = self.heros.get_mut(&hero_id) {
515 hero
516 } else {
517 return;
518 };
519
520 if hero.state == HeroState::WaitingToStart {
521 hero.state = HeroState::InMaze {
522 instant: Instant::now(),
523 }
524 }
525
526 match hero.state {
527 HeroState::WaitingToStart => unreachable!(),
528 HeroState::InMaze { instant } => {
529 let maze_id = hero.maze_id();
530 match command {
531 GameCommand::Move { direction } => {
532 hero.update_past_visible_positions(
533 self.mazes[maze_id].get_and_cache_visible_positions(
534 hero.position(),
535 hero.direction(),
536 hero.view(),
537 ),
538 );
539
540 if *direction != hero.direction() {
541 hero.set_direction(*direction);
542 }
543
544 if !hero.can_move() {
545 hero.update_past_visible_positions(
546 self.mazes[maze_id].get_and_cache_visible_positions(
547 hero.position(),
548 hero.direction(),
549 hero.view(),
550 ),
551 );
552 return;
553 }
554
555 let (new_x, new_y) = hero.position().into_direction(direction);
556
557 if !self.mazes[maze_id].is_valid_position((new_x, new_y)) {
558 hero.update_past_visible_positions(
559 self.mazes[maze_id].get_and_cache_visible_positions(
560 hero.position(),
561 hero.direction(),
562 hero.view(),
563 ),
564 );
565 return;
566 }
567
568 hero.set_position((new_x, new_y));
569 for &position in self.mazes[maze_id].power_up_positions.iter() {
570 if position == hero.position()
571 && !hero.power_up_collected_at(hero.maze_id(), hero.position())
572 {
573 hero.apply_random_power_up_at_position(hero.position());
574 }
575 }
576
577 if self.mazes[maze_id].is_entrance_position(hero.position()) && maze_id > 0
579 {
580 let to = maze_id - 1;
581 self.mazes[maze_id].decrease_attempted();
582 self.mazes[to].decrease_passed();
583 hero.set_maze_id(to);
584
585 self.hero_rooms[maze_id].retain(|id| *id != hero.id());
587 self.hero_rooms[to].push(hero.id());
588
589 if hero.vision() == Hero::MAX_VISION {
591 hero.decrease_vision();
592 }
593
594 for (idx, entrance) in
595 self.mazes[maze_id].entrance_positions().iter().enumerate()
596 {
597 if hero.position() == *entrance {
598 hero.set_position(self.mazes[to].exit_positions()[idx]);
599 break;
600 }
601 }
602 } else if self.mazes[maze_id].is_exit_position(hero.position()) {
603 let to = maze_id + 1;
604 self.mazes[maze_id].increase_passed();
605
606 self.hero_rooms[maze_id].retain(|id| *id != hero.id());
608
609 if to == MAX_MAZE_ID {
610 hero.state = HeroState::Victory {
611 duration: instant.elapsed(),
612 instant: Instant::now(),
613 };
614 } else {
615 hero.set_maze_id(to);
616 self.hero_rooms[to].push(hero.id());
617 self.mazes[to].increase_attempted();
618
619 if hero.vision() == Hero::MAX_VISION {
621 hero.decrease_vision();
622 }
623
624 for (idx, exit) in
625 self.mazes[maze_id].exit_positions().iter().enumerate()
626 {
627 if hero.position() == *exit {
628 hero.set_position(self.mazes[to].entrance_positions()[idx]);
629 break;
630 }
631 }
632 }
633 }
634
635 hero.update_past_visible_positions(
636 self.mazes[hero.maze_id()].get_and_cache_visible_positions(
637 hero.position(),
638 hero.direction(),
639 hero.view(),
640 ),
641 );
642 }
643
644 GameCommand::TurnClockwise => {
645 hero.update_past_visible_positions(
646 self.mazes[maze_id].get_and_cache_visible_positions(
647 hero.position(),
648 hero.direction(),
649 hero.view(),
650 ),
651 );
652 hero.set_direction(hero.direction().rotate_clockwise());
653 hero.update_past_visible_positions(
654 self.mazes[maze_id].get_and_cache_visible_positions(
655 hero.position(),
656 hero.direction(),
657 hero.view(),
658 ),
659 );
660 }
661
662 GameCommand::TurnCounterClockwise => {
663 hero.update_past_visible_positions(
664 self.mazes[maze_id].get_and_cache_visible_positions(
665 hero.position(),
666 hero.direction(),
667 hero.view(),
668 ),
669 );
670 hero.set_direction(hero.direction().rotate_counter_clockwise());
671 hero.update_past_visible_positions(
672 self.mazes[maze_id].get_and_cache_visible_positions(
673 hero.position(),
674 hero.direction(),
675 hero.view(),
676 ),
677 );
678 }
679
680 GameCommand::CycleUiOptions => hero.cycle_ui_options(),
681 }
682 }
683 _ => {}
684 }
685
686 self.update_hero_record(hero_id);
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::{Game, MAX_MAZE_ID};
693 use crate::{game::utils::to_player_name, AppResult, PlayerId};
694 use rand::RngExt;
695 use std::time::Duration;
696
697 #[test]
698 fn test_top_heros() -> AppResult<()> {
699 let mut game = Game::new()?;
700
701 let rng = &mut rand::rng();
702
703 for _ in 0..100 {
704 game.top_heros_map.insert(
705 PlayerId::new_v4(),
706 (
707 to_player_name(rng, "name"),
708 rng.random_range(0..=MAX_MAZE_ID),
709 Duration::from_millis(rng.random_range(15000..150000)),
710 ),
711 );
712 }
713
714 game.update_top_heros();
715 for index in 0..game.top_heros.len() {
716 let (_, _, maze_id, timer) = game.top_heros[index];
717 let (_, _, next_maze_id, next_timer) = game.top_heros[index];
718
719 assert!(maze_id > next_maze_id || timer <= next_timer);
720 }
721
722 Ok(())
723 }
724}