arct_core/
challenge.rs

1//! Daily and weekly challenge system
2//!
3//! This module provides an engaging challenge system with daily tasks,
4//! weekly scenarios, and speed challenges to motivate continuous learning.
5
6use crate::lesson::{CommandValidation, Difficulty};
7use chrono::{Datelike, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10
11/// Represents a learning challenge that can be completed
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Challenge {
14    pub id: String,
15    pub title: String,
16    pub description: String,
17    pub difficulty: Difficulty,
18    pub challenge_type: ChallengeType,
19    pub points: u32,
20}
21
22/// Different types of challenges available
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum ChallengeType {
25    /// A single command task to complete daily
26    DailyCommand {
27        command: String,
28        task_description: String,
29        validation: CommandValidation,
30    },
31    /// Multi-step scenario challenge for weekly engagement
32    WeeklyScenario {
33        scenario: String,
34        steps: Vec<ChallengeStep>,
35    },
36    /// Timed challenge to complete tasks quickly
37    SpeedChallenge {
38        task: String,
39        time_limit_seconds: u32,
40        commands: Vec<String>,
41    },
42}
43
44/// A single step within a multi-step challenge
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ChallengeStep {
47    pub description: String,
48    pub validation: CommandValidation,
49    pub hint: Option<String>,
50}
51
52/// Manages active and completed challenges
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ChallengeManager {
55    /// Currently active daily challenge
56    pub daily_challenge: Option<Challenge>,
57    /// Currently active weekly challenge
58    pub weekly_challenge: Option<Challenge>,
59    /// Set of completed challenge IDs
60    pub completed_challenges: HashSet<String>,
61    /// Date when daily challenge was last generated
62    last_daily_generated: Option<chrono::NaiveDate>,
63    /// ISO week number when weekly challenge was last generated
64    last_weekly_generated: Option<u32>,
65}
66
67impl ChallengeManager {
68    /// Create a new challenge manager
69    pub fn new() -> Self {
70        Self {
71            daily_challenge: None,
72            weekly_challenge: None,
73            completed_challenges: HashSet::new(),
74            last_daily_generated: None,
75            last_weekly_generated: None,
76        }
77    }
78
79    /// Get or generate today's daily challenge
80    pub fn get_daily_challenge(&mut self) -> Challenge {
81        let today = Utc::now().date_naive();
82
83        // Check if we need to generate a new daily challenge
84        if self.last_daily_generated != Some(today) {
85            self.daily_challenge = Some(self.generate_daily_challenge(today));
86            self.last_daily_generated = Some(today);
87        }
88
89        self.daily_challenge.clone().unwrap()
90    }
91
92    /// Get or generate this week's weekly challenge
93    pub fn get_weekly_challenge(&mut self) -> Challenge {
94        let now = Utc::now();
95        let current_week = now.iso_week().week();
96
97        // Check if we need to generate a new weekly challenge
98        if self.last_weekly_generated != Some(current_week) {
99            self.weekly_challenge = Some(self.generate_weekly_challenge(current_week));
100            self.last_weekly_generated = Some(current_week);
101        }
102
103        self.weekly_challenge.clone().unwrap()
104    }
105
106    /// Mark a challenge as completed
107    pub fn complete_challenge(&mut self, challenge_id: String) {
108        self.completed_challenges.insert(challenge_id);
109    }
110
111    /// Check if a challenge has been completed
112    pub fn is_challenge_completed(&self, challenge_id: &str) -> bool {
113        self.completed_challenges.contains(challenge_id)
114    }
115
116    /// Generate a daily challenge based on the date
117    fn generate_daily_challenge(&self, date: chrono::NaiveDate) -> Challenge {
118        let all_daily = all_daily_challenges();
119        // Use day of year to deterministically select a challenge
120        let index = (date.ordinal0() as usize) % all_daily.len();
121        all_daily[index].clone()
122    }
123
124    /// Generate a weekly challenge based on the week number
125    fn generate_weekly_challenge(&self, week: u32) -> Challenge {
126        let all_weekly = all_weekly_challenges();
127        // Use week number to deterministically select a challenge
128        let index = (week as usize - 1) % all_weekly.len();
129        all_weekly[index].clone()
130    }
131
132    /// Get all challenges completed this week
133    pub fn weekly_completions(&self) -> usize {
134        // This could be enhanced to track completion timestamps
135        // For now, returns total completed challenges
136        self.completed_challenges.len()
137    }
138
139    /// Check if today's daily challenge is completed
140    pub fn is_daily_completed(&self) -> bool {
141        if let Some(ref daily) = self.daily_challenge {
142            self.is_challenge_completed(&daily.id)
143        } else {
144            false
145        }
146    }
147
148    /// Check if this week's weekly challenge is completed
149    pub fn is_weekly_completed(&self) -> bool {
150        if let Some(ref weekly) = self.weekly_challenge {
151            self.is_challenge_completed(&weekly.id)
152        } else {
153            false
154        }
155    }
156}
157
158impl Default for ChallengeManager {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164/// Get all available daily challenges
165pub fn all_daily_challenges() -> Vec<Challenge> {
166    vec![
167        Challenge {
168            id: "daily_grep_recursive".to_string(),
169            title: "Master Recursive Search".to_string(),
170            description: "Use grep with the -r flag to search for 'error' in all files within a directory.".to_string(),
171            difficulty: Difficulty::Intermediate,
172            challenge_type: ChallengeType::DailyCommand {
173                command: "grep -r 'error' /var/log".to_string(),
174                task_description: "Search recursively for the word 'error' in log files".to_string(),
175                validation: CommandValidation::CommandAndFlags,
176            },
177            points: 25,
178        },
179        Challenge {
180            id: "daily_chmod_executable".to_string(),
181            title: "Make It Executable".to_string(),
182            description: "Use chmod to make a script file executable by adding execute permissions.".to_string(),
183            difficulty: Difficulty::Beginner,
184            challenge_type: ChallengeType::DailyCommand {
185                command: "chmod +x script.sh".to_string(),
186                task_description: "Make a file executable using chmod".to_string(),
187                validation: CommandValidation::CommandAndFlags,
188            },
189            points: 20,
190        },
191        Challenge {
192            id: "daily_find_by_name".to_string(),
193            title: "Find by Name".to_string(),
194            description: "Use the find command to locate all .txt files in your home directory.".to_string(),
195            difficulty: Difficulty::Intermediate,
196            challenge_type: ChallengeType::DailyCommand {
197                command: "find ~ -name '*.txt'".to_string(),
198                task_description: "Find all text files in home directory".to_string(),
199                validation: CommandValidation::CommandAndFlags,
200            },
201            points: 25,
202        },
203        Challenge {
204            id: "daily_pipe_mastery".to_string(),
205            title: "Pipe Master".to_string(),
206            description: "Combine ls and grep using a pipe to find files containing 'test' in their names.".to_string(),
207            difficulty: Difficulty::Beginner,
208            challenge_type: ChallengeType::DailyCommand {
209                command: "ls | grep test".to_string(),
210                task_description: "Use pipes to filter ls output with grep".to_string(),
211                validation: CommandValidation::CommandAndFlags,
212            },
213            points: 20,
214        },
215        Challenge {
216            id: "daily_tar_archive".to_string(),
217            title: "Archive Creation".to_string(),
218            description: "Create a compressed tar archive of a directory using tar -czf.".to_string(),
219            difficulty: Difficulty::Intermediate,
220            challenge_type: ChallengeType::DailyCommand {
221                command: "tar -czf archive.tar.gz directory/".to_string(),
222                task_description: "Create a compressed tar archive".to_string(),
223                validation: CommandValidation::CommandAndFlags,
224            },
225            points: 30,
226        },
227        Challenge {
228            id: "daily_curl_download".to_string(),
229            title: "Web Fetcher".to_string(),
230            description: "Use curl to download a file from a URL and save it with -o option.".to_string(),
231            difficulty: Difficulty::Intermediate,
232            challenge_type: ChallengeType::DailyCommand {
233                command: "curl -o output.html https://example.com".to_string(),
234                task_description: "Download a file using curl".to_string(),
235                validation: CommandValidation::CommandAndFlags,
236            },
237            points: 25,
238        },
239        Challenge {
240            id: "daily_process_check".to_string(),
241            title: "Process Inspector".to_string(),
242            description: "Use ps aux combined with grep to find a specific running process.".to_string(),
243            difficulty: Difficulty::Intermediate,
244            challenge_type: ChallengeType::DailyCommand {
245                command: "ps aux | grep firefox".to_string(),
246                task_description: "Find a running process by name".to_string(),
247                validation: CommandValidation::CommandAndFlags,
248            },
249            points: 25,
250        },
251        Challenge {
252            id: "daily_disk_usage".to_string(),
253            title: "Disk Usage Detective".to_string(),
254            description: "Use du -sh to find the size of a directory in human-readable format.".to_string(),
255            difficulty: Difficulty::Beginner,
256            challenge_type: ChallengeType::DailyCommand {
257                command: "du -sh /var/log".to_string(),
258                task_description: "Check directory size with du".to_string(),
259                validation: CommandValidation::CommandAndFlags,
260            },
261            points: 20,
262        },
263        Challenge {
264            id: "daily_sed_replace".to_string(),
265            title: "Text Transformer".to_string(),
266            description: "Use sed to replace all occurrences of a word in a file.".to_string(),
267            difficulty: Difficulty::Advanced,
268            challenge_type: ChallengeType::DailyCommand {
269                command: "sed -i 's/old/new/g' file.txt".to_string(),
270                task_description: "Replace text in a file using sed".to_string(),
271                validation: CommandValidation::CommandAndFlags,
272            },
273            points: 35,
274        },
275        Challenge {
276            id: "daily_symlink_create".to_string(),
277            title: "Link Creator".to_string(),
278            description: "Create a symbolic link using ln -s pointing to a target file.".to_string(),
279            difficulty: Difficulty::Beginner,
280            challenge_type: ChallengeType::DailyCommand {
281                command: "ln -s /path/to/target linkname".to_string(),
282                task_description: "Create a symbolic link".to_string(),
283                validation: CommandValidation::CommandAndFlags,
284            },
285            points: 20,
286        },
287        Challenge {
288            id: "daily_history_search".to_string(),
289            title: "History Hunter".to_string(),
290            description: "Use history with grep to find commands you've run in the past.".to_string(),
291            difficulty: Difficulty::Beginner,
292            challenge_type: ChallengeType::DailyCommand {
293                command: "history | grep git".to_string(),
294                task_description: "Search command history".to_string(),
295                validation: CommandValidation::CommandAndFlags,
296            },
297            points: 15,
298        },
299        Challenge {
300            id: "daily_wc_count".to_string(),
301            title: "Word Counter".to_string(),
302            description: "Count the number of lines in a file using wc -l.".to_string(),
303            difficulty: Difficulty::Beginner,
304            challenge_type: ChallengeType::DailyCommand {
305                command: "wc -l file.txt".to_string(),
306                task_description: "Count lines in a file".to_string(),
307                validation: CommandValidation::CommandAndFlags,
308            },
309            points: 15,
310        },
311        Challenge {
312            id: "daily_env_check".to_string(),
313            title: "Environment Explorer".to_string(),
314            description: "Use env or printenv to display all environment variables.".to_string(),
315            difficulty: Difficulty::Beginner,
316            challenge_type: ChallengeType::DailyCommand {
317                command: "env".to_string(),
318                task_description: "Display environment variables".to_string(),
319                validation: CommandValidation::CommandOnly,
320            },
321            points: 15,
322        },
323        Challenge {
324            id: "daily_tail_follow".to_string(),
325            title: "Log Follower".to_string(),
326            description: "Use tail -f to continuously monitor a log file in real-time.".to_string(),
327            difficulty: Difficulty::Intermediate,
328            challenge_type: ChallengeType::DailyCommand {
329                command: "tail -f /var/log/syslog".to_string(),
330                task_description: "Follow a log file in real-time".to_string(),
331                validation: CommandValidation::CommandAndFlags,
332            },
333            points: 25,
334        },
335        Challenge {
336            id: "daily_awk_column".to_string(),
337            title: "Column Extractor".to_string(),
338            description: "Use awk to extract a specific column from formatted text output.".to_string(),
339            difficulty: Difficulty::Advanced,
340            challenge_type: ChallengeType::DailyCommand {
341                command: "ps aux | awk '{print $1, $11}'".to_string(),
342                task_description: "Extract specific columns using awk".to_string(),
343                validation: CommandValidation::CommandAndFlags,
344            },
345            points: 35,
346        },
347    ]
348}
349
350/// Get all available weekly challenges
351pub fn all_weekly_challenges() -> Vec<Challenge> {
352    vec![
353        Challenge {
354            id: "weekly_debug_server".to_string(),
355            title: "Debug the Broken Server".to_string(),
356            description: "A simulated web server isn't starting. Use diagnostic commands to find and fix the issues.".to_string(),
357            difficulty: Difficulty::Advanced,
358            challenge_type: ChallengeType::WeeklyScenario {
359                scenario: "The web server on port 8080 isn't responding. Debug the issue step by step.".to_string(),
360                steps: vec![
361                    ChallengeStep {
362                        description: "Check if the server process is running".to_string(),
363                        validation: CommandValidation::Regex("ps.*8080|netstat.*8080|lsof.*8080".to_string()),
364                        hint: Some("Try 'ps aux | grep server' or 'netstat -tulpn'".to_string()),
365                    },
366                    ChallengeStep {
367                        description: "Check the server log files for errors".to_string(),
368                        validation: CommandValidation::Regex("tail.*log|cat.*log|less.*log".to_string()),
369                        hint: Some("Look in /var/log/ or check application-specific logs".to_string()),
370                    },
371                    ChallengeStep {
372                        description: "Verify the port is not in use by another process".to_string(),
373                        validation: CommandValidation::Regex("netstat.*8080|lsof.*8080|ss.*8080".to_string()),
374                        hint: Some("Use 'lsof -i :8080' or 'netstat -tulpn | grep 8080'".to_string()),
375                    },
376                ],
377            },
378            points: 100,
379        },
380        Challenge {
381            id: "weekly_organize_files".to_string(),
382            title: "Organize the Chaos".to_string(),
383            description: "A directory is filled with mixed file types. Organize them into proper subdirectories.".to_string(),
384            difficulty: Difficulty::Intermediate,
385            challenge_type: ChallengeType::WeeklyScenario {
386                scenario: "The downloads folder has 100+ files of different types all mixed together. Organize them!".to_string(),
387                steps: vec![
388                    ChallengeStep {
389                        description: "Create directories for different file types (images, documents, code)".to_string(),
390                        validation: CommandValidation::Regex("mkdir.*images|mkdir.*documents|mkdir.*code".to_string()),
391                        hint: Some("Use 'mkdir -p' to create multiple directories".to_string()),
392                    },
393                    ChallengeStep {
394                        description: "Find and move all image files to the images directory".to_string(),
395                        validation: CommandValidation::Regex("mv.*\\.(jpg|png|gif)|find.*\\.(jpg|png).*-exec mv".to_string()),
396                        hint: Some("Use 'find' with '-name' or a for loop with 'mv'".to_string()),
397                    },
398                    ChallengeStep {
399                        description: "Create an archive of the organized directories".to_string(),
400                        validation: CommandValidation::Regex("tar.*czf|zip.*-r".to_string()),
401                        hint: Some("Use 'tar -czf organized.tar.gz' or 'zip -r'".to_string()),
402                    },
403                ],
404            },
405            points: 80,
406        },
407        Challenge {
408            id: "weekly_security_audit".to_string(),
409            title: "Security Audit".to_string(),
410            description: "Perform a basic security audit by checking file permissions and user access.".to_string(),
411            difficulty: Difficulty::Advanced,
412            challenge_type: ChallengeType::WeeklyScenario {
413                scenario: "Audit file permissions and find potential security issues in a system directory.".to_string(),
414                steps: vec![
415                    ChallengeStep {
416                        description: "Find all world-writable files".to_string(),
417                        validation: CommandValidation::Regex("find.*-perm.*777|find.*-perm.*o=w".to_string()),
418                        hint: Some("Use 'find /path -perm -002'".to_string()),
419                    },
420                    ChallengeStep {
421                        description: "List all users who can access the system".to_string(),
422                        validation: CommandValidation::Regex("cat /etc/passwd|getent passwd|cut.*passwd".to_string()),
423                        hint: Some("Check /etc/passwd or use 'getent passwd'".to_string()),
424                    },
425                    ChallengeStep {
426                        description: "Check for files with setuid bit enabled".to_string(),
427                        validation: CommandValidation::Regex("find.*-perm.*4000|find.*-perm.*u=s".to_string()),
428                        hint: Some("Use 'find / -perm -4000'".to_string()),
429                    },
430                ],
431            },
432            points: 120,
433        },
434        Challenge {
435            id: "weekly_backup_strategy".to_string(),
436            title: "Backup Strategy".to_string(),
437            description: "Create a comprehensive backup of important files with proper organization.".to_string(),
438            difficulty: Difficulty::Intermediate,
439            challenge_type: ChallengeType::WeeklyScenario {
440                scenario: "Set up an automated backup system for critical directories.".to_string(),
441                steps: vec![
442                    ChallengeStep {
443                        description: "Create a timestamped backup directory".to_string(),
444                        validation: CommandValidation::Regex("mkdir.*backup.*$(date|date.*\\+)".to_string()),
445                        hint: Some("Use 'mkdir backup-$(date +%Y%m%d)'".to_string()),
446                    },
447                    ChallengeStep {
448                        description: "Copy important files preserving permissions and timestamps".to_string(),
449                        validation: CommandValidation::Regex("cp -a|cp.*-p|rsync.*-a".to_string()),
450                        hint: Some("Use 'cp -a' or 'rsync -av'".to_string()),
451                    },
452                    ChallengeStep {
453                        description: "Create a compressed archive of the backup".to_string(),
454                        validation: CommandValidation::Regex("tar.*czf|gzip|zip".to_string()),
455                        hint: Some("Use 'tar -czf backup.tar.gz backup/'".to_string()),
456                    },
457                ],
458            },
459            points: 90,
460        },
461        Challenge {
462            id: "weekly_log_analysis".to_string(),
463            title: "Log Analysis Detective".to_string(),
464            description: "Analyze system logs to find errors, warnings, and unusual patterns.".to_string(),
465            difficulty: Difficulty::Advanced,
466            challenge_type: ChallengeType::WeeklyScenario {
467                scenario: "Investigate system logs to identify and report issues.".to_string(),
468                steps: vec![
469                    ChallengeStep {
470                        description: "Find all error messages in system logs".to_string(),
471                        validation: CommandValidation::Regex("grep.*error|grep.*ERROR|egrep.*'error|ERROR'".to_string()),
472                        hint: Some("Use 'grep -i error /var/log/syslog'".to_string()),
473                    },
474                    ChallengeStep {
475                        description: "Count occurrences of different error types".to_string(),
476                        validation: CommandValidation::Regex("sort|uniq -c|wc".to_string()),
477                        hint: Some("Combine 'sort | uniq -c | sort -n'".to_string()),
478                    },
479                    ChallengeStep {
480                        description: "Extract and save the top 10 most frequent errors to a file".to_string(),
481                        validation: CommandValidation::Regex("head.*>|>.*head|tee".to_string()),
482                        hint: Some("Use pipes and redirect with '> errors.txt'".to_string()),
483                    },
484                ],
485            },
486            points: 110,
487        },
488    ]
489}
490
491/// Get all available speed challenges
492pub fn all_speed_challenges() -> Vec<Challenge> {
493    vec![
494        Challenge {
495            id: "speed_cleanup_tmp".to_string(),
496            title: "Rapid Cleanup".to_string(),
497            description: "Find and delete all .tmp files in a directory tree within 60 seconds.".to_string(),
498            difficulty: Difficulty::Intermediate,
499            challenge_type: ChallengeType::SpeedChallenge {
500                task: "Remove all temporary files (.tmp extension) from the current directory and subdirectories".to_string(),
501                time_limit_seconds: 60,
502                commands: vec![
503                    "find . -name '*.tmp' -delete".to_string(),
504                    "find . -name '*.tmp' -exec rm {} \\;".to_string(),
505                ],
506            },
507            points: 50,
508        },
509        Challenge {
510            id: "speed_count_files".to_string(),
511            title: "Quick Count".to_string(),
512            description: "Count all files in a directory within 30 seconds.".to_string(),
513            difficulty: Difficulty::Beginner,
514            challenge_type: ChallengeType::SpeedChallenge {
515                task: "Count the total number of files (not directories) in the current directory tree".to_string(),
516                time_limit_seconds: 30,
517                commands: vec![
518                    "find . -type f | wc -l".to_string(),
519                    "ls -lR | grep '^-' | wc -l".to_string(),
520                ],
521            },
522            points: 40,
523        },
524        Challenge {
525            id: "speed_largest_files".to_string(),
526            title: "Size Hunter".to_string(),
527            description: "Find the 5 largest files in a directory within 45 seconds.".to_string(),
528            difficulty: Difficulty::Intermediate,
529            challenge_type: ChallengeType::SpeedChallenge {
530                task: "List the 5 largest files in the directory tree with their sizes".to_string(),
531                time_limit_seconds: 45,
532                commands: vec![
533                    "find . -type f -exec ls -lh {} \\; | sort -k5 -hr | head -5".to_string(),
534                    "du -ah . | sort -rh | head -5".to_string(),
535                ],
536            },
537            points: 55,
538        },
539    ]
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn test_challenge_manager_creation() {
548        let manager = ChallengeManager::new();
549        assert!(manager.daily_challenge.is_none());
550        assert!(manager.weekly_challenge.is_none());
551        assert_eq!(manager.completed_challenges.len(), 0);
552    }
553
554    #[test]
555    fn test_daily_challenge_generation() {
556        let mut manager = ChallengeManager::new();
557        let challenge = manager.get_daily_challenge();
558        assert!(!challenge.id.is_empty());
559        assert!(!challenge.title.is_empty());
560        assert!(challenge.points > 0);
561    }
562
563    #[test]
564    fn test_weekly_challenge_generation() {
565        let mut manager = ChallengeManager::new();
566        let challenge = manager.get_weekly_challenge();
567        assert!(!challenge.id.is_empty());
568        assert!(!challenge.title.is_empty());
569        assert!(challenge.points > 0);
570    }
571
572    #[test]
573    fn test_challenge_completion() {
574        let mut manager = ChallengeManager::new();
575        assert!(!manager.is_challenge_completed("test_challenge"));
576
577        manager.complete_challenge("test_challenge".to_string());
578        assert!(manager.is_challenge_completed("test_challenge"));
579    }
580
581    #[test]
582    fn test_daily_challenges_exist() {
583        let challenges = all_daily_challenges();
584        assert!(!challenges.is_empty());
585        assert!(challenges.len() >= 10);
586    }
587
588    #[test]
589    fn test_weekly_challenges_exist() {
590        let challenges = all_weekly_challenges();
591        assert!(!challenges.is_empty());
592        assert!(challenges.len() >= 5);
593    }
594
595    #[test]
596    fn test_deterministic_daily_selection() {
597        let mut manager1 = ChallengeManager::new();
598        let mut manager2 = ChallengeManager::new();
599
600        let challenge1 = manager1.get_daily_challenge();
601        let challenge2 = manager2.get_daily_challenge();
602
603        // Same day should give same challenge
604        assert_eq!(challenge1.id, challenge2.id);
605    }
606}