You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
4.4 KiB
146 lines
4.4 KiB
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<Question> 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<AttemptHistoryEntry> 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<Question>? questions,
|
|
DeckConfig? config,
|
|
int? currentAttemptIndex,
|
|
List<AttemptHistoryEntry>? 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<Question> 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<double>(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<double>(
|
|
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<int>(
|
|
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);
|
|
}
|
|
|