strategem-hero 0.4.0

Simple CLI Game following Strageme Hero mini-game from Helldivers 2
use std::time::Duration;

use crossterm::{
    cursor,
    event::{Event, KeyCode, KeyEvent, KeyEventKind},
    terminal::{self, ClearType},
    ExecutableCommand,
};

use crate::{
    event::Key,
    strategem::Strategem,
    utility::{self, GameTimer, HideCursor, Multiplier, Penalty},
};

struct GameState {
    game_timer: GameTimer,
    score: usize,
    best_score: usize,
    streak: usize,
    strategem: Strategem,
}

impl GameState {
    fn new(game_timer: GameTimer) -> Self {
        Self {
            game_timer,
            score: 0,
            best_score: 0,
            streak: 0,
            strategem: crate::strategem::random(),
        }
    }

    fn reset(&mut self) {
        self.game_timer.reset();
        self.score = 0;
        self.streak = 0;
        self.strategem = crate::strategem::random();
    }
}

pub struct Game {
    state: GameState,
    penalty: Penalty,
    is_running: bool,
}

impl Game {
    pub fn new(game_timer: GameTimer, penalty: Penalty) -> Self {
        Self {
            state: GameState::new(game_timer),
            penalty,
            is_running: true,
        }
    }

    pub fn run(&mut self) -> std::io::Result<()> {
        let _guard = HideCursor::hide()?;

        while self.is_running {
            let GameState {
                score,
                strategem,
                game_timer,
                streak,
                ..
            } = &mut self.state;

            if crossterm::event::poll(Duration::from_millis(17))? {
                self.handle_input()?;
            } else {
                print!("Score: {} {}\n", score, Multiplier::get(*streak));
                print!("Time left: {}\n", game_timer);
                print!("{:32}\n", strategem.name());
                println!("{}", strategem);

                std::io::stdout().execute(crossterm::cursor::MoveUp(4))?;
                if strategem.is_completed() {
                    *streak += 1;
                    *score +=
                        utility::get_score_value(strategem.difficulty(), Multiplier::get(*streak));
                    game_timer.add(Duration::from_millis(1500));
                    *strategem = crate::strategem::random();
                } else if !strategem.is_valid() {
                    *streak = 0;
                    self.penalty.apply(|| strategem.reset());
                }

                if game_timer.is_over() {
                    self.handle_game_over()?;
                }
            }
        }

        Ok(())
    }

    fn handle_input(&mut self) -> std::io::Result<()> {
        match crate::event::read()? {
            Some(Key::Escape) => {
                std::io::stdout()
                    .execute(cursor::MoveUp(4))?
                    .execute(terminal::Clear(ClearType::FromCursorDown))?;
                self.is_running = false;
            }
            Some(key) => self.state.strategem.assert_key(key.into()),

            _ => (),
        };

        Ok(())
    }

    fn handle_game_over(&mut self) -> std::io::Result<()> {
        std::io::stdout().execute(terminal::Clear(ClearType::FromCursorDown))?;
        println!("Game Over!");

        if self.state.score > self.state.best_score {
            self.state.best_score = self.state.score;
        }

        println!("Best score: {}", self.state.best_score);
        println!("Your score: {}", self.state.score);

        self.yes_no_prompt(
            "Restart the game",
            |this| {
                this.state.reset();
                std::io::stdout()
                    .execute(cursor::MoveUp(4))?
                    .execute(terminal::Clear(ClearType::FromCursorDown))?;
                Ok(())
            },
            |this| this.is_running = false,
        )?;

        Ok(())
    }

    fn yes_no_prompt(
        &mut self,
        msg: &str,
        on_yes: impl FnOnce(&mut Game) -> std::io::Result<()>,
        on_no: impl FnOnce(&mut Game),
    ) -> std::io::Result<()> {
        println!("{msg} [y/n]?");
        while let Event::Key(ev) = crossterm::event::read()? {
            match ev {
                KeyEvent {
                    code: KeyCode::Char('y'),
                    kind: KeyEventKind::Press,
                    ..
                } => {
                    on_yes(self)?;
                    break;
                }
                KeyEvent {
                    code: KeyCode::Char('n'),
                    kind: KeyEventKind::Press,
                    ..
                } => {
                    on_no(self);
                    break;
                }
                _ => (),
            }
        }

        Ok(())
    }
}