Skip to main content

asterion_core/
game.rs

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        // Equal maze id record --> compare timer
63        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        // Update heros
267        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                        // Move hero between rooms
273                        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                        // Move hero between rooms
290                        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        // Update minotaurs
307        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        // Override empty positions.
384        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            // The base player image is black.
433            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                // Each position in the past_visible_positions is copied from the maze_image, with alpha channel depending on the time passed.
441                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                    // Each position in the visible_positions is copied from the maze_image, with alpha channel depending on the distance from the hero.
457                    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            // Add powerup position
468            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            // Add other heros position
477            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            // Add minotaurs position
487            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            // Add hero position
506            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                        // Transition between rooms
578                        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                            // Move hero between rooms
586                            self.hero_rooms[maze_id].retain(|id| *id != hero.id());
587                            self.hero_rooms[to].push(hero.id());
588
589                            // If hero acquired max vision in this maze, reduce it by one.
590                            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                            // Move hero between rooms
607                            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 acquired max vision in this maze, reduce it by one.
620                                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}