import 'deck_config.dart'; import 'question.dart'; import 'attempt_history_entry.dart'; import 'incomplete_attempt.dart'; /// A practice deck containing questions and configuration. class Deck { /// Unique identifier for this deck. final String id; /// Title of the deck. final String title; /// Description of the deck. final String description; /// List of questions in this deck. final List questions; /// Configuration for this deck. final DeckConfig config; /// Current global attempt index (incremented with each attempt). final int currentAttemptIndex; /// History of completed attempts. final List attemptHistory; /// Incomplete attempt that can be resumed. final IncompleteAttempt? incompleteAttempt; const Deck({ required this.id, required this.title, required this.description, required this.questions, required this.config, this.currentAttemptIndex = 0, this.attemptHistory = const [], this.incompleteAttempt, }); /// Creates a copy of this deck with the given fields replaced. Deck copyWith({ String? id, String? title, String? description, List? questions, DeckConfig? config, int? currentAttemptIndex, List? attemptHistory, IncompleteAttempt? incompleteAttempt, bool clearIncompleteAttempt = false, }) { return Deck( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, questions: questions ?? this.questions, config: config ?? this.config, currentAttemptIndex: currentAttemptIndex ?? this.currentAttemptIndex, attemptHistory: attemptHistory ?? this.attemptHistory, incompleteAttempt: clearIncompleteAttempt ? null : (incompleteAttempt ?? this.incompleteAttempt), ); } /// Total number of questions in the deck. int get numberOfQuestions => questions.length; /// Number of questions marked as known. int get knownCount => questions.where((q) => q.isKnown).length; /// Number of questions the user has flagged. int get flaggedCount => questions.where((q) => q.isFlagged).length; /// Questions that the user has flagged. List get flaggedQuestions => questions.where((q) => q.isFlagged).toList(); /// Practice percentage: (known / total) * 100 double get practicePercentage { if (questions.isEmpty) return 0.0; return (knownCount / questions.length) * 100.0; } /// Progress percentage including partial credit: each question contributes /// 1.0 if known, otherwise (consecutiveCorrect / requiredConsecutiveCorrect) /// capped at 1.0. Averaged over all questions * 100. double get progressPercentage { if (questions.isEmpty) return 0.0; final required = config.requiredConsecutiveCorrect; final sum = questions.fold(0.0, (sum, q) { if (q.isKnown) return sum + 1.0; final partial = (q.consecutiveCorrect / required).clamp(0.0, 1.0); return sum + partial; }); return (sum / questions.length) * 100.0; } /// Number of completed attempts. int get attemptCount => attemptHistory.length; /// Average percentage correct across all attempts. double get averagePercentageCorrect { if (attemptHistory.isEmpty) return 0.0; final sum = attemptHistory.fold( 0.0, (sum, entry) => sum + entry.percentageCorrect, ); return sum / attemptHistory.length; } /// Total time spent on all attempts in milliseconds. int get totalTimeSpent { return attemptHistory.fold( 0, (sum, entry) => sum + entry.timeSpent, ); } @override bool operator ==(Object other) => identical(this, other) || other is Deck && runtimeType == other.runtimeType && id == other.id && title == other.title && description == other.description && questions.toString() == other.questions.toString() && config == other.config && currentAttemptIndex == other.currentAttemptIndex && attemptHistory.toString() == other.attemptHistory.toString() && incompleteAttempt == other.incompleteAttempt; @override int get hashCode => id.hashCode ^ title.hashCode ^ description.hashCode ^ questions.hashCode ^ config.hashCode ^ currentAttemptIndex.hashCode ^ attemptHistory.hashCode ^ (incompleteAttempt?.hashCode ?? 0); }