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}