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                    // write because swap value of counter and renew fruits if expired
113                    let mut fruits_manager_write = fruits_manager.write().unwrap(); // Read lock
114                    fruits_manager_write.update();
115                    frame.render_widget(&*fruits_manager_write, frame.area());
116
117                    // And game_logic status
118                    let state_guard = state.read().unwrap();
119                    rendering_break = game_state_render(
120                        &state_guard.status,
121                        &mut fruits_manager_write,
122                        state_guard.score,
123                        state_guard.rank,
124                        frame,
125                    );
126                }
127            })
128            .expect("bad rendering, check sprites position");
129        if rendering_break {
130            //let time for the user to see the farewell/menu screen
131            sleep(Duration::from_secs(1));
132            //nice labeled loop :)
133            break 'render_loop;
134        }
135        //time to display the current frame
136        let frame_time = start_frame_time.elapsed();
137        // If you want to reduce CPU usage, maintain consistent frame timing
138        // If the frame generation takes longer than the target, no need to sleep (already sub 60 fps)
139        if caps_fps && frame_time < target_frame_time {
140            sleep(target_frame_time.saturating_sub(frame_time));
141        }
142        trace!(?frame_time, "Frame rendered");
143    }
144}
145/// Return whether stop the rendering
146fn game_state_render(
147    state: &GameStatus,
148    fruits_manager: &mut FruitsManager,
149    score: u32,
150    rank: Option<usize>,
151    frame: &mut Frame,
152) -> bool {
153    let mut rendering_break = false;
154    match state {
155        GameStatus::Paused => {
156            // this little logic part must be there (graphic related), because only the draw thread is fast enough
157            // as snake speed elsewhere speeds the game logic
158            fruits_manager.set_timer_paused(true);
159            menus::messages::pause_paragraph(frame);
160        }
161        GameStatus::GameOver(selection) => {
162            fruits_manager.set_timer_paused(true);
163            menus::messages::game_over_paragraph(frame, selection, score, rank);
164        }
165        GameStatus::ByeBye => {
166            menus::messages::byebye_paragraph(frame);
167            rendering_break = true;
168        }
169        GameStatus::Playing => {
170            fruits_manager.set_timer_paused(false);
171        }
172        GameStatus::Restarting => {
173            fruits_manager.set_timer_paused(true);
174            menus::messages::restart_paragraph(frame);
175        }
176        GameStatus::Menu => {
177            menus::messages::menu_paragraph(frame);
178            rendering_break = true;
179        }
180    }
181    rendering_break
182}