diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 196 |
1 files changed, 104 insertions, 92 deletions
diff --git a/src/main.rs b/src/main.rs index f63da20..dfccb9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,19 @@ -//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} + cursor::Hide, + cursor::Show, + event::{Event, KeyCode, KeyEvent}, + execute, + terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; +use rand::prelude::*; +use std::io::{self, Write}; +use std::time::{Duration, Instant}; use tui::{ - 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}, + backend::CrosstermBackend, + layout::{Alignment, Constraint, Direction, Layout, Margin}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, Paragraph}, + Terminal, }; const PARAGRAPHS: [&str; 3] = [ @@ -37,9 +32,7 @@ struct GameState { paragraph: String, user_input: String, incorrect_chars: usize, - difficulty: usize, start_time: Instant, - deleted_chars: usize, end_time: Option<Instant>, current_index: usize, } @@ -47,14 +40,11 @@ struct GameState { impl GameState { fn new() -> GameState { let paragraph = PARAGRAPHS.choose(&mut thread_rng()).unwrap().to_string(); - let difficulty = paragraph.len() / 4; GameState { paragraph, user_input: String::new(), incorrect_chars: 0, - difficulty, start_time: Instant::now(), - deleted_chars: 0, end_time: None, current_index: 0, } @@ -78,7 +68,6 @@ impl GameState { 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; @@ -88,93 +77,107 @@ impl GameState { if c == '\u{8}' { // Backspace if self.user_input.is_empty() { - self.deleted_chars = 0; + self.incorrect_chars += 1; } 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() { + if self.current_index < self.paragraph.len() { + if self.paragraph.chars().nth(self.current_index).unwrap() != c { self.incorrect_chars += 1; } + self.input(c); } } } 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; + let cpm = (self.user_input.len()) 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(); + let total_chars = self.user_input.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() + match self.end_time { + Some(end) => end - self.start_time, + None => self.start_time.elapsed(), + } } - 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> { + fn render_widgets<W>( + &self, + terminal: &mut Terminal<CrosstermBackend<W>>, + ) -> Result<(), io::Error> + where + W: Write, + { // 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 = "Typerpunk"; - let title_widget = Paragraph::new(title) - .style(Style::default().fg(Color::White)) - .block(Block::default().borders(Borders::ALL)); - - // 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| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(5) + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(80), + ] + .as_ref(), + ) + .split(size); + + // Title + let title = "Typerpunk"; + let title_widget = Paragraph::new(title) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + + // Stats + let stats_text = format!( + "WPM: {:.1} | Accuracy: {:.1}% | Time: {:.0}s", + self.wpm(), + self.accuracy(), + self.elapsed_time().as_secs() + ); + let stats_widget = Paragraph::new(stats_text) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + + // Paragraph + let paragraph_text = format!( + "{}\n{}", + self.paragraph, + self.user_input + .chars() + .map(|c| if c.is_whitespace() { ' ' } else { '_' }) + .collect::<String>() + ); + let paragraph_widget = Paragraph::new(paragraph_text) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title_widget, chunks[0]); f.render_widget(stats_widget, chunks[1]); f.render_widget(paragraph_widget, chunks[2]); })?; Ok(()) } - } fn main() -> Result<(), io::Error> { @@ -182,7 +185,7 @@ fn main() -> Result<(), io::Error> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - execute!(terminal.backend_mut(), EnterAlternateScreen, Hide)?; + execute!(terminal.backend_mut(), terminal::EnterAlternateScreen, Hide)?; let mut app_state = AppState::Playing(GameState::new()); let mut rng = thread_rng(); @@ -190,29 +193,38 @@ fn main() -> Result<(), io::Error> { match app_state { AppState::Playing(ref mut state) => { let stats = state.elapsed_time(); - state.render_widgets(&mut terminal, Some(stats))?; + state.render_widgets(&mut terminal)?; if let Ok(event) = crossterm::event::read() { match event { - crossterm::event::Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) => { + 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, .. }) => { + } + Event::Key(KeyEvent { + code: KeyCode::Enter, + .. + }) => { state.reset(); - }, - crossterm::event::Event::Key(KeyEvent { code: KeyCode::Esc, .. }) => { + } + 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)) + .alignment(Alignment::Center) .block(Block::default().borders(Borders::ALL)); terminal.draw(|f| { let size = f.size(); @@ -220,14 +232,14 @@ fn main() -> Result<(), io::Error> { })?; 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)?; + execute!(terminal.backend_mut(), terminal::LeaveAlternateScreen, Show)?; Ok(()) } |
