diff --git a/lib/features/tournament/presentation/provider/shuffle_provider.dart b/lib/features/tournament/presentation/provider/shuffle_provider.dart new file mode 100644 index 0000000..fb0e0e0 --- /dev/null +++ b/lib/features/tournament/presentation/provider/shuffle_provider.dart @@ -0,0 +1,146 @@ +// lib/features/tournament/presentation/provider/shuffle_provider.dart + +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../data/models/player_model.dart'; +import 'active_players_provider.dart'; +import 'shuffle_settings_provider.dart'; + +class ShuffleResult { + final List> fields; // 3 Felder à max 12 + final List bench; + final String message; + + ShuffleResult(this.fields, this.bench, {this.message = ''}); +} + +// Dies ist jetzt ein STATEFUL Provider → wird nur bei explizitem Aufruf neu berechnet +final shuffleResultProvider = +StateProvider((ref) => ShuffleResult([], [], message: 'Noch nicht gemischt')); + +final shuffleControllerProvider = Provider((ref) => ShuffleController(ref)); + +class ShuffleController { + final Ref _ref; + ShuffleController(this._ref); + + /// Hauptfunktion – wird nur vom Button aufgerufen + void shuffle() { + final activePlayers = _ref.read(activePlayersProvider); + final settings = _ref.read(shuffleSettingsProvider); + + if (activePlayers.isEmpty) { + _ref.read(shuffleResultProvider.notifier).state = + ShuffleResult([], [], message: 'Keine aktiven Spieler'); + return; + } + + final result = ShuffleAlgorithm().run(activePlayers, settings); + + // Jetzt erst speichern (einmalig!) + _commitAllChanges(result); + + // UI aktualisieren + _ref.read(shuffleResultProvider.notifier).state = result; + } + + void _commitAllChanges(ShuffleResult result) { + // Aussetzer markieren + for (final p in result.bench) { + p.hasSatOut = true; + p.save(); + } + + // Alle anderen zurücksetzen (falls nötig) + final allActive = _ref.read(activePlayersProvider); + for (final p in allActive) { + if (!result.bench.contains(p)) { + p.hasSatOut = false; + p.save(); + } + } + } +} + +class ShuffleAlgorithm { + final Random _random = Random(); + + ShuffleResult run(List players, ShuffleSettings settings) { + // 1. Aussetzer bestimmen + final bench = _determineBench(players); + final playing = players.where((p) => !bench.contains(p)).toList(); + + // 2. Mischen + List ordered; + switch (settings.mode) { + case ShuffleMode.random: + ordered = playing..shuffle(_random); + break; + default: + ordered = _balancedShuffle(playing, settings); + break; + } + + // 3. Auf 3 Felder verteilen + final fields = >[]; + for (int i = 0; i < 3; i++) { + final start = i * 12; + final end = (start + 12).clamp(0, ordered.length); + fields.add(start < ordered.length ? ordered.sublist(start, end) : []); + } + + return ShuffleResult( + fields, + bench, + message: 'Gemischte Teams (${ordered.length} Spieler)', + ); + } + + List _determineBench(List players) { + if (players.length <= 36) return []; + + final excess = players.length - 36; + final never = players.where((p) => !p.hasSatOut).toList(); + final already = players.where((p) => p.hasSatOut).toList(); + + final bench = []; + bench.addAll(never.take(excess)); + if (bench.length < excess) { + bench.addAll(already.take(excess - bench.length)); + } + return bench; + } + + List _balancedShuffle(List players, ShuffleSettings settings) { + players.sort((a, b) { + if (settings.prioritizeGenderBalance) { + final g = a.gender.index.compareTo(b.gender.index); + if (g != 0) return g; + } + if (settings.prioritizeLevelBalance) { + final l = b.skillLevel.index.compareTo(a.skillLevel.index); + if (l != 0) return l; + } + if (settings.usePositions) { + final az = a.positions.contains(Position.zuspieler) ? 1 : 0; + final bz = b.positions.contains(Position.zuspieler) ? 1 : 0; + if (az != bz) return bz.compareTo(az); + } + return 0; + }); + + final teams = List.generate(6, (_) => []); + for (int i = 0; i < players.length; i++) { + teams[i % 6].add(players[i]); + } + for (final t in teams) t.shuffle(_random); + + final result = []; + for (int i = 0; i < 3; i++) { + result.addAll(teams[i]); + result.addAll(teams[i + 3]); + } + return result; + } +} \ No newline at end of file diff --git a/lib/features/tournament/presentation/screens/fields_screen.dart b/lib/features/tournament/presentation/screens/fields_screen.dart index 6d7b225..c717f55 100644 --- a/lib/features/tournament/presentation/screens/fields_screen.dart +++ b/lib/features/tournament/presentation/screens/fields_screen.dart @@ -1,31 +1,23 @@ +// lib/features/tournament/presentation/screens/fields_screen.dart + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../data/models/player_model.dart'; -import '../provider/active_players_provider.dart'; -import '../provider/player_provider.dart'; // für awardWinToPlayers -import '../widgets/volleyball_field_widget.dart'; import 'package:flutter/services.dart'; +import '../../data/models/player_model.dart'; +import '../provider/player_provider.dart'; +import '../provider/shuffle_provider.dart'; // <-- jetzt mit shuffleResultProvider + shuffleControllerProvider +import '../widgets/volleyball_field_widget.dart'; class FieldsScreen extends ConsumerWidget { const FieldsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final activePlayers = ref.watch(activePlayersProvider); - - // Shuffle für Demo (später echter Algorithmus) - final shuffled = List.from(activePlayers)..shuffle(); - - // Maximal 36 Spieler auf Felder, Rest Aussetzer - final playersOnFields = shuffled.length > 36 ? shuffled.sublist(0, 36) : shuffled; - final bench = shuffled.length > 36 ? shuffled.sublist(36) : []; - - // Felder aufteilen - final field1 = _slice(playersOnFields, 0, 12); - final field2 = _slice(playersOnFields, 12, 24); - final field3 = _slice(playersOnFields, 24, 36); - - final fields = [field1, field2, field3]; + // Hier lesen wir den aktuellen Shuffle-Stand aus + final shuffleResult = ref.watch(shuffleResultProvider); + final fields = shuffleResult.fields; + final bench = shuffleResult.bench; + final message = shuffleResult.message; return Scaffold( body: Column( @@ -35,9 +27,11 @@ class FieldsScreen extends ConsumerWidget { child: PageView.builder( itemCount: 3, itemBuilder: (context, index) { - final fieldPlayers = fields[index]; + final fieldPlayers = index < fields.length ? fields[index] : []; - final teamA = fieldPlayers.sublist(0, fieldPlayers.length.clamp(0, 6)); + final teamA = fieldPlayers.isNotEmpty + ? fieldPlayers.sublist(0, fieldPlayers.length.clamp(0, 6)) + : []; final teamB = fieldPlayers.length > 6 ? fieldPlayers.sublist(6, fieldPlayers.length.clamp(6, 12)) : []; @@ -46,11 +40,14 @@ class FieldsScreen extends ConsumerWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - Expanded(child: VolleyballFieldWidget(teamA: teamA, teamB: teamB, fieldNumber: index + 1)), - + Expanded( + child: VolleyballFieldWidget( + teamA: teamA, + teamB: teamB, + fieldNumber: index + 1, + ), + ), const SizedBox(height: 20), - - // Gewinner-Buttons Row( children: [ Expanded( @@ -74,7 +71,7 @@ class FieldsScreen extends ConsumerWidget { SnackBar( content: Text('Oben gewinnt! +1 Schleifchen für ${teamA.length} Spieler'), backgroundColor: Colors.green, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 800), ), ); }, @@ -95,14 +92,14 @@ class FieldsScreen extends ConsumerWidget { ), onPressed: teamB.isEmpty ? null - : () async{ - HapticFeedback.mediumImpact(); + : () async { + await HapticFeedback.mediumImpact(); ref.read(playerListProvider.notifier).awardWinToPlayers(teamB); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Unten gewinnt! +1 Schleifchen für ${teamB.length} Spieler'), backgroundColor: Colors.red, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 800), ), ); }, @@ -131,28 +128,30 @@ class FieldsScreen extends ConsumerWidget { spacing: 12, runSpacing: 8, children: bench - .map((p) => Chip( - label: Text(p.name), - backgroundColor: Colors.orange.shade300, - )) + .map((p) => Chip(label: Text(p.name), backgroundColor: Colors.orange.shade300)) .toList(), ), ], ), ), - // Shuffle Button unten + // NEUER SHUFFLE-BUTTON – ruft den Controller auf Padding( padding: const EdgeInsets.all(16), child: ElevatedButton.icon( icon: const Icon(Icons.shuffle), - label: const Text('Neu mischen', style: TextStyle(fontSize: 18)), - onPressed: () async{ - HapticFeedback.mediumImpact(); - ref.invalidate(activePlayersProvider); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Neue Teams gemischt!'), duration: Duration(milliseconds: 100)), - ); + label: Text( + message.isEmpty ? 'Neu mischen' : message, + style: const TextStyle(fontSize: 18), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.deepPurple, + ), + onPressed: () async { + await HapticFeedback.selectionClick(); + // Das ist der entscheidende Aufruf! + ref.read(shuffleControllerProvider).shuffle(); }, ), ), @@ -160,11 +159,4 @@ class FieldsScreen extends ConsumerWidget { ), ); } - - // Hilfsfunktion für sicheres Slicing - List _slice(List list, int start, int end) { - final actualEnd = end.clamp(0, list.length); - if (start >= actualEnd) return []; - return list.sublist(start, actualEnd); - } } \ No newline at end of file