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.
317 lines
9.7 KiB
317 lines
9.7 KiB
# Practice Engine
|
|
|
|
A pure Dart package implementing spaced repetition, weighted selection, and practice tracking for question decks. This package provides a complete solution for building quiz and flashcard applications with intelligent question selection based on learning progress.
|
|
|
|
## What It Does
|
|
|
|
The Practice Engine helps you build applications where users practice questions (like flashcards or quizzes) with an intelligent system that:
|
|
|
|
- **Adapts to learning progress**: Questions you struggle with appear more frequently, while mastered questions appear less often
|
|
- **Uses spaced repetition**: Even "known" questions occasionally reappear to reinforce memory
|
|
- **Tracks comprehensive statistics**: Monitor progress, streaks, and overall deck mastery
|
|
- **Provides flexible configuration**: Customize behavior per deck to match different learning styles
|
|
|
|
The engine is designed to be pure Dart (no Flutter dependencies), making it suitable for any Dart application including CLI tools, web apps, and mobile applications.
|
|
|
|
## Features
|
|
|
|
- **Spaced Repetition**: Known questions have low but increasing probability of appearing based on time since last seen
|
|
- **Weighted Random Selection**: Questions are selected based on priority points and spaced repetition logic
|
|
- **Priority Scoring**: Questions gain priority when answered incorrectly and lose it when answered correctly
|
|
- **Question Streak Logic**: Tracks consecutive correct answers to determine when a question is "known"
|
|
- **Attempt/Quiz Logic**: Create and process practice attempts with comprehensive result tracking
|
|
- **Deck Progress Tracking**: Track overall deck progress with practice percentage
|
|
- **Deck Configuration**: Customizable settings per deck (streak threshold, attempt size, priority changes, etc.)
|
|
- **Manual Overrides**: Manually mark questions as known or needs practice
|
|
- **Reset Behavior**: Reset deck state with optional attempt count preservation
|
|
|
|
## Installation
|
|
|
|
Add this package to your `pubspec.yaml`:
|
|
|
|
```yaml
|
|
dependencies:
|
|
practice_engine:
|
|
path: packages/practice_engine # For local development
|
|
# Or use pub.dev if published:
|
|
# practice_engine: ^1.0.0
|
|
```
|
|
|
|
Then run:
|
|
|
|
```bash
|
|
dart pub get
|
|
```
|
|
|
|
## Requirements
|
|
|
|
- Dart SDK >= 3.0.0
|
|
|
|
## Usage
|
|
|
|
### Creating a Deck
|
|
|
|
A deck contains questions and configuration. Each question tracks its learning state (streaks, priority, known status).
|
|
|
|
```dart
|
|
import 'package:practice_engine/practice_engine.dart';
|
|
|
|
// Configure deck behavior
|
|
final config = DeckConfig(
|
|
requiredConsecutiveCorrect: 3, // Need 3 correct in a row to mark as "known"
|
|
defaultAttemptSize: 10, // Default questions per practice session
|
|
priorityIncreaseOnIncorrect: 5, // Add 5 priority points when wrong
|
|
priorityDecreaseOnCorrect: 2, // Remove 2 priority points when correct
|
|
immediateFeedbackEnabled: true, // Show feedback immediately
|
|
);
|
|
|
|
// Create questions
|
|
final questions = [
|
|
Question(
|
|
id: 'q1',
|
|
prompt: 'What is 2+2?',
|
|
answers: ['3', '4', '5'],
|
|
correctAnswerIndex: 1, // Answer at index 1 is correct
|
|
),
|
|
Question(
|
|
id: 'q2',
|
|
prompt: 'What is the capital of France?',
|
|
answers: ['London', 'Paris', 'Berlin'],
|
|
correctAnswerIndex: 1,
|
|
),
|
|
// ... more questions
|
|
];
|
|
|
|
// Create the deck
|
|
final deck = Deck(
|
|
id: 'math-deck',
|
|
title: 'Math Basics',
|
|
description: 'Basic arithmetic questions',
|
|
questions: questions,
|
|
config: config,
|
|
);
|
|
```
|
|
|
|
### Creating and Processing an Attempt
|
|
|
|
An attempt represents a practice session. The engine intelligently selects questions based on priority and spaced repetition.
|
|
|
|
```dart
|
|
// Create attempt service (optionally with seed for reproducible randomness)
|
|
final attemptService = AttemptService(seed: 42);
|
|
|
|
// Create a new practice attempt
|
|
final attempt = attemptService.createAttempt(deck: deck);
|
|
// Or specify custom attempt size:
|
|
// final attempt = attemptService.createAttempt(deck: deck, attemptSize: 5);
|
|
|
|
// Display questions to user and collect answers
|
|
// answers maps questionId -> userSelectedAnswerIndex
|
|
final answers = {
|
|
'q1': 1, // User selected answer at index 1
|
|
'q2': 0, // User selected answer at index 0
|
|
// ...
|
|
};
|
|
|
|
// Process the attempt and get results
|
|
final result = attemptService.processAttempt(
|
|
deck: deck,
|
|
attempt: attempt,
|
|
answers: answers,
|
|
);
|
|
|
|
// Access the updated deck (with learning progress applied)
|
|
final updatedDeck = result.updatedDeck;
|
|
|
|
// Access attempt results
|
|
print('Score: ${result.result.percentageCorrect}%');
|
|
print('Correct: ${result.result.correctCount}/${result.result.totalQuestions}');
|
|
print('Deck progress: ${updatedDeck.practicePercentage.toStringAsFixed(1)}%');
|
|
```
|
|
|
|
### Accessing Deck Statistics
|
|
|
|
```dart
|
|
// Get overall deck statistics
|
|
print('Total questions: ${deck.numberOfQuestions}');
|
|
print('Known questions: ${deck.knownCount}');
|
|
print('Practice percentage: ${deck.practicePercentage}%');
|
|
print('Current attempt index: ${deck.currentAttemptIndex}');
|
|
|
|
// Access individual question state
|
|
final question = deck.questions.firstWhere((q) => q.id == 'q1');
|
|
print('Consecutive correct: ${question.consecutiveCorrect}');
|
|
print('Priority points: ${question.priorityPoints}');
|
|
print('Total attempts: ${question.totalAttempts}');
|
|
print('Is known: ${question.isKnown}');
|
|
```
|
|
|
|
### Manual Overrides
|
|
|
|
Manually control question state when needed (e.g., user wants to mark something as learned).
|
|
|
|
```dart
|
|
// Mark question as known (resets streak, sets isKnown=true)
|
|
final updatedDeck = DeckService.markQuestionAsKnown(
|
|
deck: deck,
|
|
questionId: 'q1',
|
|
);
|
|
|
|
// Mark question as needs practice (resets streak, sets isKnown=false, adds priority)
|
|
final updatedDeck2 = DeckService.markQuestionAsNeedsPractice(
|
|
deck: deck,
|
|
questionId: 'q1',
|
|
);
|
|
```
|
|
|
|
### Resetting a Deck
|
|
|
|
Reset learning progress while optionally preserving attempt history.
|
|
|
|
```dart
|
|
// Reset all learning progress (streaks, priority, known status)
|
|
final resetDeck = DeckService.resetDeck(
|
|
deck: deck,
|
|
resetAttemptCounts: false, // Keep attempt history if true
|
|
);
|
|
```
|
|
|
|
### Complete Example
|
|
|
|
```dart
|
|
import 'package:practice_engine/practice_engine.dart';
|
|
|
|
void main() {
|
|
// Setup
|
|
final config = const DeckConfig(
|
|
requiredConsecutiveCorrect: 2,
|
|
defaultAttemptSize: 5,
|
|
);
|
|
|
|
final questions = [
|
|
Question(id: 'q1', prompt: '2+2?', answers: ['3', '4'], correctAnswerIndex: 1),
|
|
Question(id: 'q2', prompt: '3+3?', answers: ['5', '6'], correctAnswerIndex: 1),
|
|
Question(id: 'q3', prompt: '4+4?', answers: ['7', '8'], correctAnswerIndex: 1),
|
|
];
|
|
|
|
var deck = Deck(
|
|
id: 'example',
|
|
title: 'Example Deck',
|
|
description: 'Simple math',
|
|
questions: questions,
|
|
config: config,
|
|
);
|
|
|
|
// Practice session
|
|
final service = AttemptService();
|
|
final attempt = service.createAttempt(deck: deck);
|
|
|
|
// Simulate user answers (all correct)
|
|
final answers = {
|
|
for (var q in attempt.questions) q.id: q.correctAnswerIndex
|
|
};
|
|
|
|
final result = service.processAttempt(
|
|
deck: deck,
|
|
attempt: attempt,
|
|
answers: answers,
|
|
);
|
|
|
|
deck = result.updatedDeck;
|
|
print('Score: ${result.result.percentageCorrect}%');
|
|
print('Deck progress: ${deck.practicePercentage.toStringAsFixed(1)}%');
|
|
}
|
|
```
|
|
|
|
## Architecture
|
|
|
|
The package is organized into three main layers:
|
|
|
|
- **Models** (`lib/models/`): Immutable data classes representing the domain
|
|
- `Deck`: Container for questions and configuration
|
|
- `Question`: Individual question with learning state
|
|
- `Attempt`: A practice session
|
|
- `AttemptResult`: Results from processing an attempt
|
|
- `DeckConfig`: Configuration settings
|
|
|
|
- **Algorithms** (`lib/algorithms/`): Pure functions for calculations
|
|
- `WeightedSelector`: Selects questions based on priority and spaced repetition
|
|
- `SpacedRepetition`: Calculates probability for known questions
|
|
- `PriorityManager`: Manages priority point calculations
|
|
|
|
- **Services** (`lib/logic/`): Business logic and orchestration
|
|
- `DeckService`: Deck-level operations (resets, manual overrides)
|
|
- `AttemptService`: Attempt creation and processing
|
|
|
|
All models are immutable and use `copyWith` for updates, ensuring predictable state management.
|
|
|
|
## Testing
|
|
|
|
### Running Tests
|
|
|
|
From the `packages/practice_engine` directory:
|
|
|
|
```bash
|
|
# Run all tests
|
|
dart test
|
|
|
|
# Run tests with coverage
|
|
dart test --coverage=coverage
|
|
|
|
# Run a specific test file
|
|
dart test test/deck_test.dart
|
|
|
|
# Run tests in watch mode (requires dart_test package)
|
|
dart test --watch
|
|
```
|
|
|
|
### Test Coverage
|
|
|
|
The package includes comprehensive test coverage for:
|
|
|
|
- Deck creation and statistics
|
|
- Question state management
|
|
- Attempt creation and processing
|
|
- Priority and spaced repetition algorithms
|
|
- Manual overrides
|
|
- Reset functionality
|
|
- Configuration validation
|
|
|
|
Test files are located in the `test/` directory and mirror the structure of the `lib/` directory.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
practice_engine/
|
|
├── lib/
|
|
│ ├── algorithms/ # Core algorithms (selection, repetition, priority)
|
|
│ ├── logic/ # Business logic services
|
|
│ ├── models/ # Data models
|
|
│ └── practice_engine.dart # Main export file
|
|
├── test/ # Test files
|
|
├── coverage/ # Test coverage reports
|
|
├── pubspec.yaml # Package configuration
|
|
└── README.md # This file
|
|
```
|
|
|
|
## Integration
|
|
|
|
This package is designed to be used as a library. To use it in your application:
|
|
|
|
1. **Flutter Apps**: Add to `pubspec.yaml` and import as shown in usage examples
|
|
2. **Dart CLI Tools**: Same as Flutter, just import and use
|
|
3. **Web Apps**: Compatible with Dart web compilation
|
|
|
|
The package has no external dependencies beyond the Dart SDK, making it lightweight and easy to integrate.
|
|
|
|
## Contributing
|
|
|
|
When contributing, ensure all tests pass:
|
|
|
|
```bash
|
|
dart test
|
|
```
|
|
|
|
Follow the existing code style and maintain test coverage for new features.
|
|
|