use config::TyperacerConfig;
use graphs::show_graphs;
use info::show_info;
use std::{
collections::HashSet,
fmt,
io::{stdin, stdout},
};
use termion::{event::Key, input::TermRead, raw::IntoRawMode, screen::AlternateScreen};
use tui::{backend::TermionBackend, text::Span, Terminal};
use crate::{
actions::Action, config, dirs::setup_dirs::get_db_path, graphs, info,
passage_controller::PassageInfo, stats,
};
pub mod formatter;
pub mod indexer;
pub mod split;
pub mod word_processing;
mod game_db;
mod game_render;
const TERRIBLE_DB_FAILURE: &str =
"HELP - TROUBLE STORING DATA IN THE DB, CONTACT THE MAINTAINER AND SHOW THEM THIS ERROR:";
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum GameMode {
Default,
InstantDeath,
Training,
}
impl GameMode {
pub fn next(self) -> Self {
match self {
GameMode::Default => GameMode::InstantDeath,
GameMode::InstantDeath => GameMode::Training,
GameMode::Training => GameMode::Default,
}
}
pub fn prev(self) -> Self {
match self {
GameMode::Default => GameMode::Training,
GameMode::Training => GameMode::InstantDeath,
GameMode::InstantDeath => GameMode::Default,
}
}
}
impl fmt::Display for GameMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GameMode::InstantDeath => write!(f, "Instant Death"),
GameMode::Default => write!(f, "Default"),
GameMode::Training => write!(f, "Training"),
}
}
}
impl From<GameMode> for i64 {
fn from(gm: GameMode) -> i64 {
match gm {
GameMode::Training => 2,
GameMode::InstantDeath => 1,
GameMode::Default => 0,
}
}
}
impl From<i64> for GameMode {
fn from(i: i64) -> Self {
match i {
2 => GameMode::Training,
1 => GameMode::InstantDeath,
_ => GameMode::Default,
}
}
}
pub fn play_game(
passage_info: &PassageInfo,
stats: &mut stats::Stats,
debug_enabled: bool,
game_mode: GameMode,
typeracer_version: &str,
typeracer_config: &TyperacerConfig,
) -> Action {
let stdout = stdout()
.into_raw_mode()
.expect("Failed to manipulate terminal to raw mode");
let screen = AlternateScreen::from(stdout);
let backend = TermionBackend::new(screen);
let mut terminal = Terminal::new(backend).expect("Unable to get handle to terminal.");
terminal.hide_cursor().expect("Failed to hide the cursor");
let mut formatted_texts = formatter::FormattedTexts {
passage: passage_info
.passage
.chars()
.map(|it| Span::raw(it.to_string()))
.collect(),
input: vec![],
error: false,
complete: false,
};
let mut user_input = String::new();
let mut mistaken_words: HashSet<String> = HashSet::new();
let words: Vec<&str> = split::to_words(&passage_info.passage);
let text_mode = word_processing::get_game_mode(&passage_info.passage);
let mut current_word_idx = 0;
loop {
game_render::render(
&mut terminal,
game_render::GameState {
texts: &formatted_texts,
user_input: &user_input,
stats,
title: &passage_info.title,
game_mode,
config: typeracer_config,
debug_enabled,
word_idx: current_word_idx,
passage_path: &passage_info.passage_path,
complete: formatted_texts.complete,
current_word: if current_word_idx == words.len() || formatted_texts.complete {
"DONE"
} else {
words[current_word_idx]
},
mistaken_words: &mistaken_words,
},
typeracer_version,
);
if formatted_texts.complete {
break;
}
let mut new_char = false;
let mut last_input_char = ' ';
let stdin = stdin();
let c = stdin.keys().find_map(Result::ok);
let mut allowed_to_increment_combo = false;
match c.unwrap() {
Key::Ctrl('a') => show_info(&mut terminal, typeracer_version),
Key::Ctrl('c') => return Action::Quit,
Key::Ctrl('n') => return Action::NextPassage,
Key::Ctrl('p') => return Action::PreviousPassage,
Key::Ctrl('r') => return Action::RestartPassage,
Key::Ctrl('g') => show_graphs(&mut terminal, &get_db_path(), game_mode)
.expect("Unable to get data for graph"),
Key::Ctrl('u') => user_input.clear(),
Key::Ctrl('w') => {
user_input = word_processing::get_all_input_minus_last_word(&user_input)
}
Key::Backspace | Key::Ctrl('h') => {
user_input.pop();
}
Key::Char(c) => {
new_char = true;
last_input_char = c;
stats.update_start_time();
if word_processing::word_completed(
&text_mode,
last_input_char,
words[current_word_idx],
&user_input,
) {
if !typeracer_config.display_settings.always_full {
formatted_texts.passage = word_processing::get_updated_texts(
&text_mode,
formatted_texts.passage,
words[current_word_idx],
);
}
current_word_idx += 1;
user_input.clear();
} else if c == '\n' || c == '\t' {
} else {
user_input.push(c);
}
stats.update_wpm(current_word_idx, &words);
allowed_to_increment_combo = true;
}
_ => {}
}
formatted_texts = if current_word_idx >= words.len() {
formatted_texts
} else if typeracer_config.display_settings.always_full {
formatter::get_formatted_texts(
&text_mode,
&words,
&user_input.to_string(),
current_word_idx,
last_input_char,
new_char,
formatted_texts.passage,
)
} else {
formatter::get_formatted_texts_line_mode(
&text_mode,
words[current_word_idx],
&user_input.to_string(),
last_input_char,
new_char,
formatted_texts.passage,
)
};
let current_letter_idx =
indexer::get_trying_letter_idx(&text_mode, &words, current_word_idx, &user_input);
if formatted_texts.error && new_char {
stats.increment_errors(current_letter_idx);
mistaken_words.insert(words[current_word_idx].to_string());
if game_mode == GameMode::InstantDeath {
formatted_texts = formatter::get_reformatted_failed_texts(&text_mode, &words);
continue;
}
} else if allowed_to_increment_combo {
stats.increment_combo(current_letter_idx);
}
if word_processing::decide_game_end(&text_mode, current_word_idx, &words, &user_input) {
formatted_texts = formatter::get_reformatted_complete_texts(&text_mode, &words);
current_word_idx += 1;
stats.update_wpm(current_word_idx, &words);
user_input.clear();
}
}
if let Err(e) = game_db::store_stats(&get_db_path(), stats, passage_info, game_mode) {
println!("{} {}", TERRIBLE_DB_FAILURE, e);
}
if game_mode == GameMode::Training {
if let Err(e) = game_db::roll_to_delete_mistaken_words_typed_correctly(
&get_db_path(),
&words,
&mistaken_words,
) {
println!("{} {}", TERRIBLE_DB_FAILURE, e);
}
}
if let Err(e) = game_db::store_mistaken_words(&get_db_path(), &mistaken_words) {
println!("{} {}", TERRIBLE_DB_FAILURE, e);
}
loop {
let stdin = stdin();
for c in stdin.keys() {
match c.unwrap() {
Key::Ctrl('a') => {
show_info(&mut terminal, typeracer_version);
game_render::render(
&mut terminal,
game_render::GameState {
texts: &formatted_texts,
user_input: &user_input,
stats,
title: &passage_info.title,
game_mode,
config: typeracer_config,
debug_enabled,
complete: formatted_texts.complete,
word_idx: current_word_idx,
passage_path: &passage_info.passage_path,
current_word: if current_word_idx == words.len() {
"DONE"
} else {
words[current_word_idx]
},
mistaken_words: &mistaken_words,
},
typeracer_version,
);
}
Key::Ctrl('c') => return Action::Quit,
Key::Ctrl('n') => return Action::NextPassage,
Key::Ctrl('p') => return Action::PreviousPassage,
Key::Ctrl('r') => return Action::RestartPassage,
Key::Ctrl('g') => {
show_graphs(&mut terminal, &get_db_path(), game_mode)
.expect("Unable to get data for graph");
game_render::render(
&mut terminal,
game_render::GameState {
texts: &formatted_texts,
user_input: &user_input,
stats,
title: &passage_info.title,
game_mode,
config: typeracer_config,
debug_enabled,
complete: formatted_texts.complete,
word_idx: current_word_idx,
passage_path: &passage_info.passage_path,
current_word: if current_word_idx == words.len() {
"DONE"
} else {
words[current_word_idx]
},
mistaken_words: &mistaken_words,
},
typeracer_version,
);
}
_ => (),
}
}
}
}