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    uncaps_fps: bool,
27    speed_symbol: &str,
28    terminal: &mut DefaultTerminal,
29) {
30    //better to pre-format a string than doing it each time
31    let speed_text = format!("Speed: {speed_symbol}");
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().resize_to_terminal();
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                rendering_break = game_state_render(&state.read().unwrap().status, frame);
116            })
117            .expect("bad rendering, check sprites position");
118        if rendering_break {
119            //let time for the user to see the farewell/menu screen
120            sleep(Duration::from_millis(1000));
121            //nice labeled loop :)
122            break 'render_loop;
123        }
124        //time to display the current frame
125        let frame_time = start_frame_time.elapsed();
126        // If you want to reduce CPU usage, maintain consistent frame timing
127        // If the frame generation takes longer than the target, no need to sleep (already sub 60 fps)
128        if !uncaps_fps && frame_time < target_frame_time {
129            sleep(target_frame_time.saturating_sub(frame_time));
130        }
131    }
132}
133/// Return whether stop the rendering
134fn game_state_render(state: &GameStatus, frame: &mut Frame) -> bool {
135    let mut rendering_break = false;
136    match state {
137        GameStatus::Paused => {
138            menus::status::pause_paragraph(frame);
139        }
140        GameStatus::GameOver => {
141            menus::status::game_over_paragraph(frame);
142        }
143        GameStatus::ByeBye => {
144            menus::status::byebye_paragraph(frame);
145            rendering_break = true;
146        }
147        GameStatus::Playing => (),
148        GameStatus::Restarting => {
149            menus::status::restart_paragraph(frame);
150        }
151        GameStatus::Menu => {
152            menus::status::menu_paragraph(frame);
153            rendering_break = true;
154        }
155    }
156    rendering_break
157}