aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main.rs196
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(())
}