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