aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs280
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(())
+}