adaptive_gamification 0.0.1 copy "adaptive_gamification: ^0.0.1" to clipboard
adaptive_gamification: ^0.0.1 copied to clipboard

Adaptive gamification engine powered by reinforcement learning for dynamic difficulty adjustment.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:adaptive_gamification/adaptive_gamification.dart';

void main() {
  runApp(const AdaptiveGamificationExampleApp());
}

class AdaptiveGamificationExampleApp extends StatelessWidget {
  const AdaptiveGamificationExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Adaptive Gamification Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const DemoHomePage(),
    );
  }
}

class DemoHomePage extends StatefulWidget {
  const DemoHomePage({super.key});

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage> {
  final AdaptiveEngine _engine = AdaptiveEngine();

  bool _loading = true;
  String _error = '';

  // ---- Demo user/session state (frontend state) ----
  int _difficultyIndex = 2; // 0..4 (veryEasy..veryHard) - you control this mapping in the app
  int _streak = 0;
  int _total = 0;
  int _correct = 0;

  double _responseTime = 4.0; // seconds (user adjusts)
  AdaptiveDecision? _lastDecision;

  @override
  void initState() {
    super.initState();
    _initEngine();
  }

  Future<void> _initEngine() async {
    try {
      // IMPORTANT: This asset lives in the EXAMPLE app, not in the library package.
      await _engine.initFromAsset(policyAssetPath: 'assets/adaptive_policy.json');
      setState(() => _loading = false);
    } catch (e) {
      setState(() {
        _loading = false;
        _error = e.toString();
      });
    }
  }

  double get _accuracy {
    // Laplace smoothing prevents extreme 0/1 early in the session.
    // Example: first wrong -> (0+1)/(1+2)=0.33 instead of 0.0
    return ((_correct + 1) / (_total + 2)).clamp(0.0, 1.0);
  }

  String _difficultyLabelFromIndex(int i) {
    const labels = ['veryEasy', 'easy', 'medium', 'hard', 'veryHard'];
    if (i < 0) return labels.first;
    if (i >= labels.length) return labels.last;
    return labels[i];
  }

  void _applyDecision(String nextDifficulty) {
    // nextDifficulty from policy is string (veryEasy..veryHard)
    // Apply a *bounded* transition to avoid unrealistic jumps in the demo UI.
    const order = ['veryEasy', 'easy', 'medium', 'hard', 'veryHard'];
    final target = order.indexOf(nextDifficulty);
    if (target == -1) return;

    final current = _difficultyIndex;
    if (target == current) return;

    // Move at most one step toward the target.
    if (target > current) {
      _difficultyIndex = (current + 1).clamp(0, order.length - 1);
    } else {
      _difficultyIndex = (current - 1).clamp(0, order.length - 1);
    }
  }

  Future<void> _answer(bool isCorrect) async {
    setState(() {
      _total += 1;
      if (isCorrect) {
        _correct += 1;
        _streak += 1;
      } else {
        _streak = 0;
      }
    });

    final userState = UserState(
      currentDifficultyIndex: _difficultyIndex,
      accuracy: _accuracy,
      responseTime: _responseTime,
      correctStreak: _streak,
    );

    final decision = _engine.decide(userState);

    setState(() {
      _lastDecision = decision;
      _applyDecision(decision.nextDifficulty);
    });
  }

  void _reset() {
    setState(() {
      _difficultyIndex = 2;
      _streak = 0;
      _total = 0;
      _correct = 0;
      _responseTime = 4.0;
      _lastDecision = null;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    if (_error.isNotEmpty) {
      return Scaffold(
        appBar: AppBar(title: const Text('Adaptive Gamification Example')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Text(
            'Failed to initialize engine:\n\n$_error',
            style: const TextStyle(fontSize: 14),
          ),
        ),
      );
    }

    final currentDifficulty = _difficultyLabelFromIndex(_difficultyIndex);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Adaptive Gamification Example'),
        actions: [
          IconButton(
            onPressed: _reset,
            icon: const Icon(Icons.refresh),
            tooltip: 'Reset',
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _Card(
            title: 'Current Session State',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _RowLine(label: 'Difficulty', value: currentDifficulty),
                _RowLine(label: 'Accuracy', value: (_accuracy * 100).toStringAsFixed(1) + '%'),
                _RowLine(label: 'Streak', value: '$_streak'),
                _RowLine(label: 'Answered', value: '$_total (correct: $_correct)'),
              ],
            ),
          ),
          const SizedBox(height: 12),
          _Card(
            title: 'Response Time (seconds)',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(_responseTime.toStringAsFixed(2)),
                Slider(
                  value: _responseTime,
                  min: 1.0,
                  max: 20.0,
                  divisions: 38,
                  label: _responseTime.toStringAsFixed(2),
                  onChanged: (v) => setState(() => _responseTime = v),
                ),
              ],
            ),
          ),
          const SizedBox(height: 12),
          _Card(
            title: 'Next Decision',
            child: _lastDecision == null
                ? const Text('Answer a question to see a decision.')
                : Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _RowLine(label: 'Next Difficulty', value: _lastDecision!.nextDifficulty),
                _RowLine(label: 'Reason', value: _lastDecision!.reason),
              ],
            ),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: () => _answer(true),
                  icon: const Icon(Icons.check_circle_outline),
                  label: const Text('Correct'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: () => _answer(false),
                  icon: const Icon(Icons.cancel_outlined),
                  label: const Text('Wrong'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          const Text(
            'Tip: change response time and alternate correct/wrong answers to observe adaptation. '
            'The demo limits difficulty changes to one level per answer to avoid abrupt jumps.',
          ),
        ],
      ),
    );
  }
}

class _Card extends StatelessWidget {
  final String title;
  final Widget child;

  const _Card({required this.title, required this.child});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0.5,
      child: Padding(
        padding: const EdgeInsets.all(14),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 10),
            child,
          ],
        ),
      ),
    );
  }
}

class _RowLine extends StatelessWidget {
  final String label;
  final String value;

  const _RowLine({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 6),
      child: Row(
        children: [
          SizedBox(width: 120, child: Text(label)),
          Expanded(child: Text(value, style: const TextStyle(fontWeight: FontWeight.w600))),
        ],
      ),
    );
  }
}
1
likes
0
points
89
downloads

Publisher

unverified uploader

Weekly Downloads

Adaptive gamification engine powered by reinforcement learning for dynamic difficulty adjustment.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on adaptive_gamification