Skip to main content

rsnaker/game_logic/
playing_logic.rs

1use crate::controls::direction::Direction;
2use crate::controls::speed::Speed;
3use crate::game_logic::fruits_manager::FruitsManager;
4use crate::game_logic::high_score::{HighScore, HighScoreManager};
5use crate::game_logic::state::{GameOverMenu, GameState, GameStatus};
6use crate::graphics::sprites::fruit::Fruit;
7use crate::graphics::sprites::map::Map;
8use crate::graphics::sprites::snake_body::SnakeBody;
9use std::sync::{Arc, RwLock};
10use std::thread::sleep;
11use std::time::{Duration, Instant};
12use tracing::{debug, error, info, trace, trace_span, warn};
13
14/// # Panics
15/// if Arc panics while holding the resources (poisoning),
16/// no recovery mechanism implemented better crash  
17pub fn playing_logic_loop(
18    direction: &Arc<RwLock<Direction>>,
19    snake: &Arc<RwLock<SnakeBody>>,
20    gs: &Arc<RwLock<GameState>>,
21    carte: &Arc<RwLock<Map>>,
22    fruits_manager: &Arc<RwLock<FruitsManager>>,
23    (game_speed, snake_symbols, negative_size_fruits, snake_growth_factor): (
24        Speed,
25        String,
26        bool,
27        u16,
28    ),
29) {
30    let mut gsc;
31    debug!(fruits = ?fruits_manager.read().unwrap().get_fruits(), "Initial fruits on map");
32    loop {
33        let _span = trace_span!("logic_iteration").entered();
34        let loop_start = Instant::now();
35        //do not want to keep the lock for too long + cannot hold in the same thread 2 times the same hold
36        // so match a clone or use a let
37        gsc = gs.read().unwrap().status.clone();
38        //dead snakes tell no tales, nor move :p
39        match gsc {
40            GameStatus::Playing => {
41                let mut write_guard = snake.write().unwrap();
42                //Move the snake!
43                let position = write_guard.ramp(&direction.read().unwrap(), &carte.read().unwrap());
44                debug!(?position, "Snake moved");
45                // Check if we eat a fruit
46                let fruits = fruits_manager.read().unwrap().eat_some_fruits(position);
47                // fruits effects
48                if let Some(fruits) = fruits {
49                    debug!(count = fruits.len(), "Fruits eaten by snake");
50                    let score_fruits = fruits.iter().map(Fruit::get_score).sum::<i32>();
51                    let base_size_effect = fruits
52                        .iter()
53                        .map(Fruit::get_grow_snake)
54                        .map(i32::from)
55                        .sum::<i32>();
56                    let size_effect = i16::try_from(
57                        base_size_effect
58                            .saturating_mul(i32::from(snake_growth_factor))
59                            .clamp(i32::from(i16::MIN), i32::from(i16::MAX)),
60                    )
61                    .unwrap();
62                    info!(?fruits, "Fruit characteristics");
63                    // In classic-like mode, negative fruits are disabled.
64                    if negative_size_fruits || size_effect > 0i16 {
65                        write_guard.relative_size_change(size_effect);
66                    }
67                    //NB:Converting an u16 to an i32 is always safe in Rust because the range of u16 (0 to 65,535)
68                    // fits entirely within the range of i32 (−2,147,483,648 to 2,147,483,647).
69                    //So no need to do: speed_score_modifier.try_into().expect("too much")/match for conversion
70                    let fruit_impact = score_fruits * i32::from(game_speed.score_modifier());
71                    info!(
72                        fruit_impact = fruit_impact,
73                        "Fruit impact on score because of speed score modifier"
74                    );
75                    let mut state_guard_gs = gs.write().unwrap();
76                    // to always keep the score positive and between bounds
77                    if fruit_impact < 0 {
78                        state_guard_gs.score = state_guard_gs
79                            .score
80                            .saturating_sub(fruit_impact.unsigned_abs());
81                    } else {
82                        state_guard_gs.score = state_guard_gs
83                            .score
84                            .saturating_add(fruit_impact.unsigned_abs());
85                    }
86                    info!(
87                        score = state_guard_gs.score,
88                        snake_size = write_guard.body.len(),
89                        "Current game metrics"
90                    );
91                    fruits_manager.write().unwrap().replace_fruits(&fruits);
92                    debug!(fruits = ?fruits_manager.read().unwrap().get_fruits(), "Current fruits on map");
93                }
94                // Check if the gamer will lose one life
95                if write_guard.is_snake_eating_itself() {
96                    //Ouch. You bite yourself
97                    let mut state_guard = gs.write().unwrap();
98                    if (state_guard.life) >= 1 {
99                        state_guard.life -= 1;
100                        warn!(
101                            remaining_life = state_guard.life,
102                            "Ouch! Did that taste good? You bit yourself!",
103                        );
104                    }
105                    if state_guard.life == 0 {
106                        error!(
107                            score = state_guard.score,
108                            "FATAL: Snake succumbed to its injuries. Game Over!"
109                        );
110                        //The GameOverMenu option will be used to store the user selection of what to do
111                        state_guard.status = GameStatus::GameOver(GameOverMenu::default());
112
113                        // Save High Score
114                        if let Ok(manager) = HighScoreManager::new() {
115                            let score_value: u32 = state_guard.score;
116                            let rank = manager.save_score(&HighScore::new(
117                                snake_symbols.clone(),
118                                score_value,
119                                //using Display implementation
120                                game_speed.to_string(),
121                                snake_growth_factor,
122                            ));
123                            state_guard.rank = rank.unwrap_or(None);
124                        }
125                    }
126                }
127            }
128            GameStatus::Restarting => {
129                //let some time for the restarting screen to appear
130                sleep(Duration::from_secs(1));
131                gs.write().unwrap().reset();
132                snake.write().unwrap().reset();
133                *direction.write().unwrap() = Direction::Right;
134                fruits_manager.write().unwrap().reset();
135                //graphical resize on rendering part (not really a game_logic constant)
136            }
137            GameStatus::ByeBye | GameStatus::Menu => break,
138            GameStatus::Paused | GameStatus::GameOver(_) => {}
139        }
140        let elapsed = loop_start.elapsed();
141        trace!(?elapsed, "Logic loop iteration finished");
142        sleep(Duration::from_millis(game_speed.ms_value()));
143    }
144}