1use crate::layout;
4use crate::store::Progress;
5
6pub const SPEED_SHARE: f64 = 0.9;
8pub const WORDS_ACCURACY: f64 = 97.0;
9pub const REAL_ACCURACY: f64 = 95.0;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Tick {
14 Kept,
15 Vim,
16 Workday,
17}
18
19impl Tick {
20 pub fn key(self) -> char {
21 match self {
22 Tick::Kept => 'k',
23 Tick::Vim => 'v',
24 Tick::Workday => 'w',
25 }
26 }
27
28 pub fn toggle(self, progress: &mut Progress, stage: usize) {
29 let m = progress.manual_mut(stage);
30 match self {
31 Tick::Kept => m.oryx = !m.oryx,
32 Tick::Vim => m.vim = !m.vim,
33 Tick::Workday => m.workday = !m.workday,
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct Item {
40 pub label: String,
41 pub ok: bool,
42 pub detail: String,
43 pub tick: Option<Tick>,
44}
45
46pub fn items(progress: &Progress, stage: usize) -> Vec<Item> {
47 let manual = progress.manual(stage);
48 if stage == 0 {
49 let n = progress.runs_for(0, "real").len();
50 let detail = match (n, progress.baseline()) {
51 (0, Some(b)) => format!("Typed in: {b:.0} WPM"),
52 (0, None) => "Do three real-text runs, or run `very-normal baseline <wpm>`".into(),
53 (n, Some(b)) => format!("{n} run(s), averaging {b:.0} WPM"),
54 (n, None) => format!("{n} run(s)"),
55 };
56 return vec![
57 Item {
58 label: "Planck setup kept safe (old revision or firmware file)".into(),
59 ok: manual.oryx,
60 detail: String::new(),
61 tick: Some(Tick::Kept),
62 },
63 Item {
64 label: "Baseline recorded".into(),
65 ok: n >= 3 || manual.wpm > 0.0,
66 detail,
67 tick: None,
68 },
69 ];
70 }
71
72 let words = progress.runs_for(stage, "words");
73 let last3: Vec<_> = words.iter().rev().take(3).collect();
74 let words_ok = last3.len() == 3 && last3.iter().all(|r| r.acc >= WORDS_ACCURACY);
75 let words_detail = if last3.is_empty() {
76 "No new-keys runs yet".into()
77 } else {
78 format!(
79 "Last runs: {}",
80 last3
81 .iter()
82 .rev()
83 .map(|r| format!("{:.0}%", r.acc))
84 .collect::<Vec<_>>()
85 .join(", ")
86 )
87 };
88
89 let baseline = progress.baseline();
90 let real = progress.runs_for(stage, "real");
91 let best = real
92 .iter()
93 .rev()
94 .take(5)
95 .filter(|r| r.acc >= REAL_ACCURACY)
96 .max_by(|a, b| a.wpm.total_cmp(&b.wpm));
97 let (real_ok, real_detail) = match baseline {
98 None => (false, "Record a baseline in stage 0 first".to_string()),
99 Some(b) => {
100 let target = b * SPEED_SHARE;
101 let ok = best.is_some_and(|r| r.wpm >= target);
102 let best = best.map_or("no qualifying run yet".to_string(), |r| {
103 format!("best recent {:.0} WPM at {:.0}%", r.wpm, r.acc)
104 });
105 (ok, format!("Target {target:.0} WPM; {best}"))
106 }
107 };
108
109 vec![
110 Item {
111 label: format!("New keys at {WORDS_ACCURACY:.0}%+, three runs in a row"),
112 ok: words_ok,
113 detail: words_detail,
114 tick: None,
115 },
116 Item {
117 label: format!(
118 "Real text at {:.0}% of baseline, {REAL_ACCURACY:.0}%+ accuracy",
119 SPEED_SHARE * 100.0
120 ),
121 ok: real_ok,
122 detail: real_detail,
123 tick: None,
124 },
125 Item {
126 label: format!(
127 "Used {} in real editing without fumbling",
128 layout::vim(stage).join(" ")
129 ),
130 ok: manual.vim,
131 detail: String::new(),
132 tick: Some(Tick::Vim),
133 },
134 Item {
135 label: "A full workday without thinking about the moved keys".into(),
136 ok: manual.workday,
137 detail: String::new(),
138 tick: Some(Tick::Workday),
139 },
140 ]
141}
142
143pub fn passed(progress: &Progress, stage: usize) -> bool {
144 items(progress, stage).iter().all(|i| i.ok)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::store::Run;
151
152 fn run(stage: usize, mode: &str, wpm: f64, acc: f64) -> Run {
153 Run {
154 t: 0,
155 stage,
156 mode: mode.into(),
157 wpm,
158 acc,
159 secs: 60.0,
160 }
161 }
162
163 #[test]
164 fn stage_zero_passes_with_a_typed_baseline_and_the_box() {
165 let mut p = Progress::default();
166 assert!(!passed(&p, 0));
167 p.manual_mut(0).wpm = 35.0;
168 Tick::Kept.toggle(&mut p, 0);
169 assert!(passed(&p, 0));
170 }
171
172 #[test]
173 fn stage_one_needs_three_accurate_runs_speed_and_both_boxes() {
174 let mut p = Progress::default();
175 p.manual_mut(0).wpm = 30.0;
176 for acc in [98.0, 99.0, 97.5] {
177 p.runs.push(run(1, "words", 20.0, acc));
178 }
179 p.runs.push(run(1, "real", 28.0, 96.0));
180 assert!(!passed(&p, 1));
181 Tick::Vim.toggle(&mut p, 1);
182 Tick::Workday.toggle(&mut p, 1);
183 assert!(passed(&p, 1));
184
185 p.runs.push(run(1, "words", 25.0, 90.0));
186 assert!(!passed(&p, 1), "a sloppy run resets the run of three");
187 }
188
189 #[test]
190 fn slow_or_sloppy_real_text_does_not_count() {
191 let mut p = Progress::default();
192 p.manual_mut(0).wpm = 30.0;
193 p.runs.push(run(1, "real", 26.0, 99.0));
194 p.runs.push(run(1, "real", 40.0, 90.0));
195 assert!(!items(&p, 1)[1].ok);
196 }
197}