diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 280 |
1 files changed, 197 insertions, 83 deletions
diff --git a/src/main.rs b/src/main.rs index b5a5760..c693804 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,119 +1,233 @@ - -// Crates -use rand::Rng; +//use std::io::{self, Stdout}; +use std::io::{self, Write}; use std::time::{Duration, Instant}; +use rand::prelude::*; +//use rand::{seq::SliceRandom, thread_rng}; +use rand::seq::SliceRandom; +use rand::thread_rng; +//use termion::event::Key; +//use termion::input::TermRead; +use crossterm::{ + execute, terminal::{self, ClearType, disable_raw_mode, enable_raw_mode}, event::{Event, KeyCode, KeyEvent}, terminal::size, + cursor::Hide, cursor::Show, style::{Print, ResetColor, SetBackgroundColor, SetForegroundColor}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen} +}; use tui::{ - backend::TermionBackend, - layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - widgets::{Block, Borders, Paragraph, Span}, - Terminal, + backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Modifier, Color, Style}, widgets::{Block, Borders, BorderType, ListState, Paragraph, Widget, Wrap}, Terminal +// backend::TermionBackend, +// symbols::Marker, +// text::{Span, Spans}, +// terminal::{Frame, Terminal}, }; -// Constants -const PARAGRAPH: &str = "The quick brown fox jumps over the lazy dog."; +const PARAGRAPHS: [&str; 3] = [ + "The quick brown fox jumps over the lazy dog.", + "In the beginning God created the heavens and the earth.", + "To be, or not to be, that is the question:", +]; -// Enums enum AppState { Playing(GameState), - Stats(GameState, Duration), + Stats(f64, u64), Quit, } -// Structs struct GameState { paragraph: String, user_input: String, + incorrect_chars: usize, + difficulty: usize, start_time: Instant, - difficulty: Difficulty, -} - -enum Difficulty { - Easy, - Medium, - Hard, + deleted_chars: usize, + end_time: Option<Instant>, + current_index: usize, } -// Implementations impl GameState { - fn new(difficulty: Difficulty) -> Self { - let mut rng = rand::thread_rng(); - let paragraph = PARAGRAPH.to_owned(); - - let user_input = String::new(); - let start_time = Instant::now(); - - Self { + fn new() -> GameState { + let paragraph = PARAGRAPHS.choose(&mut thread_rng()).unwrap().to_string(); + let difficulty = paragraph.len() / 4; + GameState { paragraph, - user_input, - start_time, + user_input: String::new(), + incorrect_chars: 0, difficulty, + start_time: Instant::now(), + deleted_chars: 0, + end_time: None, + current_index: 0, + } + } + + fn input(&mut self, c: char) { + if self.current_index < self.paragraph.len() { + self.user_input.push(c); + self.current_index += 1; + } + } + + fn check_end_condition(&mut self) -> bool { + if self.current_index == self.paragraph.len() { + self.end_time = Some(Instant::now()); + return true; + } + false + } + + fn reset(&mut self) { + self.user_input.clear(); + self.incorrect_chars = 0; + self.deleted_chars = 0; + self.start_time = Instant::now(); + self.end_time = None; + self.current_index = 0; + } + + fn handle_input(&mut self, c: char) { + if c == '\u{8}' { + // Backspace + if self.user_input.is_empty() { + self.deleted_chars = 0; + } else { + self.user_input.pop(); + self.deleted_chars += 1; + } + } else if !c.is_control() { + // Printable character + self.user_input.push(c); + if self.user_input.chars().count() > self.paragraph.chars().count() { + self.incorrect_chars += 1; + } else if let (Some(prev_char), Some(curr_char)) = (self.user_input.chars().nth(self.user_input.len() - 2), self.user_input.chars().last()) { + if prev_char != self.paragraph.chars().nth(self.user_input.len() - 2).unwrap() || curr_char != self.paragraph.chars().nth(self.user_input.len() - 1).unwrap() { + self.incorrect_chars += 1; + } + } } } fn wpm(&self) -> f64 { - // ... + let elapsed_time = self.elapsed_time().as_secs_f64() / 60.0; + let cpm = (self.user_input.len() - self.deleted_chars) as f64 / elapsed_time; + cpm / 5.0 } fn accuracy(&self) -> f64 { - // ... + let total_chars = self.user_input.len().max(self.paragraph.len()); + let correct_chars = self.user_input.chars().zip(self.paragraph.chars()).filter(|&(a, b)| a == b).count(); + (correct_chars as f64 / total_chars as f64) * 100.0 } fn elapsed_time(&self) -> Duration { - // ... + self.start_time.elapsed() } - fn advance(&mut self) -> bool { - // ... - } + fn render_widgets<W>(&self, terminal: &mut Terminal<CrosstermBackend<W>>, stats: Option<Duration>) -> Result<(), io::Error> where W: Write { + //fn render_widgets(&self, terminal: &mut Terminal<CrosstermBackend>, stats: Option<Duration>) -> Result<(), io::Error> { + // Layout + let size = terminal.size()?; + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(5) + .constraints( + [ + Constraint::Length(3), // Title + Constraint::Length(3), // Stats + Constraint::Min(0), // Paragraph + ] + .as_ref(), + ) + .split(size); + + // Title + let title = "Typing Game"; + let title_widget = Paragraph::new(title) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)); - fn check_end_condition(&self) -> bool { - // ... + // Stats + let stats_widget = if let Some(duration) = stats { + let wpm = self.wpm(); + let accuracy = self.accuracy(); + let stats_text = format!("WPM: {:.1} | Accuracy: {:.1}% | Time: {:.0}s", wpm, accuracy, duration.as_secs()); + Paragraph::new(stats_text) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)) + } else { + Paragraph::new("") + }; + + // Paragraph + let ghost_text = self.paragraph.chars().map(|c| if c.is_whitespace() { ' ' } else { '_' }).collect::<String>(); + let user_input = self.user_input.clone(); + let paragraph_text = format!("{}\n{}", ghost_text, user_input); + let paragraph_widget = Paragraph::new(paragraph_text) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)); + + // Render + execute!(terminal.backend_mut(), terminal::Clear(ClearType::All))?; + terminal.draw(|mut f| { + f.render_widget(title_widget, chunks[0]); + f.render_widget(stats_widget, chunks[1]); + f.render_widget(paragraph_widget, chunks[2]); + })?; + Ok(()) } -} -// Functions -fn main() { - // ... } -/* - * The following code defines a typing game in Rust. - * - * In main, the game loop is started by calling `run_game()`. - * - * The `GameState` struct represents the state of the game, including the difficulty level, - * the current paragraph to type, the player's progress, and various methods for manipulating - * and querying the game state. These methods include: - * - * - `new(difficulty: Difficulty)`: creates a new game state with the given difficulty. - * - `get_paragraph(difficulty: Difficulty)`: returns a random paragraph of the specified difficulty. - * - `wpm(&self)`: calculates the current words per minute of the player. - * - `accuracy(&self)`: calculates the current accuracy of the player. - * - `check_end_condition(&self)`: checks if the game has ended. - * - `advance(&mut self)`: advances the game to the next character. - * - `reset(&mut self)`: resets the game state to its initial state. - * - * The `Difficulty` enum represents the difficulty level of the game, and includes a method to create - * an enum variant from a string: - * - * - `from_str(s: &str) -> Result<Self, &'static str>`: creates a difficulty enum variant from a string. - * - * The `App` struct represents the application as a whole, and includes methods for handling input, - * updating the game state, setting the game difficulty, resetting the game, and setting the current - * application state. These methods include: - * - * - `new()`: creates a new `App` instance. - * - `handle_input(&mut self, input: Key)`: handles keyboard input. - * - `update(&mut self, elapsed: Duration)`: updates the game state based on the elapsed time. - * - `set_difficulty(&mut self, difficulty: Difficulty)`: sets the game difficulty. - * - `reset_game(&mut self)`: resets the game state to its initial state. - * - `set_state(&mut self, state: AppState)`: sets the current application state. - * - * The `AppState` enum represents the state of the application, including whether the game is being played, - * whether the player is viewing their stats, or whether the game is quitting. The enum variants include: - * - * - `Playing(GameState)`: represents the game state while the game is being played. - * - `Stats(GameState, Instant)`: represents the game state and start time while displaying stats. - * - `Quit`: represents the state where the game is quitting. - */ +fn main() -> Result<(), io::Error> { + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + execute!(terminal.backend_mut(), EnterAlternateScreen, Hide)?; + + let mut app_state = AppState::Playing(GameState::new()); + let mut rng = thread_rng(); + loop { + match app_state { + AppState::Playing(ref mut state) => { + let stats = state.elapsed_time(); + state.render_widgets(&mut terminal, Some(stats))?; + if let Ok(event) = crossterm::event::read() { + match event { + crossterm::event::Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) => { + state.handle_input(c); + if state.check_end_condition() { + app_state = AppState::Stats(state.wpm(), state.accuracy() as u64); + } + }, + crossterm::event::Event::Key(KeyEvent { code: KeyCode::Enter, .. }) => { + state.reset(); + }, + crossterm::event::Event::Key(KeyEvent { code: KeyCode::Esc, .. }) => { + app_state = AppState::Quit; + }, + _ => {}, + } + } + }, + AppState::Stats(wpm, accuracy) => { + let stats_text = format!("Your WPM is {:.1} with {:.1}% accuracy!", wpm, accuracy); + let stats_widget = Paragraph::new(stats_text) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)); + terminal.draw(|f| { + let size = f.size(); + f.render_widget(stats_widget, size); + })?; + std::thread::sleep(rng.gen_range(Duration::from_secs(2)..Duration::from_secs(4))); + app_state = AppState::Playing(GameState::new()); + }, + AppState::Quit => { + break; + }, + } + } + + execute!(terminal.backend_mut(), LeaveAlternateScreen, Show)?; + + Ok(()) +} |
