Consistently refer to drills as drills now, rename from lesson/practice

Also fix some issues in the stats screen.
This commit is contained in:
2026-02-15 04:44:49 +00:00
parent a51adafeb0
commit 13550505c1
16 changed files with 413 additions and 319 deletions

View File

@@ -3,7 +3,7 @@ use std::time::Instant;
use crate::session::input::CharStatus;
pub struct LessonState {
pub struct DrillState {
pub target: Vec<char>,
pub input: Vec<CharStatus>,
pub cursor: usize,
@@ -12,7 +12,7 @@ pub struct LessonState {
pub typo_flags: HashSet<usize>,
}
impl LessonState {
impl DrillState {
pub fn new(text: &str) -> Self {
Self {
target: text.chars().collect(),
@@ -90,72 +90,72 @@ mod tests {
use crate::session::input;
#[test]
fn test_new_lesson() {
let lesson = LessonState::new("hello");
assert_eq!(lesson.target.len(), 5);
assert_eq!(lesson.cursor, 0);
assert!(!lesson.is_complete());
assert_eq!(lesson.progress(), 0.0);
fn test_new_drill() {
let drill = DrillState::new("hello");
assert_eq!(drill.target.len(), 5);
assert_eq!(drill.cursor, 0);
assert!(!drill.is_complete());
assert_eq!(drill.progress(), 0.0);
}
#[test]
fn test_accuracy_starts_at_100() {
let lesson = LessonState::new("test");
assert_eq!(lesson.accuracy(), 100.0);
let drill = DrillState::new("test");
assert_eq!(drill.accuracy(), 100.0);
}
#[test]
fn test_empty_lesson_progress() {
let lesson = LessonState::new("");
assert!(lesson.is_complete());
assert_eq!(lesson.progress(), 0.0);
fn test_empty_drill_progress() {
let drill = DrillState::new("");
assert!(drill.is_complete());
assert_eq!(drill.progress(), 0.0);
}
#[test]
fn test_correct_typing_no_typos() {
let mut lesson = LessonState::new("abc");
input::process_char(&mut lesson, 'a');
input::process_char(&mut lesson, 'b');
input::process_char(&mut lesson, 'c');
assert!(lesson.typo_flags.is_empty());
assert_eq!(lesson.accuracy(), 100.0);
let mut drill = DrillState::new("abc");
input::process_char(&mut drill, 'a');
input::process_char(&mut drill, 'b');
input::process_char(&mut drill, 'c');
assert!(drill.typo_flags.is_empty());
assert_eq!(drill.accuracy(), 100.0);
}
#[test]
fn test_wrong_then_backspace_then_correct_counts_as_error() {
let mut lesson = LessonState::new("abc");
let mut drill = DrillState::new("abc");
// Type wrong at pos 0
input::process_char(&mut lesson, 'x');
assert!(lesson.typo_flags.contains(&0));
input::process_char(&mut drill, 'x');
assert!(drill.typo_flags.contains(&0));
// Backspace
input::process_backspace(&mut lesson);
input::process_backspace(&mut drill);
// Typo flag persists
assert!(lesson.typo_flags.contains(&0));
assert!(drill.typo_flags.contains(&0));
// Type correct
input::process_char(&mut lesson, 'a');
assert!(lesson.typo_flags.contains(&0));
assert_eq!(lesson.typo_count(), 1);
assert!(lesson.accuracy() < 100.0);
input::process_char(&mut drill, 'a');
assert!(drill.typo_flags.contains(&0));
assert_eq!(drill.typo_count(), 1);
assert!(drill.accuracy() < 100.0);
}
#[test]
fn test_multiple_errors_same_position_counts_as_one() {
let mut lesson = LessonState::new("abc");
let mut drill = DrillState::new("abc");
// Wrong, backspace, wrong again, backspace, correct
input::process_char(&mut lesson, 'x');
input::process_backspace(&mut lesson);
input::process_char(&mut lesson, 'y');
input::process_backspace(&mut lesson);
input::process_char(&mut lesson, 'a');
assert_eq!(lesson.typo_count(), 1);
input::process_char(&mut drill, 'x');
input::process_backspace(&mut drill);
input::process_char(&mut drill, 'y');
input::process_backspace(&mut drill);
input::process_char(&mut drill, 'a');
assert_eq!(drill.typo_count(), 1);
}
#[test]
fn test_wrong_char_without_backspace() {
let mut lesson = LessonState::new("abc");
input::process_char(&mut lesson, 'x'); // wrong at pos 0
input::process_char(&mut lesson, 'b'); // correct at pos 1
assert_eq!(lesson.typo_count(), 1);
assert!(lesson.typo_flags.contains(&0));
let mut drill = DrillState::new("abc");
input::process_char(&mut drill, 'x'); // wrong at pos 0
input::process_char(&mut drill, 'b'); // correct at pos 1
assert_eq!(drill.typo_count(), 1);
assert!(drill.typo_flags.contains(&0));
}
}

View File

@@ -1,6 +1,6 @@
use std::time::Instant;
use crate::session::lesson::LessonState;
use crate::session::drill::DrillState;
#[derive(Clone, Debug)]
pub enum CharStatus {
@@ -17,16 +17,16 @@ pub struct KeystrokeEvent {
pub correct: bool,
}
pub fn process_char(lesson: &mut LessonState, ch: char) -> Option<KeystrokeEvent> {
if lesson.is_complete() {
pub fn process_char(drill: &mut DrillState, ch: char) -> Option<KeystrokeEvent> {
if drill.is_complete() {
return None;
}
if lesson.started_at.is_none() {
lesson.started_at = Some(Instant::now());
if drill.started_at.is_none() {
drill.started_at = Some(Instant::now());
}
let expected = lesson.target[lesson.cursor];
let expected = drill.target[drill.cursor];
let correct = ch == expected;
let event = KeystrokeEvent {
@@ -37,23 +37,23 @@ pub fn process_char(lesson: &mut LessonState, ch: char) -> Option<KeystrokeEvent
};
if correct {
lesson.input.push(CharStatus::Correct);
drill.input.push(CharStatus::Correct);
} else {
lesson.input.push(CharStatus::Incorrect(ch));
lesson.typo_flags.insert(lesson.cursor);
drill.input.push(CharStatus::Incorrect(ch));
drill.typo_flags.insert(drill.cursor);
}
lesson.cursor += 1;
drill.cursor += 1;
if lesson.is_complete() {
lesson.finished_at = Some(Instant::now());
if drill.is_complete() {
drill.finished_at = Some(Instant::now());
}
Some(event)
}
pub fn process_backspace(lesson: &mut LessonState) {
if lesson.cursor > 0 {
lesson.cursor -= 1;
lesson.input.pop();
pub fn process_backspace(drill: &mut DrillState) {
if drill.cursor > 0 {
drill.cursor -= 1;
drill.input.pop();
}
}

View File

@@ -1,3 +1,3 @@
pub mod input;
pub mod lesson;
pub mod drill;
pub mod result;

View File

@@ -2,10 +2,10 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::session::input::KeystrokeEvent;
use crate::session::lesson::LessonState;
use crate::session::drill::DrillState;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LessonResult {
pub struct DrillResult {
pub wpm: f64,
pub cpm: f64,
pub accuracy: f64,
@@ -15,11 +15,11 @@ pub struct LessonResult {
pub elapsed_secs: f64,
pub timestamp: DateTime<Utc>,
pub per_key_times: Vec<KeyTime>,
#[serde(default = "default_lesson_mode")]
pub lesson_mode: String,
#[serde(default = "default_drill_mode", alias = "lesson_mode")]
pub drill_mode: String,
}
fn default_lesson_mode() -> String {
fn default_drill_mode() -> String {
"adaptive".to_string()
}
@@ -30,8 +30,8 @@ pub struct KeyTime {
pub correct: bool,
}
impl LessonResult {
pub fn from_lesson(lesson: &LessonState, events: &[KeystrokeEvent], lesson_mode: &str) -> Self {
impl DrillResult {
pub fn from_drill(drill: &DrillState, events: &[KeystrokeEvent], drill_mode: &str) -> Self {
let per_key_times: Vec<KeyTime> = events
.windows(2)
.map(|pair| {
@@ -44,8 +44,8 @@ impl LessonResult {
})
.collect();
let total_chars = lesson.target.len();
let typo_count = lesson.typo_flags.len();
let total_chars = drill.target.len();
let typo_count = drill.typo_flags.len();
let accuracy = if total_chars > 0 {
((total_chars - typo_count) as f64 / total_chars as f64 * 100.0).clamp(0.0, 100.0)
} else {
@@ -53,16 +53,16 @@ impl LessonResult {
};
Self {
wpm: lesson.wpm(),
cpm: lesson.cpm(),
wpm: drill.wpm(),
cpm: drill.cpm(),
accuracy,
correct: total_chars - typo_count,
incorrect: typo_count,
total_chars,
elapsed_secs: lesson.elapsed_secs(),
elapsed_secs: drill.elapsed_secs(),
timestamp: Utc::now(),
per_key_times,
lesson_mode: lesson_mode.to_string(),
drill_mode: drill_mode.to_string(),
}
}
}