1use crate::lesson::{CommandValidation, Difficulty};
7use chrono::{Datelike, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10
11#[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#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum ChallengeType {
25 DailyCommand {
27 command: String,
28 task_description: String,
29 validation: CommandValidation,
30 },
31 WeeklyScenario {
33 scenario: String,
34 steps: Vec<ChallengeStep>,
35 },
36 SpeedChallenge {
38 task: String,
39 time_limit_seconds: u32,
40 commands: Vec<String>,
41 },
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ChallengeStep {
47 pub description: String,
48 pub validation: CommandValidation,
49 pub hint: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ChallengeManager {
55 pub daily_challenge: Option<Challenge>,
57 pub weekly_challenge: Option<Challenge>,
59 pub completed_challenges: HashSet<String>,
61 last_daily_generated: Option<chrono::NaiveDate>,
63 last_weekly_generated: Option<u32>,
65}
66
67impl ChallengeManager {
68 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 pub fn get_daily_challenge(&mut self) -> Challenge {
81 let today = Utc::now().date_naive();
82
83 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 pub fn get_weekly_challenge(&mut self) -> Challenge {
94 let now = Utc::now();
95 let current_week = now.iso_week().week();
96
97 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 pub fn complete_challenge(&mut self, challenge_id: String) {
108 self.completed_challenges.insert(challenge_id);
109 }
110
111 pub fn is_challenge_completed(&self, challenge_id: &str) -> bool {
113 self.completed_challenges.contains(challenge_id)
114 }
115
116 fn generate_daily_challenge(&self, date: chrono::NaiveDate) -> Challenge {
118 let all_daily = all_daily_challenges();
119 let index = (date.ordinal0() as usize) % all_daily.len();
121 all_daily[index].clone()
122 }
123
124 fn generate_weekly_challenge(&self, week: u32) -> Challenge {
126 let all_weekly = all_weekly_challenges();
127 let index = (week as usize - 1) % all_weekly.len();
129 all_weekly[index].clone()
130 }
131
132 pub fn weekly_completions(&self) -> usize {
134 self.completed_challenges.len()
137 }
138
139 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 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
164pub 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
350pub 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
491pub 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 assert_eq!(challenge1.id, challenge2.id);
605 }
606}