konnektoren_core/challenges/
challenge.rs1use 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 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()), 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); 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(); 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(); 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(); 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 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 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 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 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 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 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 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 assert!(
319 matches!(
320 challenge.challenge_result,
321 ChallengeResult::ContextualChoice(_)
322 ),
323 "Challenge result should be ContextualChoice type"
324 );
325
326 let correct_input = ChallengeInput::ContextualChoice(ContextItemChoiceAnswers {
328 ids: vec![0, 0], });
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 let mut challenge2 = Challenge::new(&challenge_type, &ChallengeConfig::default());
337 let incorrect_input = ChallengeInput::ContextualChoice(ContextItemChoiceAnswers {
338 ids: vec![1, 0], });
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}