konnektoren_core/challenges/
challenge.rs

1use crate::challenges::Timed;
2use crate::challenges::error::{ChallengeError, Result};
3use crate::challenges::{
4    ChallengeConfig, ChallengeInput, ChallengeResult, ChallengeType, CustomChallengeResult,
5};
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9use super::{Performance, Solvable};
10
11#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
12pub struct Challenge {
13    pub challenge_type: ChallengeType,
14    pub challenge_config: ChallengeConfig,
15    pub challenge_result: ChallengeResult,
16    pub start_time: Option<DateTime<Utc>>,
17    pub end_time: Option<DateTime<Utc>>,
18}
19
20impl Challenge {
21    pub fn new(challenge_type: &ChallengeType, challenge_config: &ChallengeConfig) -> Self {
22        // Initialize the result based on the challenge type
23        let challenge_result = match challenge_type {
24            ChallengeType::MultipleChoice(_) => ChallengeResult::MultipleChoice(Vec::new()),
25            ChallengeType::ContextualChoice(_) => ChallengeResult::ContextualChoice(Vec::new()),
26            ChallengeType::GapFill(_) => ChallengeResult::GapFill(Vec::new()),
27            ChallengeType::SortTable(_) => ChallengeResult::SortTable(Vec::new()),
28            ChallengeType::Informative(_) => ChallengeResult::Informative,
29            ChallengeType::Ordering(_) => ChallengeResult::Ordering(Vec::new()),
30            ChallengeType::Custom(_) => ChallengeResult::Custom(CustomChallengeResult::default()),
31            ChallengeType::Placeholder(_) => ChallengeResult::MultipleChoice(Vec::new()), // Placeholder uses MC
32            ChallengeType::Vocabulary(_) => ChallengeResult::Vocabulary,
33        };
34
35        Challenge {
36            challenge_type: challenge_type.clone(),
37            challenge_config: challenge_config.clone(),
38            challenge_result,
39            start_time: None,
40            end_time: None,
41        }
42    }
43
44    pub fn get_id(&self) -> String {
45        self.challenge_config.id.clone()
46    }
47
48    pub fn solved(&self) -> bool {
49        !self.challenge_result.is_empty()
50    }
51}
52
53impl Solvable for Challenge {
54    fn solve(&mut self, input: ChallengeInput, task_index: usize) -> Result<bool> {
55        self.update_end_time();
56
57        match self.challenge_result.set_input(task_index, input.clone()) {
58            Ok(_) => match (&self.challenge_type, &self.challenge_result) {
59                (ChallengeType::MultipleChoice(mc), ChallengeResult::MultipleChoice(results)) => {
60                    if let (Some(question), Some(result)) =
61                        (mc.questions.get(task_index), results.get(task_index))
62                    {
63                        Ok(question.option == result.id)
64                    } else {
65                        Ok(false)
66                    }
67                }
68                (
69                    ChallengeType::ContextualChoice(cc),
70                    ChallengeResult::ContextualChoice(results),
71                ) => {
72                    if let (Some(item), Some(choice)) =
73                        (cc.items.get(task_index), results.get(task_index))
74                    {
75                        Ok(item.choices.iter().zip(&choice.ids).all(|(c, &id)| {
76                            c.options
77                                .get(id)
78                                .is_some_and(|selected| *selected == c.correct_answer)
79                        }))
80                    } else {
81                        Ok(false)
82                    }
83                }
84                (ChallengeType::GapFill(gf), ChallengeResult::GapFill(results)) => {
85                    if let (Some(question), Some(answer)) =
86                        (gf.questions.get(task_index), results.get(task_index))
87                    {
88                        if question.gaps.len() != answer.answers.len() {
89                            return Ok(false);
90                        }
91
92                        Ok(question
93                            .gaps
94                            .iter()
95                            .zip(answer.answers.iter())
96                            .all(|(gap, ans)| gap.correct == *ans))
97                    } else {
98                        Ok(false)
99                    }
100                }
101                (ChallengeType::SortTable(st), ChallengeResult::SortTable(results)) => {
102                    if let (Some(row), Some(result)) =
103                        (st.rows.get(task_index), results.get(task_index))
104                    {
105                        Ok(row.values == result.values)
106                    } else {
107                        Ok(false)
108                    }
109                }
110                (ChallengeType::Informative(_), ChallengeResult::Informative) => Ok(true),
111                (ChallengeType::Custom(_), ChallengeResult::Custom(_)) => Ok(true),
112                _ => Err(ChallengeError::InvalidChallengeType),
113            },
114            Err(_) => Ok(false),
115        }
116    }
117}
118
119impl Performance for Challenge {
120    fn performance(&self, result: &ChallengeResult) -> u32 {
121        self.challenge_type.performance(result)
122    }
123}
124
125impl Timed for Challenge {
126    fn start(&mut self) {
127        self.start_time = Some(Utc::now());
128    }
129
130    fn update_end_time(&mut self) {
131        self.end_time = Some(Utc::now());
132    }
133
134    fn elapsed_time(&self) -> Option<Duration> {
135        if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
136            Some(end - start)
137        } else {
138            None
139        }
140    }
141
142    fn start_time(&self) -> Option<DateTime<Utc>> {
143        self.start_time
144    }
145
146    fn end_time(&self) -> Option<DateTime<Utc>> {
147        self.end_time
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::challenges::*;
155
156    #[test]
157    fn new_challenge() {
158        let challenge_type = ChallengeType::default();
159        let challenge_config = ChallengeConfig::default();
160        let challenge = Challenge::new(&challenge_type, &challenge_config);
161        assert_eq!(challenge.challenge_type, challenge_type);
162        assert_eq!(challenge.challenge_config, challenge_config);
163        assert_eq!(challenge.challenge_result, ChallengeResult::default());
164    }
165
166    #[test]
167    fn solve_challenge() {
168        let challenge_type = ChallengeType::default();
169        let challenge_config = ChallengeConfig::default();
170        let mut challenge = Challenge::new(&challenge_type, &challenge_config);
171        let input = ChallengeInput::MultipleChoice(MultipleChoiceOption::default());
172        let result = challenge.solve(input, 0); // ← Add task_index = 0
173        assert!(result.is_ok());
174        assert!(result.unwrap());
175    }
176
177    #[test]
178    fn performance_with_timer() {
179        let challenge_type = ChallengeType::default();
180        let challenge_config = ChallengeConfig::default();
181        let mut challenge = Challenge::new(&challenge_type, &challenge_config);
182        challenge.start();
183        let input = ChallengeInput::MultipleChoice(MultipleChoiceOption::default());
184        let result = challenge.solve(input, 0).unwrap(); // ← Add task_index = 0
185        assert!(result);
186        let performance = challenge.performance(&challenge.challenge_result);
187        let time_difference = challenge.end_time.unwrap() - challenge.start_time.unwrap();
188        assert!(performance >= time_difference.num_seconds() as u32);
189    }
190
191    #[test]
192    fn elapsed_time() {
193        let challenge_type = ChallengeType::default();
194        let challenge_config = ChallengeConfig::default();
195        let mut challenge = Challenge::new(&challenge_type, &challenge_config);
196        challenge.start();
197        std::thread::sleep(std::time::Duration::from_millis(1));
198        let input = ChallengeInput::MultipleChoice(MultipleChoiceOption::default());
199        let result = challenge.solve(input, 0).unwrap(); // ← Add task_index = 0
200        assert!(result);
201        let elapsed_time = challenge.elapsed_time().unwrap();
202        assert!(elapsed_time > Duration::zero());
203    }
204
205    #[test]
206    fn start_and_end_time() {
207        let challenge_type = ChallengeType::default();
208        let challenge_config = ChallengeConfig::default();
209        let mut challenge = Challenge::new(&challenge_type, &challenge_config);
210        challenge.start();
211        let start_time = challenge.start_time().unwrap();
212        std::thread::sleep(std::time::Duration::from_millis(1));
213        let input = ChallengeInput::MultipleChoice(MultipleChoiceOption::default());
214        let result = challenge.solve(input, 0).unwrap(); // ← Add task_index = 0
215        assert!(result);
216        let end_time = challenge.end_time().unwrap();
217        assert!(end_time > start_time);
218    }
219
220    #[test]
221    fn test_get_id_and_solved() {
222        let challenge_type = ChallengeType::default();
223        let challenge_config = ChallengeConfig::default();
224        let mut challenge = Challenge::new(&challenge_type, &challenge_config);
225        assert_eq!(challenge.get_id(), challenge_config.id);
226        assert!(!challenge.solved());
227        challenge.challenge_result =
228            ChallengeResult::MultipleChoice(vec![MultipleChoiceOption::default()]);
229        assert!(challenge.solved());
230    }
231
232    #[test]
233    fn test_challenge_result_initialization() {
234        // MultipleChoice
235        let mc_type = ChallengeType::MultipleChoice(MultipleChoice::default());
236        let mc_challenge = Challenge::new(&mc_type, &ChallengeConfig::default());
237        assert!(matches!(
238            mc_challenge.challenge_result,
239            ChallengeResult::MultipleChoice(_)
240        ));
241
242        // ContextualChoice
243        let cc_type = ChallengeType::ContextualChoice(ContextualChoice::default());
244        let cc_challenge = Challenge::new(&cc_type, &ChallengeConfig::default());
245        assert!(matches!(
246            cc_challenge.challenge_result,
247            ChallengeResult::ContextualChoice(_)
248        ));
249
250        // GapFill
251        let gf_type = ChallengeType::GapFill(GapFill::default());
252        let gf_challenge = Challenge::new(&gf_type, &ChallengeConfig::default());
253        assert!(matches!(
254            gf_challenge.challenge_result,
255            ChallengeResult::GapFill(_)
256        ));
257
258        // SortTable
259        let st_type = ChallengeType::SortTable(SortTable::default());
260        let st_challenge = Challenge::new(&st_type, &ChallengeConfig::default());
261        assert!(matches!(
262            st_challenge.challenge_result,
263            ChallengeResult::SortTable(_)
264        ));
265
266        // Informative
267        let inf_type = ChallengeType::Informative(Informative::default());
268        let inf_challenge = Challenge::new(&inf_type, &ChallengeConfig::default());
269        assert!(matches!(
270            inf_challenge.challenge_result,
271            ChallengeResult::Informative
272        ));
273
274        // Ordering
275        let ord_type = ChallengeType::Ordering(Ordering::default());
276        let ord_challenge = Challenge::new(&ord_type, &ChallengeConfig::default());
277        assert!(matches!(
278            ord_challenge.challenge_result,
279            ChallengeResult::Ordering(_)
280        ));
281
282        // Vocabulary
283        let voc_type = ChallengeType::Vocabulary(Vocabulary::default());
284        let voc_challenge = Challenge::new(&voc_type, &ChallengeConfig::default());
285        assert!(matches!(
286            voc_challenge.challenge_result,
287            ChallengeResult::Vocabulary
288        ));
289    }
290
291    #[test]
292    fn test_solve_contextual_choice() {
293        let contextual_choice = ContextualChoice {
294            id: "test".to_string(),
295            name: "Test".to_string(),
296            description: "Test".to_string(),
297            items: vec![ContextItem {
298                template: "Test {0} {1}".to_string(),
299                choices: vec![
300                    Choice {
301                        id: 0,
302                        options: vec!["correct".to_string(), "wrong".to_string()],
303                        correct_answer: "correct".to_string(),
304                    },
305                    Choice {
306                        id: 1,
307                        options: vec!["right".to_string(), "incorrect".to_string()],
308                        correct_answer: "right".to_string(),
309                    },
310                ],
311            }],
312        };
313
314        let challenge_type = ChallengeType::ContextualChoice(contextual_choice);
315        let mut challenge = Challenge::new(&challenge_type, &ChallengeConfig::default());
316
317        // Verify the challenge result was initialized correctly
318        assert!(
319            matches!(
320                challenge.challenge_result,
321                ChallengeResult::ContextualChoice(_)
322            ),
323            "Challenge result should be ContextualChoice type"
324        );
325
326        // Test correct answer
327        let correct_input = ChallengeInput::ContextualChoice(ContextItemChoiceAnswers {
328            ids: vec![0, 0], // Both correct (index 0 for both choices)
329        });
330
331        let result = challenge.solve(correct_input, 0);
332        assert!(result.is_ok(), "Solve should not error: {:?}", result);
333        assert!(result.unwrap(), "Should be correct");
334
335        // Test incorrect answer
336        let mut challenge2 = Challenge::new(&challenge_type, &ChallengeConfig::default());
337        let incorrect_input = ChallengeInput::ContextualChoice(ContextItemChoiceAnswers {
338            ids: vec![1, 0], // First wrong, second correct
339        });
340
341        let result = challenge2.solve(incorrect_input, 0);
342        assert!(result.is_ok(), "Solve should not error: {:?}", result);
343        assert!(!result.unwrap(), "Should be incorrect");
344    }
345}