Skip to main content

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