forne/driver.rs
1use crate::{
2 methods::{Method, RawMethod},
3 set::{Card, CardType, Set, SlimCard},
4};
5use anyhow::{bail, Error, Result};
6use lazy_static::lazy_static;
7use rand::{distributions::WeightedError, seq::SliceRandom};
8use rhai::Engine;
9use uuid::Uuid;
10
11lazy_static! {
12 // Fight me.
13 static ref TEST_RESPONSES: &'static [String] = Box::leak(Box::new(["y".to_string(), "n".to_string()]));
14}
15
16/// A system to drive user interactions forward by providing a re-entrant polling architecture. The caller should call `.next()`
17/// to get the next question/answer pair, providing the user's response for the previous question so the driver can update the set
18/// as necessary. This architecture allows the caller much greater control over the order of execution and display than an interface
19/// type opaquely driven by this library would.
20pub struct Driver<'e, 's> {
21 /// The learning method used. This is responsible for the majority of the logic.
22 ///
23 /// If this is `None`, the driver will run a test instead of a learning session, which uses custom and internal logic.
24 method: Option<Method<'e>>,
25 /// A mutable reference to the set we're operating on.
26 set: &'s mut Set,
27 /// The unique identifier of the last card returned by `.next()` or `.first()`.
28 // We can't store a mutable reference to the latest card directly here, because the lifetimes wouldn't work out at all, and this
29 // is a really subtle bug that Rust picks up on uniquely and superbly!
30 latest_card: Option<Uuid>,
31 /// The maximum number of elements to review, if one has been set.
32 max_count: Option<u32>,
33 /// The number of cards we've reviewed so far.
34 curr_count: u32,
35 /// The type of cards to be targeted by this driver.
36 target: CardType,
37
38 /// Whether or not we should mark cards that the user gets wrong as starred in tests.
39 mark_starred: bool,
40 /// Whether or not the learning method should be allowed to change the difficulty status of cards.
41 mutate_difficulty: bool,
42 /// Whether or not we should mark cards that the user gets right in tests as unstarred.
43 ///
44 /// This is especially useful when there are a small number of cards that the user is getting wrong consistently, which
45 /// they want to continue keeping track of, while also still reviewing them many times.
46 mark_unstarred: bool,
47}
48impl<'e, 's> Driver<'e, 's> {
49 /// Creates a new driver with the given set and method, with the latter provided as either the name of an inbuilt method or the body of
50 /// a custom Rhai script.
51 ///
52 /// # Errors
53 ///
54 /// This will return an error if the given method has not previously been used with this set, and a reset must be performed in that case,
55 /// which will lead to the loss of previous progress, unless a transformer is used.
56 pub(crate) fn new_learn(
57 set: &'s mut Set,
58 raw_method: RawMethod,
59 engine: &'e Engine,
60 ) -> Result<Self> {
61 let method = raw_method.into_method(engine)?;
62 let instance = Self {
63 method: Some(method),
64 set,
65 max_count: None,
66 curr_count: 0,
67 target: CardType::All,
68 latest_card: None,
69
70 mark_starred: true,
71 mutate_difficulty: true,
72 mark_unstarred: true,
73 };
74 if !instance.method_correct() {
75 bail!("given method is not the same as the one that has been previously used for this set (please reset the set before attempting to use a new method)");
76 }
77
78 Ok(instance)
79 }
80 /// Creates a new driver with the given set, running in test mode. This takes no custom method, as it runs a test, and it is infallible.
81 pub(crate) fn new_test(set: &'s mut Set) -> Self {
82 Self {
83 method: None,
84 set,
85 max_count: None,
86 curr_count: 0,
87 target: CardType::All,
88 latest_card: None,
89
90 mark_starred: true,
91 mutate_difficulty: true,
92 mark_unstarred: true,
93 }
94 }
95 /// Sets a specific type of card that this driver will exclusively target. By default, drivers target all cards.
96 pub fn set_target(&mut self, target: CardType) -> &mut Self {
97 self.target = target;
98 self
99 }
100 /// Sets a maximum number of elements to be reviewed through this driver. This can be useful for long-term learning, in which you only
101 /// want to review, say, 30 cards per day.
102 ///
103 /// Obviously, if there are not enough cards to reach this maximum count, the driver will stop before the count is reached, and will
104 /// not go back to the beginning.
105 pub fn set_max_count(&mut self, count: u32) -> &mut Self {
106 self.max_count = Some(count);
107 self
108 }
109 /// If this driver is being used to run a test, prevents cards the user gets wrong from being automatically starred.
110 pub fn no_mark_starred(&mut self) -> &mut Self {
111 self.mark_starred = false;
112 self
113 }
114 /// If this driver is being used to run a test, prevents cards the user gets right from being unstarred if they were previously starred.
115 ///
116 /// This is especially useful when there are a small number of cards that the user is getting wrong consistently, which
117 /// they want to continue keeping track of, while also still reviewing them many times.
118 pub fn no_mark_unstarred(&mut self) -> &mut Self {
119 self.mark_unstarred = false;
120 self
121 }
122 /// If this driver is being used to run a learning session, prevents the learning method from marking cards as difficult, or from downgrading
123 /// cards currently marked as difficult to no longer difficult.
124 ///
125 /// Since the `difficult` metadatum is almost entirely internal, there are generally very few scenarios in which this behaviour is desired.
126 pub fn no_mutate_difficulty(&mut self) -> &mut Self {
127 self.mutate_difficulty = false;
128 self
129 }
130 /// Gets the number of cards that have been reviewed by this driver so far.
131 pub fn get_count(&self) -> u32 {
132 self.curr_count
133 }
134 /// Performs a sanity check that the method this driver has been instantiated with is the same as the one that has been being used for the set.
135 fn method_correct(&self) -> bool {
136 if let Some(method) = &self.method {
137 method.name == self.set.method
138 } else {
139 // We're running a test
140 true
141 }
142 }
143 /// Gets the first question/answer pair of this run. While it is perfectly safe to run this at any time, it
144 /// is semantically nonsensical to run this more than once, as Forn's internals will become completely
145 /// useless. If you want to display each card to the user only once, irrespective of the metadata attached to
146 /// it, you should instantiate the driver for a test, rather than a learning session.
147 ///
148 /// This will return `None` if there are no more cards with non-zero weightings, in which case the learn or test
149 /// session described by this driver is complete. (If progress is not cleared, this could easily happen with a
150 /// `.first()` call.)
151 ///
152 /// This will automatically continue the most recent session of either learning or testing, if there is one.
153 // No instance can be constructed without first checking if the method matches the set, so assuming it does
154 // is perfectly safe here.
155 pub fn first(&mut self) -> Result<Option<SlimCard>> {
156 let mut rng = rand::thread_rng();
157
158 // Update the set's state for either learning or testing
159 if let Some(method) = &self.method {
160 self.set.run_state = Some(method.name.clone());
161 } else {
162 self.set.test_in_progress = true;
163 }
164
165 if self.max_count.is_some() && self.max_count.unwrap() == self.curr_count {
166 return Ok(None);
167 }
168
169 // Randomly select a card according to the weights generated by the method
170 let mut cards_with_ids = self.set.cards.iter().collect::<Vec<_>>();
171 let (card_id, card) =
172 match cards_with_ids.choose_weighted_mut(&mut rng, |(_, card): &(&Uuid, &Card)| {
173 if let Some(method) = &self.method {
174 let res = match &self.target {
175 CardType::All => {
176 (method.get_weight)(card.method_data.clone(), card.difficult)
177 }
178 CardType::Starred if card.starred => {
179 (method.get_weight)(card.method_data.clone(), card.difficult)
180 }
181 CardType::Difficult if card.difficult => {
182 (method.get_weight)(card.method_data.clone(), card.difficult)
183 }
184 _ => Ok(0.0),
185 };
186 // TODO handle errors (very realistic that they would occur with custom scripts!)
187 res.unwrap()
188 } else {
189 match &self.target {
190 CardType::All if !card.seen_in_test => 1.0,
191 CardType::Starred if card.starred && !card.seen_in_test => 1.0,
192 CardType::Difficult if card.difficult && !card.seen_in_test => 1.0,
193 _ => 0.0,
194 }
195 }
196 }) {
197 Ok(data) => data,
198 // We're done!
199 Err(WeightedError::AllWeightsZero) => {
200 // If we've genuinely finished, say so
201 if self.method.is_some() {
202 self.set.run_state = None;
203 } else {
204 self.set.test_in_progress = false;
205 self.set.reset_test();
206 }
207
208 return Ok(None);
209 }
210 Err(err) => return Err(Error::new(err)),
211 };
212
213 // Using a slim representation avoids potentially expensive cloning of the `Dynamic` data the method
214 // maintains about this card
215 let slim = SlimCard {
216 question: card.question.clone(),
217 answer: card.answer.clone(),
218 starred: card.starred,
219 difficult: card.difficult,
220 };
221
222 self.latest_card = Some(**card_id);
223 self.curr_count += 1;
224
225 Ok(Some(slim))
226 }
227 /// Provides the allowed responses for this learn method (or for the test, if this driver is being used for
228 /// a test), in the order they were defined. The argument to `.next()` must be an element in the list this
229 /// returns.
230 pub fn allowed_responses(&self) -> &[String] {
231 if let Some(method) = &self.method {
232 &method.responses
233 } else {
234 &TEST_RESPONSES
235 }
236 }
237 /// Gets the next question/answer pair, given a response to the last question/answer. If this is the first,
238 /// you should call `.first()` instead, as calling this will lead to an error. Note that the provided response
239 /// must be *identical* to one of the responses defined by the method in use (these can be found with `.allowed_responses()`).
240 pub fn next(&mut self, response: String) -> Result<Option<SlimCard>> {
241 if !self.allowed_responses().iter().any(|x| x == &response) {
242 bail!("invalid user response to card");
243 }
244
245 if let Some(card_id) = self.latest_card.as_mut() {
246 // We know this element exists (we hold the only mutable reference to the set)
247 let card = self.set.cards.get_mut(card_id).unwrap();
248 if let Some(method) = &self.method {
249 let (method_data, difficult) =
250 (method.adjust_card)(response, card.method_data.clone(), card.difficult)?;
251 card.method_data = method_data;
252 if self.mutate_difficulty {
253 card.difficult = difficult;
254 }
255 } else {
256 card.seen_in_test = true;
257
258 if response == "n" && self.mark_starred {
259 card.starred = true;
260 } else if response == "y" && self.mark_unstarred {
261 card.starred = false;
262 }
263
264 // Prevent this card from being double-adjusted if there's an error later
265 self.latest_card = None;
266 }
267
268 // Everything has been adjusted
269 self.first()
270 } else {
271 bail!("called `.next()` before `.first()`, or without handling error");
272 }
273 }
274 /// Saves the underlying set to JSON. This should generally be called between each presentation of a card to ensure the user
275 /// does not lose their progress.
276 pub fn save_set_to_json(&self) -> Result<String> {
277 self.set.save()
278 }
279}