rsnaker/graphics/
playing_render.rs

1use crate::game_logic::fruits_manager::FruitsManager;
2use crate::game_logic::state::{GameState, GameStatus};
3use crate::graphics::menus;
4use crate::graphics::sprites::map::Map;
5use crate::graphics::sprites::snake_body::SnakeBody;
6use ratatui::layout::Rect;
7use ratatui::widgets::Paragraph;
8use ratatui::{DefaultTerminal, Frame};
9use std::sync::{Arc, RwLock};
10use std::thread::sleep;
11use std::time::{Duration, Instant};
12
13///Position to render elements
14///will be clamped to the frame area border anyway, 9999 to go to the last line allowing easy resizing
15const BOTTOM_SPEED_FPS_SCORE_RECT: Rect = Rect::new(1, 9999, 70, 1);
16const LIFE_RECT: Rect = Rect::new(1, 0, 60, 1);
17const NB_OF_FRAMES_WINDOW: f64 = 1_000.0;
18const TOO_MUCH_LIVES_TO_DISPLAY: &str = " life: ❤️❤️❤️❤️❤️... ";
19/// # Panics                                                                                              
20/// if Arc panics while holding the resources (poisoning), no recovery mechanism implemented better crash  
21pub fn playing_render_loop<'a: 'b, 'b>(
22    carte: &Arc<RwLock<Map>>,
23    fruits_manager: &Arc<RwLock<FruitsManager<'a, 'b>>>,
24    state: &Arc<RwLock<GameState>>,
25    serpent: &Arc<RwLock<SnakeBody>>,
26    caps_fps: bool,
27    speed_effect: (u16, &str),
28    terminal: &mut DefaultTerminal,
29) {
30    //better to pre-format a string than doing it each time
31    let speed_text = format!("Speed: x{}{}", speed_effect.0, speed_effect.1);
32
33    //configure display variable with default value
34    let mut rendering_break = false;
35    let mut need_carte_resize = false;
36    let mut frame_count = 0f64;
37    let mut start_windows_time = Instant::now();
38    let mut start_frame_time: Instant;
39    let target_frame_time = Duration::from_secs_f64(1.0 / 60.0); // Target 60 FPS
40
41    //As quick as efficient as possible
42    //Avoid sub functions to limit arc clone, otherwise create a display structure
43    'render_loop: loop {
44        // for FPS stats
45        start_frame_time = Instant::now();
46        frame_count += 1.0;
47        //windows for frame calcul
48        if frame_count >= NB_OF_FRAMES_WINDOW {
49            frame_count = 1.0;
50            start_windows_time = Instant::now();
51        }
52        // start rendering game sprites
53        terminal
54            .draw(|frame| {
55                let area = frame.area();
56                //maps
57                {
58                    //sub scope to release the lock faster
59                    let map_guard = carte.read().unwrap();
60                    let area_map = map_guard.area();
61                    frame.render_widget(map_guard.get_widget(), *area_map);
62                    if area.height != area_map.height || area.width != area_map.width {
63                        need_carte_resize = true;
64                    }
65                }
66                //remember: cannot unlock in the same scope twice (even less write/read),
67                // so use boolean to limit the number of unlocking
68                if need_carte_resize {
69                    carte.write().unwrap().resize_to_terminal(area);
70                    fruits_manager.write().unwrap().reset_to_terminal_size();
71                    need_carte_resize = false;
72                }
73                //sub scope to release the lock faster
74                {
75                    //snake speed & FPS & Score
76                    frame.render_widget(
77                        Paragraph::new(format!(
78                            "{speed_text} | FPS: {} | Score: {} ",
79                            (frame_count / start_windows_time.elapsed().as_secs_f64()).floor(),
80                            state.read().unwrap().score
81                        )),
82                        BOTTOM_SPEED_FPS_SCORE_RECT.clamp(frame.area()),
83                    );
84                }
85                //sub scope to release the lock faster
86                {
87                    let state_guard = state.read().unwrap();
88                    //life
89                    let life = state_guard.life as usize;
90                    frame.render_widget(
91                        Paragraph::new(if life > 5 {
92                            TOO_MUCH_LIVES_TO_DISPLAY.to_string()
93                        } else {
94                            format!(" life: {} ", "❤️ ".repeat(life))
95                        }),
96                        LIFE_RECT.clamp(frame.area()),
97                    );
98                }
99                //Snake
100                // circle bad on not squared terminal => use emoji with position
101                {
102                    //NB: to have lighter code,we could implement Widget on custom Type wrapper
103                    //over RwLock using the NewType Pattern to overcome the Orphan Rule
104                    let snake_read = serpent.read().unwrap(); // Read lock
105                    frame.render_widget(&*snake_read, frame.area());
106                }
107                {
108                    //NB: to have lighter code, we could implement Widget on custom Type wrapper
109                    // over RwLock using the NewType Pattern to overcome the Orphan Rule
110                    let fruits_manager_read = fruits_manager.read().unwrap(); // Read lock
111                    frame.render_widget(&*fruits_manager_read, frame.area());
112                }
113
114                // And game_logic status
115                let state_guard = state.read().unwrap();
116                rendering_break = game_state_render(
117                    &state_guard.status,
118                    state_guard.score,
119                    state_guard.rank,
120                    frame,
121                );
122            })
123            .expect("bad rendering, check sprites position");
124        if rendering_break {
125            //let time for the user to see the farewell/menu screen
126            sleep(Duration::from_millis(1000));
127            //nice labeled loop :)
128            break 'render_loop;
129        }
130        //time to display the current frame
131        let frame_time = start_frame_time.elapsed();
132        // If you want to reduce CPU usage, maintain consistent frame timing
133        // If the frame generation takes longer than the target, no need to sleep (already sub 60 fps)
134        if caps_fps && frame_time < target_frame_time {
135            sleep(target_frame_time.saturating_sub(frame_time));
136        }
137    }
138}
139/// Return whether stop the rendering
140fn game_state_render(
141    state: &GameStatus,
142    score: u32,
143    rank: Option<usize>,
144    frame: &mut Frame,
145) -> bool {
146    let mut rendering_break = false;
147    match state {
148        GameStatus::Paused => {
149            menus::messages::pause_paragraph(frame);
150        }
151        GameStatus::GameOver(selection) => {
152            menus::messages::game_over_paragraph(frame, selection, score, rank);
153        }
154        GameStatus::ByeBye => {
155            menus::messages::byebye_paragraph(frame);
156            rendering_break = true;
157        }
158        GameStatus::Playing => (),
159        GameStatus::Restarting => {
160            menus::messages::restart_paragraph(frame);
161        }
162        GameStatus::Menu => {
163            menus::messages::menu_paragraph(frame);
164            rendering_break = true;
165        }
166    }
167    rendering_break
168}