konnektoren_core/commands/
challenge_command.rs

1//! This module contains the implementation of challenge-level commands.
2
3use super::command::CommandTrait;
4use super::command_type::CommandType;
5use crate::challenges::Timed;
6use crate::challenges::error::ChallengeError;
7use crate::challenges::{
8    Challenge, ChallengeInput, ChallengeResult, ChallengeType, ContextItemChoiceAnswers,
9    GapFillAnswer, MultipleChoiceOption, OrderingResult, Solvable, SortTableRow,
10};
11use crate::commands::error::{CommandError, Result};
12use crate::game::GamePath;
13use crate::game::GameState;
14use crate::game::error::GameError;
15
16/// Represents challenge-level commands that can be executed on the game state.
17#[allow(clippy::large_enum_variant)]
18#[derive(Debug, Clone, PartialEq)]
19pub enum ChallengeCommand {
20    Start(Challenge),
21    /// Command to move to the next task within a challenge.
22    NextTask,
23    /// Command to move to the previous task within a challenge.
24    PreviousTask,
25    /// Command to solve a multiple choice option.
26    SolveOption(usize),
27    /// Command to finish the challenge with a custom result.
28    Finish(Option<ChallengeResult>),
29}
30
31impl CommandTrait for ChallengeCommand {
32    /// Executes the challenge command on the given game state.
33    ///
34    /// # Arguments
35    ///
36    /// * `state` - A mutable reference to the current game state.
37    ///
38    /// # Returns
39    ///
40    /// A `Result` indicating success or containing an error if the command execution failed.
41    fn execute(&self, state: &mut GameState) -> Result<()> {
42        match self {
43            ChallengeCommand::Start(challenge_type) => Self::start_challenge(state, challenge_type),
44            ChallengeCommand::NextTask => Self::next_task(state),
45            ChallengeCommand::PreviousTask => Self::previous_task(state),
46            ChallengeCommand::SolveOption(option_index) => Self::solve_option(state, *option_index),
47            ChallengeCommand::Finish(result) => Self::finish_challenge(state, result),
48        }
49    }
50
51    /// Gets the type of the command.
52    fn get_type(&self) -> CommandType {
53        CommandType::Challenge
54    }
55}
56
57impl ChallengeCommand {
58    /// Starts a new challenge with the given challenge configuration.
59    fn start_challenge(state: &mut GameState, challenge: &Challenge) -> Result<()> {
60        let mut challenge = challenge.clone();
61        challenge.start();
62        state.challenge = challenge;
63        state.current_task_index = 0;
64        Ok(())
65    }
66
67    /// Moves the game state to the next task within the current challenge.
68    ///
69    /// # Arguments
70    ///
71    /// * `state` - A mutable reference to the current game state.
72    ///
73    /// # Returns
74    ///
75    /// A `Result` indicating success or containing an error if there are no more tasks.
76    fn next_task(state: &mut GameState) -> Result<()> {
77        let current_game_path: &GamePath = state
78            .game
79            .game_paths
80            .get(state.current_game_path)
81            .ok_or(CommandError::GameError(GameError::GamePathNotFound))?;
82
83        let challenge_config = &current_game_path.challenges[state.current_challenge_index];
84        let max_questions = challenge_config.tasks.len();
85
86        if state.current_task_index >= max_questions - 1 {
87            return Err(CommandError::ChallengeError(ChallengeError::NoMoreTasks));
88        }
89
90        // QUICKFIX: If current task wasn't answered, add a default answer to keep indices aligned
91        let result_len = state.challenge.challenge_result.len();
92        if result_len <= state.current_task_index {
93            // Add default answer based on challenge type
94            let default_input = match &state.challenge.challenge_type {
95                ChallengeType::MultipleChoice(mc) => {
96                    if let Some(first_option) = mc.options.first() {
97                        ChallengeInput::MultipleChoice(MultipleChoiceOption {
98                            id: first_option.id,
99                            name: first_option.name.clone(),
100                        })
101                    } else {
102                        return Err(CommandError::ChallengeError(
103                            ChallengeError::InvalidChallengeType,
104                        ));
105                    }
106                }
107                ChallengeType::ContextualChoice(cc) => {
108                    if let Some(first_item) = cc.items.first() {
109                        // Create default answer with empty choices
110                        let ids = vec![0; first_item.choices.len()];
111                        ChallengeInput::ContextualChoice(ContextItemChoiceAnswers { ids })
112                    } else {
113                        return Err(CommandError::ChallengeError(
114                            ChallengeError::InvalidChallengeType,
115                        ));
116                    }
117                }
118                ChallengeType::GapFill(_) => ChallengeInput::GapFill(GapFillAnswer {
119                    question_index: state.current_task_index,
120                    answers: vec![],
121                }),
122                ChallengeType::SortTable(_) => ChallengeInput::SortTable(SortTableRow::default()),
123                ChallengeType::Ordering(_) => ChallengeInput::Ordering(OrderingResult::default()),
124                _ => {
125                    // For other types, don't add default input
126                    state.current_task_index += 1;
127                    return Ok(());
128                }
129            };
130
131            let _ = state.challenge.challenge_result.add_input(default_input);
132        }
133
134        state.current_task_index += 1;
135        Ok(())
136    }
137
138    /// Moves the game state to the previous task within the current challenge.
139    ///
140    /// # Arguments
141    ///
142    /// * `state` - A mutable reference to the current game state.
143    ///
144    /// # Returns
145    ///
146    /// A `Result` indicating success or containing an error if there are no previous tasks.
147    fn previous_task(state: &mut GameState) -> Result<()> {
148        if state.current_task_index == 0 {
149            return Err(CommandError::ChallengeError(
150                ChallengeError::NoPreviousTasks,
151            ));
152        }
153        state.current_task_index -= 1;
154        Ok(())
155    }
156
157    /// Solves the current task with the selected option and moves to the next task.
158    ///
159    /// # Arguments
160    ///
161    /// * `state` - A mutable reference to the current game state.
162    /// * `option_index` - The index of the selected option.
163    ///
164    /// # Returns
165    ///
166    /// A `Result` indicating success or containing an error if the solution is invalid.
167    fn solve_option(state: &mut GameState, option_index: usize) -> Result<()> {
168        let challenge_input = match state.challenge.challenge_type {
169            ChallengeType::MultipleChoice(ref dataset) => {
170                let option =
171                    dataset
172                        .options
173                        .get(option_index)
174                        .ok_or(CommandError::ChallengeError(
175                            ChallengeError::InvalidOptionId(option_index),
176                        ))?;
177
178                ChallengeInput::MultipleChoice(MultipleChoiceOption {
179                    id: option.id,
180                    name: option.name.clone(),
181                })
182            }
183            _ => {
184                return Err(CommandError::ChallengeError(
185                    ChallengeError::InvalidChallengeType,
186                ));
187            }
188        };
189
190        state
191            .challenge
192            .solve(challenge_input, state.current_task_index)
193            .map_err(CommandError::ChallengeError)?;
194
195        // Attempt to move to the next task, but ignore "no more tasks" errors
196        let _ = Self::next_task(state);
197
198        Ok(())
199    }
200
201    /// Finishes the current challenge with a custom result.
202    fn finish_challenge(
203        state: &mut GameState,
204        custom_result: &Option<ChallengeResult>,
205    ) -> Result<()> {
206        state.challenge.update_end_time();
207        // Logic to handle finishing the challenge
208        if let Some(result) = custom_result {
209            state.challenge.challenge_result = result.clone();
210        }
211        // Additional logic to mark the challenge as finished
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::game::GameState;
220
221    #[test]
222    fn test_solve_option() {
223        let mut state = GameState::default();
224        state.current_game_path = 0;
225        state.current_challenge_index = 0;
226        state.current_task_index = 0;
227
228        let result = ChallengeCommand::solve_option(&mut state, 0);
229        assert!(result.is_ok());
230        assert_eq!(state.current_task_index, 1);
231    }
232
233    #[test]
234    fn test_solve_option_invalid() {
235        let mut state = GameState::default();
236        state.current_game_path = 0;
237        state.current_challenge_index = 0;
238        state.current_task_index = 0;
239
240        // Try to solve with an invalid option index
241        let result = ChallengeCommand::solve_option(&mut state, 999);
242        assert!(result.is_err());
243
244        // Verify error type
245        if let Err(error) = result {
246            match error {
247                CommandError::ChallengeError(ChallengeError::InvalidOptionId(999)) => {}
248                _ => panic!("Unexpected error type: {:?}", error),
249            }
250        }
251    }
252
253    #[test]
254    fn test_next_task() {
255        let mut state = GameState::default();
256        state.current_game_path = 0;
257        state.current_challenge_index = 0;
258        state.current_task_index = 0;
259
260        let result = ChallengeCommand::next_task(&mut state);
261        assert!(result.is_ok());
262        assert_eq!(state.current_task_index, 1);
263    }
264
265    #[test]
266    fn test_next_task_no_more() {
267        let mut state = GameState::default();
268        state.current_game_path = 0;
269        state.current_challenge_index = 0;
270
271        // Set to last task
272        let current_game_path = &state.game.game_paths[state.current_game_path];
273        let challenge_config = &current_game_path.challenges[state.current_challenge_index];
274        let max_tasks = challenge_config.tasks.len();
275        state.current_task_index = max_tasks - 1;
276
277        let result = ChallengeCommand::next_task(&mut state);
278        assert!(result.is_err());
279
280        // Verify error type
281        if let Err(error) = result {
282            match error {
283                CommandError::ChallengeError(ChallengeError::NoMoreTasks) => {}
284                _ => panic!("Unexpected error type: {:?}", error),
285            }
286        }
287    }
288
289    #[test]
290    fn test_previous_task() {
291        let mut state = GameState::default();
292        state.current_game_path = 0;
293        state.current_challenge_index = 0;
294        state.current_task_index = 1;
295
296        let result = ChallengeCommand::previous_task(&mut state);
297        assert!(result.is_ok());
298        assert_eq!(state.current_task_index, 0);
299    }
300
301    #[test]
302    fn test_previous_task_no_more() {
303        let mut state = GameState::default();
304        state.current_game_path = 0;
305        state.current_challenge_index = 0;
306        state.current_task_index = 0;
307
308        let result = ChallengeCommand::previous_task(&mut state);
309        assert!(result.is_err());
310
311        // Verify error type
312        if let Err(error) = result {
313            match error {
314                CommandError::ChallengeError(ChallengeError::NoPreviousTasks) => {}
315                _ => panic!("Unexpected error type: {:?}", error),
316            }
317        }
318    }
319
320    #[test]
321    fn test_finish_challenge() {
322        let mut state = GameState::default();
323        state.current_game_path = 0;
324        state.current_challenge_index = 0;
325        state.current_task_index = 1;
326
327        let result = ChallengeCommand::finish_challenge(&mut state, &None);
328        assert!(result.is_ok());
329    }
330
331    #[test]
332    fn test_execute() {
333        let mut state = GameState::default();
334        state.current_game_path = 0;
335        state.current_challenge_index = 0;
336        state.current_task_index = 0;
337
338        let result = ChallengeCommand::SolveOption(0).execute(&mut state);
339        assert!(result.is_ok());
340        assert_eq!(state.current_task_index, 1);
341    }
342
343    #[test]
344    fn test_invalid_challenge_type() {
345        // Create a challenge with a non-multiple-choice type
346        let mut state = GameState::default();
347
348        // Modify challenge type to something other than MultipleChoice
349        // This is a bit of a hack for testing - ideally we'd create a proper challenge with a different type
350        state.challenge.challenge_type = ChallengeType::Informative(Default::default());
351
352        let result = ChallengeCommand::solve_option(&mut state, 0);
353        assert!(result.is_err());
354
355        // Verify error type
356        if let Err(error) = result {
357            match error {
358                CommandError::ChallengeError(ChallengeError::InvalidChallengeType) => {}
359                _ => panic!("Unexpected error type: {:?}", error),
360            }
361        }
362    }
363
364    #[test]
365    fn test_game_path_not_found() {
366        let mut state = GameState::default();
367        state.current_game_path = 999; // Invalid index
368
369        let result = ChallengeCommand::next_task(&mut state);
370        assert!(result.is_err());
371
372        // Verify error type
373        if let Err(error) = result {
374            match error {
375                CommandError::GameError(GameError::GamePathNotFound) => {}
376                _ => panic!("Unexpected error type: {:?}", error),
377            }
378        }
379    }
380}