lattice_host/tutor/
engine.rs1use crate::action::EchoLevel;
4use crate::buffers::BufferId;
5use crate::dispatch::{DoEditOutcome, RendererSignal};
6use crate::editor::Editor;
7
8impl Editor {
9 pub fn do_tutor(&mut self, lesson: Option<u32>) -> Vec<RendererSignal> {
14 let lesson_num = lesson.unwrap_or(1);
15 let (lesson_text, exercises_toml): (&'static str, &'static str) = match lesson_num {
16 1 => (
17 include_str!("../../../../docs/user/tutor/lesson-1.md"),
18 include_str!("../../../../docs/user/tutor/lesson-1.exercises.toml"),
19 ),
20 2 => (
21 include_str!("../../../../docs/user/tutor/lesson-2.md"),
22 include_str!("../../../../docs/user/tutor/lesson-2.exercises.toml"),
23 ),
24 3 => (
25 include_str!("../../../../docs/user/tutor/lesson-3.md"),
26 include_str!("../../../../docs/user/tutor/lesson-3.exercises.toml"),
27 ),
28 4 => (
29 include_str!("../../../../docs/user/tutor/lesson-4.md"),
30 include_str!("../../../../docs/user/tutor/lesson-4.exercises.toml"),
31 ),
32 5 => (
33 include_str!("../../../../docs/user/tutor/lesson-5.md"),
34 include_str!("../../../../docs/user/tutor/lesson-5.exercises.toml"),
35 ),
36 6 => (
37 include_str!("../../../../docs/user/tutor/lesson-6.md"),
38 include_str!("../../../../docs/user/tutor/lesson-6.exercises.toml"),
39 ),
40 7 => (
41 include_str!("../../../../docs/user/tutor/lesson-7.md"),
42 include_str!("../../../../docs/user/tutor/lesson-7.exercises.toml"),
43 ),
44 n => {
45 self.set_message(
46 EchoLevel::Error,
47 format!(
48 "lesson {n} doesn't exist (lessons 1-7 available); \
49 contributions welcome"
50 ),
51 );
52 return Vec::new();
53 }
54 };
55 let mut path = std::env::temp_dir();
56 path.push(format!("lattice-tutor-lesson-{lesson_num}.md"));
57 if let Err(e) = std::fs::write(&path, lesson_text) {
58 self.set_message(
59 EchoLevel::Error,
60 format!("tutor: failed to write lesson file: {e}"),
61 );
62 return Vec::new();
63 }
64 let outcome = self.do_edit(Some(path), false);
65 let signals = match outcome {
66 DoEditOutcome::Opened(s) | DoEditOutcome::Activated(s) | DoEditOutcome::Reloaded(s) => {
67 s
68 }
69 DoEditOutcome::Directory(d) => self.do_open_oil(Some(d)),
70 DoEditOutcome::NoFileName | DoEditOutcome::Failed => return Vec::new(),
71 };
72 let buffer_id = self.document_buffer_id;
75 let mut session =
76 match crate::tutor::TutorSession::load(lesson_num, 7, lesson_text, exercises_toml) {
77 Ok(s) => s,
78 Err(e) => {
79 self.set_message(
80 EchoLevel::Error,
81 format!("tutor: failed to load session: {e}"),
82 );
83 return signals;
84 }
85 };
86 session.high_score = crate::tutor::TutorScores::load_or_default().high_score(lesson_num);
88 let provider_id = buffer_id.0 as u64 ^ crate::tutor::TUTOR_PROVIDER_TAG;
90 self.virtual_row_providers
91 .unregister(buffer_id, provider_id);
92 let handle = lattice_cells::SimpleHeaderlineHandle::new(
93 crate::tutor::TutorViewState::default(),
94 crate::tutor::render_tutor_headerline,
95 );
96 handle.update(|s| s.update_for_display(&session, crate::tutor::TutorHudKind::Normal));
97 self.virtual_row_providers
98 .register(buffer_id, std::sync::Arc::new(handle.provider(provider_id)));
99 self.buffer_locals
101 .entry(buffer_id)
102 .or_default()
103 .insert(crate::tutor::TutorHeaderlineState(handle));
104 self.buffer_locals
105 .entry(buffer_id)
106 .or_default()
107 .insert(session);
108 let proto_id = lattice_protocol::ids::BufferId::new(buffer_id.0 as u64);
110 let mut active = self.active_modes.remove(&buffer_id).unwrap_or_default();
111 if let Err(e) = self.mode_registry.load_full().activate_minor(
112 &mut active,
113 &self.mode_guards,
114 &self.config,
115 &self.event_bus,
116 &self.services,
117 proto_id,
118 crate::tutor::TutorMode::mode_id(),
119 self.capabilities_for_proto(proto_id),
120 ) {
121 self.note_activation_failure(buffer_id, crate::tutor::TutorMode::mode_id(), &e);
122 }
123 self.active_modes.insert(buffer_id, active);
124 signals
125 }
126
127 pub fn do_tutor_advance(&mut self) -> Vec<RendererSignal> {
138 use crate::tutor::{SuccessCondition, TutorGameState};
139
140 let buffer_id = self.document_buffer_id;
141 let Some(mut session) = self
142 .buffer_locals
143 .get(&buffer_id)
144 .and_then(|l| l.get::<crate::tutor::TutorSession>())
145 .cloned()
146 else {
147 return Vec::new();
148 };
149
150 if session.state == TutorGameState::GameOver {
152 session.advance();
153 self.buffer_locals
154 .entry(buffer_id)
155 .or_default()
156 .insert(session.clone());
157 return self.tutor_after_advance(buffer_id, session);
158 }
159
160 if session.is_complete() {
162 return self.tutor_open_next_or_complete(session);
163 }
164
165 let is_manual = matches!(
166 session.current_exercise().map(|e| &e.success),
167 Some(SuccessCondition::ManualAdvance)
168 );
169
170 if is_manual {
171 session.advance();
172 self.buffer_locals
173 .entry(buffer_id)
174 .or_default()
175 .insert(session.clone());
176 return self.tutor_after_advance(buffer_id, session);
177 }
178
179 let text = self.document.snapshot().buffer.as_string();
181 let owned: Vec<String> = text.lines().map(|l| l.to_owned()).collect();
182 let lines: Vec<&str> = owned.iter().map(|s| s.as_str()).collect();
183
184 if session.is_condition_met(&lines) {
185 session.advance();
186 self.buffer_locals
187 .entry(buffer_id)
188 .or_default()
189 .insert(session.clone());
190 return self.tutor_after_advance(buffer_id, session);
191 }
192
193 session.drain_life();
195 self.tutor_update_headerline(buffer_id, &session, crate::tutor::TutorHudKind::Normal);
196 self.buffer_locals
197 .entry(buffer_id)
198 .or_default()
199 .insert(session);
200 Vec::new()
201 }
202
203 pub fn do_tutor_retreat(&mut self) -> Vec<RendererSignal> {
208 use crate::tutor::TutorGameState;
209
210 let buffer_id = self.document_buffer_id;
211 let Some(session) = self
212 .buffer_locals
213 .get(&buffer_id)
214 .and_then(|l| l.get::<crate::tutor::TutorSession>())
215 .cloned()
216 else {
217 return Vec::new();
218 };
219
220 if session.state == TutorGameState::GameOver {
221 return self.do_tutor(Some(session.lesson));
223 }
224
225 let mut session = session;
226 session.retreat();
227 self.tutor_update_headerline(buffer_id, &session, crate::tutor::TutorHudKind::Normal);
228 self.buffer_locals
229 .entry(buffer_id)
230 .or_default()
231 .insert(session);
232 Vec::new()
233 }
234
235 fn tutor_after_advance(
238 &mut self,
239 buffer_id: BufferId,
240 session: crate::tutor::TutorSession,
241 ) -> Vec<RendererSignal> {
242 if session.is_complete() {
243 return self.tutor_open_next_or_complete(session);
244 }
245 self.tutor_update_headerline(buffer_id, &session, crate::tutor::TutorHudKind::Normal);
246 self.buffer_locals
247 .entry(buffer_id)
248 .or_default()
249 .insert(session);
250 Vec::new()
251 }
252
253 fn tutor_open_next_or_complete(
256 &mut self,
257 mut session: crate::tutor::TutorSession,
258 ) -> Vec<RendererSignal> {
259 use crate::tutor::TutorGameState;
260 let mut scores = crate::tutor::TutorScores::load_or_default();
262 let new_record = scores.record(session.lesson, session.score);
263 scores.save();
264 if new_record {
265 session.high_score = session.score;
266 }
267 let next = session.lesson + 1;
268 if next <= session.total_lessons {
269 return self.do_tutor(Some(next));
270 }
271 let buffer_id = self.document_buffer_id;
273 session.state = TutorGameState::AllComplete;
274 self.tutor_update_headerline(buffer_id, &session, crate::tutor::TutorHudKind::Normal);
275 self.buffer_locals
276 .entry(buffer_id)
277 .or_default()
278 .insert(session);
279 Vec::new()
280 }
281
282 fn tutor_update_headerline(
285 &self,
286 buffer_id: BufferId,
287 session: &crate::tutor::TutorSession,
288 kind: crate::tutor::TutorHudKind,
289 ) {
290 if let Some(state) = self
291 .buffer_locals
292 .get(&buffer_id)
293 .and_then(|l| l.get::<crate::tutor::TutorHeaderlineState>())
294 {
295 state.0.update(|s| s.update_for_display(session, kind));
296 }
297 }
298
299 pub(crate) fn check_tutor_session(&mut self) {
304 let buffer_id = self.document_buffer_id;
305 let Some(mut session) = self
306 .buffer_locals
307 .get(&buffer_id)
308 .and_then(|l| l.get::<crate::tutor::TutorSession>())
309 .cloned()
310 else {
311 return;
312 };
313 use crate::tutor::TutorGameState;
314 if session.state == TutorGameState::GameOver || session.is_complete() {
315 return;
316 }
317 let current_version = self.document.version();
318 if current_version == session.last_version {
319 return;
320 }
321 session.last_version = current_version;
322 let text = self.document.snapshot().buffer.as_string();
323 let owned: Vec<String> = text.lines().map(|l| l.to_owned()).collect();
324 let lines: Vec<&str> = owned.iter().map(|s| s.as_str()).collect();
325 let success = session.check(&lines);
326 let kind = if success {
327 crate::tutor::TutorHudKind::StageClear
328 } else {
329 crate::tutor::TutorHudKind::Normal
330 };
331 self.tutor_update_headerline(buffer_id, &session, kind);
332 self.buffer_locals
333 .entry(buffer_id)
334 .or_default()
335 .insert(session);
336 }
337}