mod menus;
use std::{
fmt::Debug,
fs::File,
io::{self, Read, Write},
num::{NonZeroU32, NonZeroUsize},
path::PathBuf,
time::Duration,
};
use crossterm::{
cursor::{self, MoveTo},
event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
terminal::{self, Clear, ClearType},
ExecutableCommand,
};
use falling_tetromino_engine::{
Board, Button, DelayParameters, ExtDuration, Game, GameBuilder, GameEndCause, InGameTime,
Input, Notification, NotificationFeed, NotificationLevel, Stat, Tetromino,
};
use crate::{
application::menus::{Menu, MenuUpdate},
game_modes::{self, game_modifiers, GameMode},
gameplay_settings::*,
graphics_settings::*,
keybinds::*,
palette::*,
};
pub type Slots<T> = Vec<(String, T)>;
pub type UncompressedInputHistory = Vec<(InGameTime, Input)>;
#[derive(
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub struct CompressedInputHistory(Vec<u128>);
impl CompressedInputHistory {
pub const BUTTON_CHANGE_BITSIZE: usize =
1 + Button::VARIANTS.len().next_power_of_two().ilog2() as usize;
pub fn new(game_input_history: &UncompressedInputHistory) -> Self {
let mut compressed_inputs = Vec::new();
if let Some((mut update_time_0, button_change)) = game_input_history.first() {
let i = Self::compress_input((update_time_0, *button_change));
compressed_inputs.push(i);
for (update_time_1, button_change) in game_input_history.iter().skip(1) {
let time_diff = update_time_1.saturating_sub(update_time_0);
let i = Self::compress_input((time_diff, *button_change));
compressed_inputs.push(i);
update_time_0 = *update_time_1;
}
};
Self(compressed_inputs)
}
pub fn decompress(&self) -> UncompressedInputHistory {
let mut decompressed_inputs = Vec::new();
if let Some(i) = self.0.first() {
let (mut update_time_0, button_change) = Self::decompress_input(*i);
decompressed_inputs.push((update_time_0, button_change));
for i in self.0.iter().skip(1) {
let (time_diff, button_change) = Self::decompress_input(*i);
let update_time_1 = update_time_0.saturating_add(time_diff);
decompressed_inputs.push((update_time_1, button_change));
update_time_0 = update_time_1;
}
}
decompressed_inputs
}
fn compress_input((update_target_time, button_change): (InGameTime, Input)) -> u128 {
let millis: u128 = update_target_time.as_millis();
let bc_bits: u8 = Self::compress_buttonchange(&button_change);
(millis << Self::BUTTON_CHANGE_BITSIZE) | u128::from(bc_bits)
}
fn decompress_input(i: u128) -> (InGameTime, Input) {
let mask = u128::MAX >> (128 - Self::BUTTON_CHANGE_BITSIZE);
let bc_bits = u8::try_from(i & mask).unwrap();
let millis = u64::try_from(i >> Self::BUTTON_CHANGE_BITSIZE).unwrap();
(
std::time::Duration::from_millis(millis),
Self::decompress_buttonchange(bc_bits),
)
}
fn compress_buttonchange(button_change: &Input) -> u8 {
match button_change {
Input::Deactivate(button) => (*button as u8) << 1,
Input::Activate(button) => ((*button as u8) << 1) | 1,
}
}
fn decompress_buttonchange(b: u8) -> Input {
(if b.is_multiple_of(2) {
Input::Deactivate
} else {
Input::Activate
})(Button::VARIANTS[usize::from(b >> 1)])
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameRestorationData<T> {
builder: GameBuilder,
mod_ids_args: Vec<(String, String)>,
input_history: T,
forfeit: Option<InGameTime>,
}
impl<T> GameRestorationData<T> {
fn new(game: &Game, input_history: T, forfeit: Option<InGameTime>) -> GameRestorationData<T> {
let (builder, mod_ids_args) = game.blueprint();
GameRestorationData {
builder,
mod_ids_args,
input_history,
forfeit,
}
}
fn map<U>(self, f: impl Fn(T) -> U) -> GameRestorationData<U> {
GameRestorationData::<U> {
builder: self.builder,
mod_ids_args: self.mod_ids_args,
input_history: f(self.input_history),
forfeit: self.forfeit,
}
}
}
impl GameRestorationData<UncompressedInputHistory> {
fn restore(&self, input_index: usize) -> Game {
let builder = self.builder.clone();
let mut game = if self.mod_ids_args.is_empty() {
builder.build()
} else {
match game_modes::game_modifiers::reconstruct_build_modded(&builder, &self.mod_ids_args)
{
Ok((mut modded_game, unrecognized_mod_ids)) => {
if !unrecognized_mod_ids.is_empty() {
let warn_messages = unrecognized_mod_ids
.into_iter()
.map(|mod_desc| format!("WARNING: idk mod {mod_desc:?}"))
.collect();
let print_warn_msgs_mod =
game_modifiers::PrintMsgs::modifier(warn_messages);
modded_game.modifiers.push(print_warn_msgs_mod);
}
modded_game
}
Err(msg) => {
let error_messages = vec![format!("ERROR: {msg}")];
let print_error_msg_mod = game_modifiers::PrintMsgs::modifier(error_messages);
builder.build_modded(vec![print_error_msg_mod])
}
}
};
let restore_notification_level = game.config.notification_level;
game.config.notification_level = NotificationLevel::Silent;
for (update_time, button_change) in self.input_history.iter().take(input_index) {
let _v = game.update(*update_time, Some(*button_change));
}
game.config.notification_level = restore_notification_level;
game
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameMetaData {
pub datetime: String,
pub title: String,
pub comparison_stat: (Stat, bool),
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameSave<T> {
game_meta_data: GameMetaData,
game_restoration_data: GameRestorationData<T>,
inputs_to_load: usize,
}
impl<T> GameSave<T> {
fn map<U>(self, f: impl Fn(T) -> U) -> GameSave<U> {
GameSave {
game_restoration_data: self.game_restoration_data.map(f),
game_meta_data: self.game_meta_data,
inputs_to_load: self.inputs_to_load,
}
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct ScoresEntry {
game_meta_data: GameMetaData,
end_cause: GameEndCause,
is_win: bool,
time_elapsed: InGameTime,
lineclears: u32,
points_scored: u32,
pieces_locked: [u32; Tetromino::VARIANTS.len()],
fall_delay_reached: ExtDuration,
lock_delay_reached: Option<ExtDuration>,
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum ScoresSorting {
Chronological,
Scoring,
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct ScoresAndReplays {
sorting: ScoresSorting,
entries: Vec<(
ScoresEntry,
Option<GameRestorationData<CompressedInputHistory>>,
)>,
}
impl Default for ScoresAndReplays {
fn default() -> Self {
Self {
sorting: ScoresSorting::Scoring,
entries: Vec::new(),
}
}
}
#[derive(
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub struct Statistics {
total_new_games_started: u32,
total_games_ended: u32,
total_play_time: Duration,
total_pieces_locked: u32,
total_points_scored: u32,
total_lines_cleared: u32,
total_mono: u32,
total_duo: u32,
total_tri: u32,
total_tetra: u32,
total_spin: u32,
total_perfect_clear: u32,
total_combo: u32,
}
impl Statistics {
const BLACKLIST_TITLE_PREFIXES: &[&str] = &[GameMode::TITLE_PUZZLE, GameMode::TITLE_COMBO];
fn accumulate_from_feed(&mut self, feed: &NotificationFeed) {
for (notification, _notif_time) in feed {
match notification {
Notification::PieceLocked { .. } => {
self.total_pieces_locked += 1;
}
Notification::Accolade {
score_bonus,
lineclears,
combo,
is_spin,
is_perfect_clear,
tetromino: _,
} => {
self.total_points_scored += score_bonus;
self.total_lines_cleared += lineclears;
match lineclears {
1 => self.total_mono += 1,
2 => self.total_duo += 1,
3 => self.total_tri += 1,
4 => self.total_tetra += 1,
_ => {}
}
self.total_spin += if *is_spin { 1 } else { 0 };
self.total_perfect_clear += if *is_perfect_clear { 1 } else { 0 };
self.total_combo += if *combo > 1 { 1 } else { 0 };
}
_ => {}
}
}
}
fn accumulate(&mut self, other: &Statistics) {
let Statistics {
total_new_games_started,
total_games_ended,
total_play_time,
total_pieces_locked,
total_points_scored,
total_lines_cleared,
total_mono,
total_duo,
total_tri,
total_tetra,
total_spin,
total_perfect_clear,
total_combo,
} = self;
*total_new_games_started += other.total_new_games_started;
*total_games_ended += other.total_games_ended;
*total_play_time += other.total_play_time;
*total_pieces_locked += other.total_pieces_locked;
*total_points_scored += other.total_points_scored;
*total_lines_cleared += other.total_lines_cleared;
*total_mono += other.total_mono;
*total_duo += other.total_duo;
*total_tri += other.total_tri;
*total_tetra += other.total_tetra;
*total_spin += other.total_spin;
*total_perfect_clear += other.total_perfect_clear;
*total_combo += other.total_combo;
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct NewGameSettings {
custom_fall_delay_params: DelayParameters,
custom_win_condition: Option<Stat>,
custom_seed: Option<u64>,
custom_encoded_board: Option<String>,
cheese_tiles_per_line: NonZeroUsize,
cheese_fall_lock_delays: (ExtDuration, ExtDuration),
cheese_limit: Option<NonZeroU32>,
combo_limit: Option<NonZeroU32>,
combo_initial_layout: u16,
master_mode_unlocked: bool,
experimental_mode_unlocked: bool,
}
impl Default for NewGameSettings {
fn default() -> Self {
Self {
custom_fall_delay_params: DelayParameters::standard_fall(),
custom_win_condition: None,
custom_seed: None,
custom_encoded_board: None,
cheese_limit: Some(NonZeroU32::try_from(20).unwrap()),
cheese_fall_lock_delays: (ExtDuration::Infinite, ExtDuration::Infinite),
cheese_tiles_per_line: NonZeroUsize::new(Game::WIDTH - 1).unwrap(),
combo_limit: Some(NonZeroU32::try_from(30).unwrap()),
combo_initial_layout: game_modifiers::Combo::LAYOUTS[0],
master_mode_unlocked: false,
experimental_mode_unlocked: false,
}
}
}
impl NewGameSettings {
#[allow(dead_code)]
pub fn encode_board(board: &Board) -> String {
board
.iter()
.map(|line| {
line.iter()
.map(|tile| if tile.is_some() { 'X' } else { ' ' })
.collect::<String>()
})
.collect::<String>()
.trim_end()
.to_owned()
}
pub fn decode_board(board_str: &str) -> Board {
let grey_tile = Some(std::num::NonZeroU8::try_from(254).unwrap());
let mut new_board = Board::default();
let mut chars = board_str.chars();
for line in &mut new_board {
for tile in line {
for char in chars.by_ref() {
if char == ' ' {
*tile = None;
break;
} else if char == '\n' {
continue;
} else {
*tile = grey_tile;
break;
}
}
}
}
new_board
}
}
#[serde_with::serde_as]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Settings {
graphics_slot_active: usize,
keybinds_slot_active: usize,
gameplay_slot_active: usize,
graphics_slots_that_should_not_be_changed: usize,
palette_slots_that_should_not_be_changed: usize,
keybinds_slots_that_should_not_be_changed: usize,
gameplay_slots_that_should_not_be_changed: usize,
graphics_slots: Slots<GraphicsSettings>,
palette_slots: Slots<Palette>,
gameplay_slots: Slots<GameplaySettings>,
#[serde_as(as = "Vec<(_, Vec<(_, _)>)>")]
keybinds_slots: Slots<Keybinds>,
new_game: NewGameSettings,
}
impl Default for Settings {
fn default() -> Self {
let graphics_slots = vec![
("Default".to_owned(), GraphicsSettings::default()),
("Focus+".to_owned(), GraphicsSettings::extra_focus()),
("Guideline".to_owned(), GraphicsSettings::guideline()),
("High Compat.".to_owned(), GraphicsSettings::compatibility()),
(
"Elektronika 60".to_owned(),
GraphicsSettings::elektronika_60(),
),
];
let palette_slots = vec![
("Monochrome".to_owned(), Palette::monochrome()), ("ANSI".to_owned(), Palette::ansi()),
("Fullcolor".to_owned(), Palette::fullcolor()),
("Okpalette".to_owned(), Palette::okpalette()),
("Gruvbox".to_owned(), Palette::gruvbox()),
("Solarized".to_owned(), Palette::solarized()),
("Terafox".to_owned(), Palette::terafox()),
("Fahrenheit".to_owned(), Palette::fahrenheit()),
("The Matrix".to_owned(), Palette::matrix()),
("Sequoia".to_owned(), Palette::sequoia()),
];
let keybinds_slots = vec![
("Default".to_owned(), Keybinds::default_tetro()),
("Control+".to_owned(), Keybinds::extra_control()),
("Guideline".to_owned(), Keybinds::guideline()),
("Vim".to_owned(), Keybinds::vim()),
];
let gameplay_slots = vec![
("Default".to_owned(), GameplaySettings::default()),
("Finesse+".to_owned(), GameplaySettings::extra_finesse()),
("Guideline".to_owned(), GameplaySettings::guideline()),
("NES".to_owned(), GameplaySettings::nes()),
("Gameboy".to_owned(), GameplaySettings::gameboy()),
];
Self {
graphics_slot_active: 0,
keybinds_slot_active: 0,
gameplay_slot_active: 0,
graphics_slots_that_should_not_be_changed: graphics_slots.len(),
palette_slots_that_should_not_be_changed: palette_slots.len(),
keybinds_slots_that_should_not_be_changed: keybinds_slots.len(),
gameplay_slots_that_should_not_be_changed: gameplay_slots.len(),
graphics_slots,
palette_slots,
keybinds_slots,
gameplay_slots,
new_game: NewGameSettings::default(),
}
}
}
impl Settings {
pub fn graphics(&self) -> &GraphicsSettings {
&self.graphics_slots[self.graphics_slot_active].1
}
pub fn keybinds(&self) -> &Keybinds {
&self.keybinds_slots[self.keybinds_slot_active].1
}
pub fn gameplay(&self) -> &GameplaySettings {
&self.gameplay_slots[self.gameplay_slot_active].1
}
fn graphics_mut(&mut self) -> &mut GraphicsSettings {
&mut self.graphics_slots[self.graphics_slot_active].1
}
fn keybinds_mut(&mut self) -> &mut Keybinds {
&mut self.keybinds_slots[self.keybinds_slot_active].1
}
fn gameplay_mut(&mut self) -> &mut GameplaySettings {
&mut self.gameplay_slots[self.gameplay_slot_active].1
}
pub fn palette(&self) -> &Palette {
&self.palette_slots[self.graphics().palette_active].1
}
pub fn palette_lockedtiles(&self) -> &Palette {
&self.palette_slots[self.graphics().palette_active_lockedtiles].1
}
}
#[derive(
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Copy,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub enum SavefileGranularity {
#[default]
NoSavefile,
RememberSettings,
RememberSettingsScores,
RememberSettingsScoresReplays,
}
#[derive(
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub struct TemporaryAppData {
pub custom_terminal_state_initialized: bool,
pub kitty_detected: bool,
pub kitty_assumed: bool,
pub blindfold_enabled: bool,
pub renderernumber: usize,
pub save_on_exit: SavefileGranularity,
pub savefile_path: PathBuf, }
#[derive(PartialEq, Clone, Debug)]
pub struct Application<T: Write> {
term: T,
temp_data: TemporaryAppData,
settings: Settings,
scores_and_replays: ScoresAndReplays,
game_saves: (usize, Vec<GameSave<UncompressedInputHistory>>),
statistics: Statistics,
}
impl<T: Write> Drop for Application<T> {
fn drop(&mut self) {
let _ = self.deinitialize_terminal_state();
if self.temp_data.save_on_exit != SavefileGranularity::NoSavefile {
if let Err(e) = self.store_to_savefile() {
eprintln!("{e}");
}
} else if self
.temp_data
.savefile_path
.try_exists()
.is_ok_and(|exists| exists)
{
if let Err(e) = std::fs::remove_file(self.temp_data.savefile_path.clone()) {
eprintln!("{e}");
}
}
}
}
impl<T: Write> Application<T> {
pub const W_MAIN: u16 = 62;
pub const H_MAIN: u16 = 23;
pub const TERMINAL_TITLE: &str = "Tetro TUI";
pub const KEYBOARD_ENHANCEMENT_FLAGS: KeyboardEnhancementFlags =
KeyboardEnhancementFlags::all();
pub fn fetch_main_xy() -> (u16, u16) {
let (w_console, h_console) = terminal::size().unwrap_or((0, 0));
(
w_console.saturating_sub(Self::W_MAIN) / 2,
h_console.saturating_sub(Self::H_MAIN) / 2,
)
}
fn initialize_terminal_state(&mut self) -> io::Result<()> {
if !self.temp_data.custom_terminal_state_initialized {
self.temp_data.custom_terminal_state_initialized = true;
self.term.execute(terminal::EnterAlternateScreen)?;
terminal::enable_raw_mode()?;
self.term.execute(cursor::Hide)?;
self.term
.execute(terminal::SetTitle(Self::TERMINAL_TITLE))?;
self.term.execute(PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::empty(),
))?;
}
Ok(())
}
fn deinitialize_terminal_state(&mut self) -> io::Result<()> {
if self.temp_data.custom_terminal_state_initialized {
self.term.execute(PopKeyboardEnhancementFlags)?;
self.term.execute(cursor::Show)?;
terminal::disable_raw_mode()?;
self.term.execute(terminal::LeaveAlternateScreen)?;
self.temp_data.custom_terminal_state_initialized = true;
}
Ok(())
}
pub fn with_savefile_and_cmdlineoptions(
term: T,
savefile_path: PathBuf,
custom_start_seed: Option<u64>,
custom_start_board: Option<String>,
) -> Self {
let mut new = Self {
temp_data: TemporaryAppData::default(),
term,
settings: Settings::default(),
scores_and_replays: ScoresAndReplays::default(),
game_saves: (0, Vec::new()),
statistics: Statistics::default(),
};
if new.load_from_savefile(savefile_path.clone()).is_err() {
}
let kitty_detected = terminal::supports_keyboard_enhancement().unwrap_or(false);
new.temp_data = TemporaryAppData {
custom_terminal_state_initialized: false,
kitty_detected,
kitty_assumed: kitty_detected,
blindfold_enabled: false,
renderernumber: 0,
save_on_exit: new.temp_data.save_on_exit,
savefile_path,
};
if custom_start_board.is_some() {
new.settings.new_game.custom_encoded_board = custom_start_board;
}
if custom_start_seed.is_some() {
new.settings.new_game.custom_seed = custom_start_seed;
}
new
}
fn load_from_savefile(&mut self, savefile_path: PathBuf) -> io::Result<()> {
let mut file = File::open(savefile_path)?;
let mut save_str = String::new();
file.read_to_string(&mut save_str)?;
let compressed_game_saves: (usize, Vec<GameSave<CompressedInputHistory>>);
(
self.temp_data.save_on_exit,
self.settings,
self.scores_and_replays,
compressed_game_saves,
self.statistics,
) = serde_json::from_str(&save_str)?;
self.game_saves = (
compressed_game_saves.0,
compressed_game_saves
.1
.into_iter()
.map(|save| save.map(|input_history| input_history.decompress()))
.collect::<Vec<_>>(),
);
Ok(())
}
fn store_to_savefile(&mut self) -> io::Result<()> {
if self.temp_data.save_on_exit < SavefileGranularity::RememberSettingsScores {
self.scores_and_replays.entries.clear();
} else if self.temp_data.save_on_exit < SavefileGranularity::RememberSettingsScoresReplays {
for (_entry, restoration_data) in &mut self.scores_and_replays.entries {
restoration_data.take();
}
}
let compressed_game_saves = (
self.game_saves.0,
self.game_saves
.1
.iter()
.cloned()
.map(|save| save.map(|input_history| CompressedInputHistory::new(&input_history)))
.collect::<Vec<_>>(),
);
let save_str = serde_json::to_string(&(
self.temp_data.save_on_exit,
&self.settings,
&self.scores_and_replays,
compressed_game_saves,
&self.statistics,
))?;
let mut file = File::create(self.temp_data.savefile_path.clone())?;
let n_written = file.write(save_str.as_bytes())?;
if n_written < save_str.len() {
Err(std::io::Error::other(
"attempt to write to file consumed `n < save_str.len()` bytes",
))
} else {
Ok(())
}
}
fn sort_past_games_chronologically(&mut self) {
self.scores_and_replays
.entries
.sort_by(|(pg1, _), (pg2, _)| {
pg1.game_meta_data
.datetime
.cmp(&pg2.game_meta_data.datetime)
.reverse()
});
}
#[rustfmt::skip]
fn sort_past_games_semantically(&mut self) {
self.scores_and_replays.entries.sort_by(|(pg1, _), (pg2, _)|
pg1.game_meta_data.title.cmp(&pg2.game_meta_data.title).then_with(||
pg1.is_win.cmp(&pg2.is_win).reverse().then_with(|| {
let o = match pg1.game_meta_data.comparison_stat.0 {
Stat::TimeElapsed(_) => pg1.time_elapsed.cmp(&pg2.time_elapsed),
Stat::PiecesLocked(_) => pg1.pieces_locked.cmp(&pg2.pieces_locked),
Stat::LinesCleared(_) => pg1.lineclears.cmp(&pg2.lineclears),
Stat::PointsScored(_) => pg1.points_scored.cmp(&pg2.points_scored),
};
if pg1.game_meta_data.comparison_stat.1
{ o } else { o.reverse() }
})
)
);
}
pub fn run(&mut self) -> io::Result<()> {
let _e = self.initialize_terminal_state();
let mut menu_stack = vec![Menu::Title];
loop {
let Some(menu) = menu_stack.last_mut() else {
break;
};
let menu_update = match menu {
Menu::Title => self.run_menu_title(),
Menu::NewGame => self.run_menu_new_game(),
Menu::PlayGame {
game,
game_input_history,
game_meta_data,
game_renderer,
} => {
self.run_menu_play_game(game, game_input_history, game_meta_data, game_renderer)
}
Menu::Pause => self.run_menu_pause(),
Menu::Settings => self.run_menu_settings(),
Menu::AdjustGraphics => self.run_menu_adjust_graphics(),
Menu::AdjustKeybinds => self.run_menu_adjust_keybinds(),
Menu::AdjustGameplay => self.run_menu_adjust_gameplay(),
Menu::AdvancedSettings => self.run_menu_advanced_settings(),
Menu::GameOver { game_scoring } => self.run_menu_game_ended(game_scoring),
Menu::GameComplete { game_scoring } => self.run_menu_game_ended(game_scoring),
Menu::ScoresAndReplays {
cursor_pos,
camera_pos,
} => self.run_menu_scores_and_replays(cursor_pos, camera_pos),
Menu::ReplayGame {
game_restoration_data,
game_meta_data,
replay_length,
game_renderer,
} => self.run_menu_replay_game(
game_restoration_data,
game_meta_data,
*replay_length,
game_renderer.as_mut(),
),
Menu::Statistics => self.run_menu_statistics(),
Menu::About => self.run_menu_about(),
Menu::Quit => break,
}?;
match menu_update {
MenuUpdate::Pop => {
if menu_stack.len() > 1 {
menu_stack.pop();
}
}
MenuUpdate::Push(menu) => {
if matches!(menu, Menu::Quit) {
break;
}
if matches!(
menu,
Menu::Title
| Menu::PlayGame { .. }
| Menu::GameOver { .. }
| Menu::GameComplete { .. }
) {
menu_stack.clear();
}
if matches!(menu, Menu::GameOver { .. }) {
let h_console = terminal::size()?.1;
for y in (0..h_console).rev() {
self.term
.execute(MoveTo(0, y))?
.execute(Clear(ClearType::CurrentLine))?;
std::thread::sleep(Duration::from_secs_f32(1. / 60.0));
}
} else if matches!(menu, Menu::GameComplete { .. }) {
let h_console = terminal::size()?.1;
for y in 0..h_console {
self.term
.execute(MoveTo(0, y))?
.execute(Clear(ClearType::CurrentLine))?;
std::thread::sleep(Duration::from_secs_f32(1. / 60.0));
}
} else {
}
menu_stack.push(menu);
}
}
}
self.deinitialize_terminal_state()
}
}