Skip to content

Commit

Permalink
feat: sudden death (#111)
Browse files Browse the repository at this point in the history
* Add sudden_death_enabled field to TestEvent

- Add the field sudden_death_enabled to the TestEvent struct to
  represent whether suddean death mode is turned on.
- Extract the is_missed_word_event() lambda in the calc_missed_words()
  test into a function for use in other parts of the program.

* Add sudden death option and logic

- Add command line option --sudden-death to enabel sudden death mode
- Add logic for restarting the test as soon as an error is made if
  sudden death mode is enabled

* Make condition shorter and more precise

Co-authored-by: Max Niederman <maxniederman@gmail.com>

* refactor(test): move sudden death logic to Test

- Move logic for checking if a user makes a mistake during sudden death
  mode out of `src/main.rs` and into `src/test/mod.rs`. Note that this
  changes how sudden death mode works: previously, if a user made a
  mistake in sudden death mode, they would immediately begin a new test;
  now, they restart the current test.

---------

Co-authored-by: Max Niederman <maxniederman@gmail.com>
  • Loading branch information
PappasBrent and max-niederman committed Feb 2, 2024
1 parent 2a68484 commit d6128df
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 22 deletions.
14 changes: 12 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ struct Opt {
/// Disable backtracking to completed words
#[structopt(long)]
no_backtrack: bool,

/// Enable sudden death mode to restart on first error
#[structopt(long)]
sudden_death: bool,
}

impl Opt {
Expand Down Expand Up @@ -228,6 +232,7 @@ fn main() -> io::Result<()> {
"Couldn't get test contents. Make sure the specified language actually exists.",
),
!opt.no_backtrack,
opt.sudden_death,
));

state.render_into(&mut terminal, &config)?;
Expand Down Expand Up @@ -276,7 +281,8 @@ fn main() -> io::Result<()> {
opt.gen_contents().expect(
"Couldn't get test contents. Make sure the specified language actually exists.",
),
!opt.no_backtrack
!opt.no_backtrack,
opt.sudden_death
));
}
Event::Key(KeyEvent {
Expand All @@ -294,7 +300,11 @@ fn main() -> io::Result<()> {
.flat_map(|w| vec![w.clone(); 5])
.collect();
practice_words.shuffle(&mut thread_rng());
state = State::Test(Test::new(practice_words, !opt.no_backtrack));
state = State::Test(Test::new(
practice_words,
!opt.no_backtrack,
opt.sudden_death,
));
}
Event::Key(KeyEvent {
code: KeyCode::Char('q'),
Expand Down
55 changes: 40 additions & 15 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub struct TestEvent {
pub correct: Option<bool>,
}

pub fn is_missed_word_event(event: &TestEvent) -> bool {
event.correct != Some(true)
}

impl fmt::Debug for TestEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TestEvent")
Expand Down Expand Up @@ -48,15 +52,17 @@ pub struct Test {
pub current_word: usize,
pub complete: bool,
pub backtracking_enabled: bool,
pub sudden_death_enabled: bool,
}

impl Test {
pub fn new(words: Vec<String>, backtracking_enabled: bool) -> Self {
pub fn new(words: Vec<String>, backtracking_enabled: bool, sudden_death_enabled: bool) -> Self {
Self {
words: words.into_iter().map(TestWord::from).collect(),
current_word: 0,
complete: false,
backtracking_enabled,
sudden_death_enabled,
}
}

Expand All @@ -76,12 +82,17 @@ impl Test {
key,
})
} else if !word.progress.is_empty() || word.text.is_empty() {
word.events.push(TestEvent {
time: Instant::now(),
correct: Some(word.text == word.progress),
key,
});
self.next_word();
let correct = word.text == word.progress;
if self.sudden_death_enabled && !correct {
self.reset();
} else {
word.events.push(TestEvent {
time: Instant::now(),
correct: Some(correct),
key,
});
self.next_word();
}
}
}
KeyCode::Backspace => {
Expand Down Expand Up @@ -115,14 +126,19 @@ impl Test {
}
KeyCode::Char(c) => {
word.progress.push(c);
word.events.push(TestEvent {
time: Instant::now(),
correct: Some(word.text.starts_with(&word.progress[..])),
key,
});
if word.progress == word.text && self.current_word == self.words.len() - 1 {
self.complete = true;
self.current_word = 0;
let correct = word.text.starts_with(&word.progress[..]);
if self.sudden_death_enabled && !correct {
self.reset();
} else {
word.events.push(TestEvent {
time: Instant::now(),
correct: Some(correct),
key,
});
if word.progress == word.text && self.current_word == self.words.len() - 1 {
self.complete = true;
self.current_word = 0;
}
}
}
_ => {}
Expand All @@ -143,4 +159,13 @@ impl Test {
self.current_word += 1;
}
}

fn reset(&mut self) {
self.words.iter_mut().for_each(|word: &mut TestWord| {
word.progress.clear();
word.events.clear();
});
self.current_word = 0;
self.complete = false;
}
}
6 changes: 1 addition & 5 deletions src/test/results.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::Test;
use super::{is_missed_word_event, Test};

use crossterm::event::KeyEvent;
use std::collections::HashMap;
Expand Down Expand Up @@ -150,10 +150,6 @@ fn calc_accuracy(events: &[&super::TestEvent]) -> AccuracyData {
}

fn calc_missed_words(test: &Test) -> Vec<String> {
let is_missed_word_event = |event: &super::TestEvent| -> bool {
event.correct == Some(false) || event.correct.is_none()
};

test.words
.iter()
.filter(|word| word.events.iter().any(is_missed_word_event))
Expand Down

0 comments on commit d6128df

Please sign in to comment.