|
|
2 months ago | |
|---|---|---|
| android | 3 months ago | |
| lib | 2 months ago | |
| packages | 3 months ago | |
| .gitignore | 3 months ago | |
| .metadata | 3 months ago | |
| README.md | 3 months ago | |
| analysis_options.yaml | 3 months ago | |
| issues.json | 2 months ago | |
| pubspec.lock | 3 months ago | |
| pubspec.yaml | 3 months ago | |
README.md
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:
dependencies:
practice_engine:
path: packages/practice_engine # For local development
# Or use pub.dev if published:
# practice_engine: ^1.0.0
Then run:
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).
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.
// 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
// 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).
// 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.
// Reset all learning progress (streaks, priority, known status)
final resetDeck = DeckService.resetDeck(
deck: deck,
resetAttemptCounts: false, // Keep attempt history if true
);
Complete Example
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 domainDeck: Container for questions and configurationQuestion: Individual question with learning stateAttempt: A practice sessionAttemptResult: Results from processing an attemptDeckConfig: Configuration settings
-
Algorithms (
lib/algorithms/): Pure functions for calculationsWeightedSelector: Selects questions based on priority and spaced repetitionSpacedRepetition: Calculates probability for known questionsPriorityManager: Manages priority point calculations
-
Services (
lib/logic/): Business logic and orchestrationDeckService: 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:
# 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:
- Flutter Apps: Add to
pubspec.yamland import as shown in usage examples - Dart CLI Tools: Same as Flutter, just import and use
- 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:
dart test
Follow the existing code style and maintain test coverage for new features.