readme
This commit is contained in:
@@ -1,3 +1,316 @@
|
||||
# decky_app
|
||||
# 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.
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
Reference in New Issue
Block a user