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.

140 lines
4.1 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;
/// 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);
}

Powered by TurnKey Linux.