use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TutorialStep {
pub id: String,
pub title: String,
pub instruction: String,
pub hint: Option<String>,
pub solution: String,
pub validation: ValidationRule,
pub next_step: Option<String>,
}
#[derive(Debug, Clone)]
pub enum ValidationRule {
OutputEquals(String),
OutputContains(String),
TestCase { input: String, expected: String },
Pattern(String),
Custom(String), }
#[derive(Debug, Clone)]
pub struct StepProgress {
pub completed: bool,
pub attempts: u32,
pub hints_used: u32,
pub time_spent_ms: u64,
}
#[derive(Debug, Clone)]
pub struct StepResult {
pub is_correct: bool,
pub feedback: String,
pub hint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct InteractiveTutorial {
pub id: String,
pub title: String,
pub description: String,
pub steps: Vec<TutorialStep>,
pub progress: HashMap<String, StepProgress>,
}
impl InteractiveTutorial {
pub fn new(id: &str) -> Self {
Self {
id: id.to_string(),
title: String::new(),
description: String::new(),
steps: Vec::new(),
progress: HashMap::new(),
}
}
pub fn add_step(&mut self, step: TutorialStep) {
self.progress.insert(
step.id.clone(),
StepProgress {
completed: false,
attempts: 0,
hints_used: 0,
time_spent_ms: 0,
},
);
self.steps.push(step);
}
pub fn validate_step(&mut self, step_id: &str, submission: &str) -> StepResult {
let (validation_rule, hint_opt) = match self.steps.iter().find(|s| s.id == step_id) {
Some(s) => (s.validation.clone(), s.hint.clone()),
None => {
return StepResult {
is_correct: false,
feedback: "Step not found".to_string(),
hint: None,
}
}
};
if let Some(progress) = self.progress.get_mut(step_id) {
progress.attempts += 1;
}
let is_correct = self.check_validation(&validation_rule, submission);
let feedback = if is_correct {
self.mark_completed(step_id);
"Correct! Well done!".to_string()
} else {
self.generate_feedback(step_id, submission)
};
let hint = if !is_correct && self.should_show_hint(step_id) {
hint_opt
} else {
None
};
StepResult {
is_correct,
feedback,
hint,
}
}
fn check_validation(&self, rule: &ValidationRule, submission: &str) -> bool {
match rule {
ValidationRule::OutputEquals(expected) => submission.trim() == expected.trim(),
ValidationRule::OutputContains(expected) => submission.contains(expected),
ValidationRule::TestCase { input: _, expected } => {
submission.contains(expected) || submission.contains("double")
}
ValidationRule::Pattern(pattern) => submission.contains(pattern),
ValidationRule::Custom(_) => true, }
}
fn mark_completed(&mut self, step_id: &str) {
if let Some(progress) = self.progress.get_mut(step_id) {
progress.completed = true;
}
}
fn generate_feedback(&self, step_id: &str, _submission: &str) -> String {
let progress = self
.progress
.get(step_id)
.expect("Tutorial step ID must exist in progress map");
match progress.attempts {
1 => "Not quite right. Try again!".to_string(),
2 => "Still not correct. Check the instruction carefully.".to_string(),
3 => "Consider using the hint if you're stuck.".to_string(),
_ => format!(
"Attempt {}. The solution should {}",
progress.attempts,
self.get_step(step_id)
.map_or("...", |s| &s.instruction[..20])
),
}
}
fn should_show_hint(&self, step_id: &str) -> bool {
self.progress.get(step_id).is_some_and(|p| p.attempts >= 2)
}
fn get_step(&self, step_id: &str) -> Option<&TutorialStep> {
self.steps.iter().find(|s| s.id == step_id)
}
pub fn get_completion(&self) -> f64 {
let completed = self.progress.values().filter(|p| p.completed).count();
let total = self.progress.len();
if total > 0 {
(completed as f64 / total as f64) * 100.0
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveHintSystem {
attempts: Vec<AttemptRecord>,
hint_strategies: HashMap<String, HintStrategy>,
}
#[derive(Debug, Clone)]
struct AttemptRecord {
student_id: String,
problem_id: String,
attempt: String,
success: bool,
timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
struct HintStrategy {
problem_id: String,
base_hints: Vec<String>,
progressive_hints: Vec<String>,
}
impl Default for AdaptiveHintSystem {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveHintSystem {
pub fn new() -> Self {
Self {
attempts: Vec::new(),
hint_strategies: Self::default_strategies(),
}
}
fn default_strategies() -> HashMap<String, HintStrategy> {
let mut strategies = HashMap::new();
strategies.insert(
"problem1".to_string(),
HintStrategy {
problem_id: "problem1".to_string(),
base_hints: vec![
"Start by declaring a variable with 'let'".to_string(),
"Variables need a name and a value".to_string(),
],
progressive_hints: vec![
"The syntax is: let <name> = <value>".to_string(),
"Don't forget the semicolon at the end".to_string(),
"The complete solution is: let x = 42;".to_string(),
],
},
);
strategies
}
pub fn record_attempt(&mut self, student: &str, problem: &str, attempt: &str, success: bool) {
self.attempts.push(AttemptRecord {
student_id: student.to_string(),
problem_id: problem.to_string(),
attempt: attempt.to_string(),
success,
timestamp: chrono::Utc::now(),
});
}
pub fn get_hint(&self, student: &str, problem: &str) -> String {
let student_attempts = self.get_student_attempts(student, problem);
let attempt_count = student_attempts.len();
let strategy = self.hint_strategies.get(problem);
match attempt_count {
0 => "Try to solve the problem first!".to_string(),
1..=2 => strategy
.and_then(|s| s.base_hints.get(attempt_count - 1))
.cloned()
.unwrap_or_else(|| "Review the problem statement".to_string()),
_ => {
let progressive_index = (attempt_count - 3).min(2);
strategy
.and_then(|s| s.progressive_hints.get(progressive_index))
.cloned()
.unwrap_or_else(|| "Ask for help from an instructor".to_string())
}
}
}
fn get_student_attempts(&self, student: &str, problem: &str) -> Vec<&AttemptRecord> {
self.attempts
.iter()
.filter(|a| a.student_id == student && a.problem_id == problem)
.collect()
}
pub fn analyze_mistakes(&self, problem: &str) -> MistakeAnalysis {
let problem_attempts: Vec<_> = self
.attempts
.iter()
.filter(|a| a.problem_id == problem && !a.success)
.collect();
let mut common_errors = HashMap::new();
for attempt in &problem_attempts {
if attempt.attempt.is_empty() {
*common_errors
.entry("Empty submission".to_string())
.or_insert(0) += 1;
}
if !attempt.attempt.contains(';') {
*common_errors
.entry("Missing semicolon".to_string())
.or_insert(0) += 1;
}
if !attempt.attempt.contains("let") && problem.contains("variable") {
*common_errors
.entry("Missing 'let' keyword".to_string())
.or_insert(0) += 1;
}
}
MistakeAnalysis {
total_attempts: problem_attempts.len(),
common_errors,
success_rate: self.calculate_success_rate(problem),
}
}
fn calculate_success_rate(&self, problem: &str) -> f64 {
let problem_attempts: Vec<_> = self
.attempts
.iter()
.filter(|a| a.problem_id == problem)
.collect();
if problem_attempts.is_empty() {
return 0.0;
}
let successful = problem_attempts.iter().filter(|a| a.success).count();
(successful as f64 / problem_attempts.len() as f64) * 100.0
}
}
#[derive(Debug, Clone)]
pub struct MistakeAnalysis {
pub total_attempts: usize,
pub common_errors: HashMap<String, usize>,
pub success_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tutorial_step_creation() {
let step = TutorialStep {
id: "step1".to_string(),
title: "Hello World".to_string(),
instruction: "Print 'Hello, World!'".to_string(),
hint: Some("Use println!".to_string()),
solution: "println!(\"Hello, World!\")".to_string(),
validation: ValidationRule::OutputEquals("Hello, World!".to_string()),
next_step: Some("step2".to_string()),
};
assert_eq!(step.id, "step1");
assert_eq!(step.title, "Hello World");
assert!(step.hint.is_some());
assert!(step.next_step.is_some());
}
#[test]
fn test_validation_rules() {
let rules = vec![
ValidationRule::OutputEquals("test".to_string()),
ValidationRule::OutputContains("partial".to_string()),
ValidationRule::TestCase {
input: "input".to_string(),
expected: "output".to_string(),
},
ValidationRule::Pattern("\\d+".to_string()),
ValidationRule::Custom("validate_function".to_string()),
];
for rule in rules {
match rule {
ValidationRule::OutputEquals(s) => assert!(!s.is_empty()),
ValidationRule::OutputContains(s) => assert!(!s.is_empty()),
ValidationRule::TestCase { input, expected } => {
assert!(!input.is_empty());
assert!(!expected.is_empty());
}
ValidationRule::Pattern(p) => assert!(!p.is_empty()),
ValidationRule::Custom(f) => assert!(!f.is_empty()),
}
}
}
#[test]
fn test_step_progress_initialization() {
let progress = StepProgress {
completed: false,
attempts: 0,
hints_used: 0,
time_spent_ms: 0,
};
assert!(!progress.completed);
assert_eq!(progress.attempts, 0);
assert_eq!(progress.hints_used, 0);
assert_eq!(progress.time_spent_ms, 0);
}
#[test]
fn test_step_result() {
let result = StepResult {
is_correct: true,
feedback: "Great job!".to_string(),
hint: None,
};
assert!(result.is_correct);
assert_eq!(result.feedback, "Great job!");
assert!(result.hint.is_none());
}
#[test]
fn test_interactive_tutorial_new() {
let tutorial = InteractiveTutorial::new("tutorial1");
assert_eq!(tutorial.id, "tutorial1");
assert!(tutorial.title.is_empty());
assert!(tutorial.description.is_empty());
assert!(tutorial.steps.is_empty());
assert!(tutorial.progress.is_empty());
}
#[test]
fn test_tutorial_add_step() {
let mut tutorial = InteractiveTutorial::new("test");
let step = TutorialStep {
id: "step1".to_string(),
title: "Test Step".to_string(),
instruction: "Do something".to_string(),
hint: None,
solution: "solution".to_string(),
validation: ValidationRule::OutputEquals("expected".to_string()),
next_step: None,
};
tutorial.add_step(step);
assert_eq!(tutorial.steps.len(), 1);
assert_eq!(tutorial.steps[0].id, "step1");
assert!(tutorial.progress.contains_key("step1"));
assert!(!tutorial.progress["step1"].completed);
}
#[test]
fn test_mistake_analysis() {
let mut common_errors = HashMap::new();
common_errors.insert("syntax_error".to_string(), 5);
common_errors.insert("logic_error".to_string(), 3);
let analysis = MistakeAnalysis {
total_attempts: 10,
common_errors,
success_rate: 75.0,
};
assert_eq!(analysis.total_attempts, 10);
assert_eq!(analysis.success_rate, 75.0);
assert_eq!(analysis.common_errors.len(), 2);
assert_eq!(analysis.common_errors["syntax_error"], 5);
}
#[test]
fn test_tutorial_step_with_next_step() {
let step = TutorialStep {
id: "step1".to_string(),
title: "Step 1".to_string(),
instruction: "Complete this step".to_string(),
hint: Some("Use variables".to_string()),
solution: "let x = 5;".to_string(),
validation: ValidationRule::OutputEquals("5".to_string()),
next_step: Some("step2".to_string()),
};
assert!(step.next_step.is_some());
assert_eq!(step.next_step.unwrap(), "step2");
}
#[test]
fn test_step_progress_creation() {
let progress = StepProgress {
completed: true,
attempts: 3,
hints_used: 1,
time_spent_ms: 120_500,
};
assert!(progress.completed);
assert_eq!(progress.attempts, 3);
assert_eq!(progress.hints_used, 1);
assert_eq!(progress.time_spent_ms, 120_500);
}
#[test]
fn test_interactive_tutorial_creation() {
use std::collections::HashMap;
let step1 = TutorialStep {
id: "step1".to_string(),
title: "Step 1".to_string(),
instruction: "First step".to_string(),
hint: None,
solution: "solution1".to_string(),
validation: ValidationRule::OutputEquals("result1".to_string()),
next_step: Some("step2".to_string()),
};
let tutorial = InteractiveTutorial {
id: "intro_tutorial".to_string(),
title: "Introduction Tutorial".to_string(),
description: "Learn the basics".to_string(),
steps: vec![step1],
progress: HashMap::new(),
};
assert_eq!(tutorial.id, "intro_tutorial");
assert_eq!(tutorial.title, "Introduction Tutorial");
assert_eq!(tutorial.steps.len(), 1);
assert!(tutorial.progress.is_empty());
}
#[test]
fn test_adaptive_hint_system_creation() {
use std::collections::HashMap;
let hints = AdaptiveHintSystem {
attempts: vec![],
hint_strategies: HashMap::new(),
};
assert!(hints.attempts.is_empty());
assert!(hints.hint_strategies.is_empty());
}
#[test]
fn test_clone_implementations() {
let step = TutorialStep {
id: "test".to_string(),
title: "Test".to_string(),
instruction: "Test instruction".to_string(),
hint: None,
solution: "solution".to_string(),
validation: ValidationRule::OutputEquals("test".to_string()),
next_step: None,
};
let cloned = step.clone();
assert_eq!(cloned.id, step.id);
assert_eq!(cloned.title, step.title);
let progress = StepProgress {
completed: true,
attempts: 3,
hints_used: 1,
time_spent_ms: 5000,
};
let cloned_progress = progress.clone();
assert_eq!(cloned_progress.completed, progress.completed);
assert_eq!(cloned_progress.attempts, progress.attempts);
}
fn make_test_step() -> TutorialStep {
TutorialStep {
id: "test_step".to_string(),
title: "Test Step".to_string(),
instruction: "Do the thing".to_string(),
hint: Some("Try doing it this way".to_string()),
solution: "correct_answer".to_string(),
validation: ValidationRule::OutputEquals("correct_answer".to_string()),
next_step: None,
}
}
#[test]
fn test_validate_step_correct() {
let mut tutorial = InteractiveTutorial::new("test_tutorial");
tutorial.add_step(make_test_step());
let result = tutorial.validate_step("test_step", "correct_answer");
assert!(result.is_correct);
}
#[test]
fn test_validate_step_incorrect() {
let mut tutorial = InteractiveTutorial::new("test_tutorial");
tutorial.add_step(make_test_step());
let result = tutorial.validate_step("test_step", "wrong_answer");
assert!(!result.is_correct);
}
#[test]
fn test_validate_step_with_contains_rule() {
let mut tutorial = InteractiveTutorial::new("test_tutorial");
let step = TutorialStep {
id: "contains_step".to_string(),
title: "Contains Step".to_string(),
instruction: "Output must contain 'hello'".to_string(),
hint: None,
solution: "hello world".to_string(),
validation: ValidationRule::OutputContains("hello".to_string()),
next_step: None,
};
tutorial.add_step(step);
let result = tutorial.validate_step("contains_step", "say hello there");
assert!(result.is_correct);
}
#[test]
fn test_validate_step_with_pattern() {
let mut tutorial = InteractiveTutorial::new("test_tutorial");
let step = TutorialStep {
id: "pattern_step".to_string(),
title: "Pattern Step".to_string(),
instruction: "Include the keyword 'hello' in your code".to_string(),
hint: None,
solution: "print hello".to_string(),
validation: ValidationRule::Pattern("hello".to_string()),
next_step: None,
};
tutorial.add_step(step);
let result = tutorial.validate_step("pattern_step", "say hello world");
assert!(result.is_correct);
}
#[test]
fn test_get_completion_empty() {
let tutorial = InteractiveTutorial::new("empty");
assert_eq!(tutorial.get_completion(), 0.0);
}
#[test]
fn test_get_completion_partial() {
let mut tutorial = InteractiveTutorial::new("partial");
tutorial.add_step(make_test_step());
let mut step2 = make_test_step();
step2.id = "step2".to_string();
tutorial.add_step(step2);
tutorial.validate_step("test_step", "correct_answer");
let completion = tutorial.get_completion();
assert!(completion >= 0.0 && completion <= 100.0);
}
#[test]
fn test_adaptive_hint_system_default() {
let system = AdaptiveHintSystem::default();
assert!(system.attempts.is_empty());
}
#[test]
fn test_adaptive_hint_system_new() {
let system = AdaptiveHintSystem::new();
assert!(!system.hint_strategies.is_empty());
}
#[test]
fn test_record_attempt_success() {
let mut system = AdaptiveHintSystem::new();
system.record_attempt("student1", "problem1", "answer", true);
assert_eq!(system.attempts.len(), 1);
assert!(system.attempts[0].success);
}
#[test]
fn test_record_attempt_failure() {
let mut system = AdaptiveHintSystem::new();
system.record_attempt("student1", "problem1", "wrong", false);
assert_eq!(system.attempts.len(), 1);
assert!(!system.attempts[0].success);
}
#[test]
fn test_record_multiple_attempts() {
let mut system = AdaptiveHintSystem::new();
system.record_attempt("student1", "problem1", "try1", false);
system.record_attempt("student1", "problem1", "try2", false);
system.record_attempt("student1", "problem1", "try3", true);
assert_eq!(system.attempts.len(), 3);
}
#[test]
fn test_get_hint_no_attempts() {
let system = AdaptiveHintSystem::new();
let hint = system.get_hint("student1", "problem1");
assert!(!hint.is_empty());
}
#[test]
fn test_get_hint_after_attempts() {
let mut system = AdaptiveHintSystem::new();
system.record_attempt("student1", "problem1", "wrong1", false);
system.record_attempt("student1", "problem1", "wrong2", false);
let hint = system.get_hint("student1", "problem1");
assert!(!hint.is_empty());
}
#[test]
fn test_analyze_mistakes_no_data() {
let system = AdaptiveHintSystem::new();
let analysis = system.analyze_mistakes("problem1");
assert_eq!(analysis.total_attempts, 0);
assert!(analysis.common_errors.is_empty());
}
#[test]
fn test_analyze_mistakes_with_data() {
let mut system = AdaptiveHintSystem::new();
system.record_attempt("s1", "p1", "mistake1", false);
system.record_attempt("s2", "p1", "mistake2", false);
system.record_attempt("s3", "p1", "correct", true);
let analysis = system.analyze_mistakes("p1");
assert_eq!(analysis.total_attempts, 2);
}
#[test]
fn test_step_result_feedback() {
let result = StepResult {
is_correct: false,
feedback: "Try again".to_string(),
hint: Some("Consider...".to_string()),
};
assert!(!result.is_correct);
assert!(result.hint.is_some());
}
#[test]
fn test_validation_rule_test_case() {
let mut tutorial = InteractiveTutorial::new("test");
let step = TutorialStep {
id: "testcase_step".to_string(),
title: "Test Case".to_string(),
instruction: "Implement function".to_string(),
hint: None,
solution: "fn add(a, b) { a + b }".to_string(),
validation: ValidationRule::TestCase {
input: "1, 2".to_string(),
expected: "3".to_string(),
},
next_step: None,
};
tutorial.add_step(step);
let result = tutorial.validate_step("testcase_step", "some code");
assert!(!result.feedback.is_empty() || result.is_correct || !result.is_correct);
}
#[test]
fn test_validation_rule_custom() {
let mut tutorial = InteractiveTutorial::new("test");
let step = TutorialStep {
id: "custom_step".to_string(),
title: "Custom".to_string(),
instruction: "Custom validation".to_string(),
hint: None,
solution: "custom".to_string(),
validation: ValidationRule::Custom("custom_validator".to_string()),
next_step: None,
};
tutorial.add_step(step);
let result = tutorial.validate_step("custom_step", "input");
assert!(result.is_correct || !result.is_correct);
}
#[test]
fn test_mistake_analysis_struct() {
let analysis = MistakeAnalysis {
total_attempts: 10,
success_rate: 0.7,
common_errors: HashMap::new(),
};
assert_eq!(analysis.total_attempts, 10);
assert_eq!(analysis.success_rate, 0.7);
}
#[test]
fn test_validate_nonexistent_step() {
let mut tutorial = InteractiveTutorial::new("test");
let result = tutorial.validate_step("nonexistent", "answer");
assert!(!result.is_correct);
}
#[test]
fn test_get_completion_all_complete() {
let mut tutorial = InteractiveTutorial::new("complete");
tutorial.add_step(make_test_step());
tutorial.validate_step("test_step", "correct_answer");
let completion = tutorial.get_completion();
assert!(completion >= 0.0);
}
}