rsnaker/graphics/menus/
greeting_menu.rs

1use crate::controls::input::GreetingOption;
2use crate::controls::speed;
3use crate::controls::speed::SpeedConfig;
4use crate::graphics::menus::layout_utils::frame_vertically_centered_rect;
5use crate::graphics::sprites::fruit::FRUITS_SCORES_PROBABILITIES;
6use clap::ValueEnum;
7use ratatui::style::Stylize;
8use ratatui::text::{Line, Span, Text};
9use ratatui::widgets::{Block, BorderType, Paragraph};
10use ratatui::{DefaultTerminal, Frame};
11use std::thread::sleep;
12use std::time::Duration;
13
14/// Print the welcome screen, reminding controls
15/// Show Fruit and Speed menus alongside
16/// Sadly `slow_blink` and `fast_blink` are rendered anymore on modern terminal...
17/// # Panics
18/// Will panic if no suitable terminal for displaying ios provided
19pub fn main_greeting_menu(
20    terminal: &mut DefaultTerminal,
21    opt: &Option<GreetingOption>,
22    selected: &GreetingOption,
23) {
24    //terminal.clear().expect("Clearing terminal fail ");
25    terminal
26        .draw(|frame| {
27            let area = frame.area();
28            match opt {
29                None | Some(GreetingOption::MainMenu) => {
30                    let lines = vec![
31                        Line::from("███████╗ ███╗   ██╗  █████╗  ██╗  ██╗ ███████╗"),
32                        Line::from("██╔════╝ ████╗  ██║ ██╔══██╗ ██║ ██╔╝ ██╔════╝"),
33                        Line::from("███████╗ ██╔██╗ ██║ ███████║ █████╔╝  █████╗  "),
34                        Line::from("╚════██║ ██║╚██╗██║ ██╔══██║ ██╔═██╗  ██╔══╝  "),
35                        Line::from("███████║ ██║ ╚████║ ██║  ██║ ██║  ██╗ ███████╗"),
36                        Line::from("╚══════╝ ╚══════╝╚═ ╝  ╚═══╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝"),
37                        Line::from(get_button_span(selected)),
38                        Line::from("+--------+------+------+-------+-----+-------+"),
39                        Line::from("|Controls| ←↕→  |  Q   | P / ⎵ |  M  |   R   | "),
40                        Line::from("+--------+------+------+-------+-----+-------+"),
41                        Line::from("|Effects | Move | Quit | Pause | Menu| Start |"),
42                        Line::from("+--------+------+------+-------+-----+-------+"),
43                        Line::from("Welcome to the craziest Snake ever! 🐍".green()),
44                        Line::from("Have a good game !  🎮".green()),
45                    ];
46                    let nb_lines = lines.len();
47
48                    frame.render_widget(
49                        //centered horizontally
50                        Paragraph::new(Text::from(lines)).centered(),
51                        frame_vertically_centered_rect(area, nb_lines),
52                    );
53                }
54                Some(GreetingOption::Fruits) => {
55                    fruit_menu(frame, selected);
56                }
57                Some(GreetingOption::Velocity) => {
58                    speed_menu(frame, selected);
59                }
60                Some(GreetingOption::StartGame | GreetingOption::QuitGame) => {
61                    //Log Game is started/quit :p
62                }
63                Some(GreetingOption::Parameters) => {
64                    parameters_menu(frame, selected);
65                }
66                Some(GreetingOption::Help) => {
67                    help_menu(frame, selected);
68                }
69                Some(GreetingOption::Enter | GreetingOption::Next | GreetingOption::Previous) => {
70                    let lines = vec![
71                        Line::from(
72                            "You find an easter egg here! Not yet playable, please report\
73                          Guess some edge cases are possible :p ",
74                        ),
75                        Line::from(get_button_span(selected)),
76                    ];
77                    let nb_lines = lines.len();
78                    frame.render_widget(
79                        Paragraph::new(Text::from(lines)).centered(),
80                        frame_vertically_centered_rect(area, nb_lines),
81                    );
82                }
83            }
84            //buttons_menu(frame, &app);
85            //set a border all around the terminal
86            frame.render_widget(Block::bordered().border_type(BorderType::Double), area);
87        })
88        .expect("Unusable terminal render");
89    sleep(Duration::from_millis(100));
90}
91
92/// Display the fruit menu, vertically centered
93fn fruit_menu(frame: &mut Frame, selected: &GreetingOption) {
94    //adding fruits rules, gonna left aligned for less screen space use
95    let mut fruits_lines = Vec::new();
96    let tab_jonction = Line::from(" +---------+-------+-------------+-------------+");
97    fruits_lines.push(tab_jonction.clone());
98    fruits_lines.push(Line::from(
99        "| Fruit   | Score | Probability | Size Effect |",
100    ));
101    fruits_lines.push(tab_jonction.clone());
102    for (fruit, score, probability, size_effect) in FRUITS_SCORES_PROBABILITIES {
103        //:<6 and so on are formating options, e.g., saying aligning left with min 6 chars
104        fruits_lines.push(Line::from(format!(
105            " | {fruit:<6} | {score:>5} | {probability:>11} | {size_effect:>11} |"
106        )));
107    }
108    fruits_lines.push(tab_jonction);
109    fruits_lines.push(Line::from(get_button_span(selected)));
110    let nb_lines = fruits_lines.len();
111    //more idiomatic using lines (as in: https://ratatui.rs/recipes/render/display-text/)
112    //can mix style inside the same line using Line::from(vec!["hello".green(),
113    // " ".into(), "world".green().bold(), "3".into()]),
114    frame.render_widget(
115        Paragraph::new(Text::from(fruits_lines)).centered(),
116        frame_vertically_centered_rect(frame.area(), nb_lines),
117    );
118}
119/// Display the speed menu center aligned, vertically centered
120fn speed_menu(frame: &mut Frame, selected: &GreetingOption) {
121    // Speed effects
122    let mut speed_lines = Vec::new();
123    let speed_tab_jonction = Line::from("+------------+-------+----------------+---------+ ");
124    speed_lines.push(speed_tab_jonction.clone());
125    speed_lines.push(Line::from(
126        "| Speed Name | Value | Score Modifier | Symbol  | ",
127    ));
128    speed_lines.push(speed_tab_jonction.clone());
129    for s in speed::Speed::value_variants() {
130        let SpeedConfig {
131            name,
132            ms_value,
133            score_modifier,
134            symbol,
135        } = s.config();
136        //:<10 and so on are formating options, e.g., saying aligning left with min 10 chars
137        speed_lines.push(Line::from(format!(
138            "| {name:<10} | {ms_value:>5} | {score_modifier:>14} | {symbol:<6} | "
139        )));
140    }
141    speed_lines.push(speed_tab_jonction);
142    speed_lines.push(Line::from(get_button_span(selected)));
143    let nb_lines = speed_lines.len();
144    frame.render_widget(
145        Paragraph::new(Text::from(speed_lines)).centered(),
146        frame_vertically_centered_rect(frame.area(), nb_lines),
147    );
148}
149/// Display the parameters menu center aligned, vertically centered
150fn parameters_menu(frame: &mut Frame, selected: &GreetingOption) {
151    let lines = vec![
152        Line::from("Parameters are not yet implemented 😜"),
153        Line::from("For now use the command line parameter or load a parameters file"),
154        Line::from("Upvote this issue for faster release or bring me a coffee on kofi🥤 "),
155        Line::from(get_button_span(selected)),
156    ];
157    let nb_lines = lines.len();
158    frame.render_widget(
159        Paragraph::new(Text::from(lines)).centered(),
160        frame_vertically_centered_rect(frame.area(), nb_lines),
161    );
162}
163fn help_menu(frame: &mut Frame, selected: &GreetingOption) {
164    //formating reminder: Where:
165    // - `<10` means left-aligned with width 10
166    // - `>5` means right-aligned with width 5
167    let lines = vec![
168        Line::from("Snake Game Rules:".bold().yellow()),
169        Line::from(""),
170        Line::from(format!(
171            "• {:<80}",
172            "Control the snake using the arrow keys (←↕→)"
173        )),
174        Line::from(format!(
175            "• {:<80}",
176            "Eat fruits to grow longer and score points"
177        )),
178        Line::from(format!(
179            "• {:<80}",
180            "Different fruits give various scores / effects, some even reduce size (and score)"
181        )),
182        Line::from(format!(
183            "• {:<80}",
184            "Avoid hitting yourself or your own tail"
185        )),
186        Line::from(format!(
187            "• {:<80}",
188            "Walls are circulars so your head will appear on the other side of the screen"
189        )),
190        Line::from(format!(
191            "• {:<80}",
192            "Game speeds can be changed to increase difficulty and score multipliers"
193        )),
194        Line::from(format!("• {:<80}", "Press P or Space to pause the game")),
195        Line::from(format!("• {:<80}", "Press Q to quit anytime")),
196        Line::from(format!("• {:<80}", "Press R to start a new game")),
197        Line::from(format!("• {:<80}", "Press M to return to the menu")),
198        Line::from(""),
199        Line::from(format!(
200            "{:<80}",
201            "Check the Fruit and Speed menus for more details on game mechanics!"
202        )),
203        Line::from(""),
204        Line::from(format!(
205            "{:<80}",
206            "If you enjoy this game consider bringing me a coffee on kofi🥤 "
207        )),
208        Line::from(get_button_span(selected)),
209    ];
210
211    let nb_lines = lines.len();
212    frame.render_widget(
213        Paragraph::new(Text::from(lines)).centered(),
214        frame_vertically_centered_rect(frame.area(), nb_lines),
215    );
216}
217
218fn get_button_span(selected: &GreetingOption) -> Vec<Span> {
219    let selected_main: Vec<Span> = vec![" [ ".red(), "Main".into(), " ] ".red().bold()];
220    let default_main: Vec<Span> = vec![" [".into(), "M".yellow(), "ain]".into()];
221    let selected_fruit: Vec<Span> = vec![" [ ".red(), "Fruit".into(), " ] ".red().bold()];
222    let default_fruit: Vec<Span> = vec![" [".into(), "F".yellow(), "ruit]".into()];
223    let selected_velocity: Vec<Span> = vec![" [ ".red(), "Speed".into(), " ] ".red().bold()];
224    let default_velocity: Vec<Span> = vec![" [".into(), "S".yellow(), "peed]".into()];
225    let selected_start: Vec<Span> = vec![" [ ".red(), "Run".into(), " ] ".red().bold()];
226    let default_start: Vec<Span> = vec![" [".into(), "R".yellow(), "un]".into()];
227    let selected_setup: Vec<Span> = vec![" [ ".red(), "Edit⚙️".into(), " ] ".red().bold()];
228    let default_setup: Vec<Span> = vec![" [".into(), "E".yellow(), "dit⚙️]".into()];
229    let selected_help: Vec<Span> = vec![" [ ".red(), "Help".into(), " ] ".red().bold()];
230    let default_help: Vec<Span> = vec![" [".into(), "H".yellow(), "elp]".into()];
231    let mut vec_line_button = vec!["↔".into()];
232    if selected == &GreetingOption::MainMenu {
233        vec_line_button.extend(selected_main);
234    } else {
235        vec_line_button.extend(default_main);
236    }
237    if selected == &GreetingOption::Fruits {
238        vec_line_button.extend(selected_fruit);
239    } else {
240        vec_line_button.extend(default_fruit);
241    }
242    if selected == &GreetingOption::Velocity {
243        vec_line_button.extend(selected_velocity);
244    } else {
245        vec_line_button.extend(default_velocity);
246    }
247    if selected == &GreetingOption::StartGame {
248        vec_line_button.extend(selected_start);
249    } else {
250        vec_line_button.extend(default_start);
251    }
252    if selected == &GreetingOption::Parameters {
253        vec_line_button.extend(selected_setup);
254    } else {
255        vec_line_button.extend(default_setup);
256    }
257    if selected == &GreetingOption::Help {
258        vec_line_button.extend(selected_help);
259    } else {
260        vec_line_button.extend(default_help);
261    }
262    vec_line_button
263}