Aktuelle Version mit richtigem Shuffle
This commit is contained in:
parent
41a4b2481a
commit
404769af45
@ -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<List<PlayerModel>> fields; // 3 Felder à max 12
|
||||||
|
final List<PlayerModel> 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<ShuffleResult>((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<PlayerModel> players, ShuffleSettings settings) {
|
||||||
|
// 1. Aussetzer bestimmen
|
||||||
|
final bench = _determineBench(players);
|
||||||
|
final playing = players.where((p) => !bench.contains(p)).toList();
|
||||||
|
|
||||||
|
// 2. Mischen
|
||||||
|
List<PlayerModel> 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 = <List<PlayerModel>>[];
|
||||||
|
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<PlayerModel> _determineBench(List<PlayerModel> 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 = <PlayerModel>[];
|
||||||
|
bench.addAll(never.take(excess));
|
||||||
|
if (bench.length < excess) {
|
||||||
|
bench.addAll(already.take(excess - bench.length));
|
||||||
|
}
|
||||||
|
return bench;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlayerModel> _balancedShuffle(List<PlayerModel> 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, (_) => <PlayerModel>[]);
|
||||||
|
for (int i = 0; i < players.length; i++) {
|
||||||
|
teams[i % 6].add(players[i]);
|
||||||
|
}
|
||||||
|
for (final t in teams) t.shuffle(_random);
|
||||||
|
|
||||||
|
final result = <PlayerModel>[];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
result.addAll(teams[i]);
|
||||||
|
result.addAll(teams[i + 3]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +1,23 @@
|
|||||||
|
// lib/features/tournament/presentation/screens/fields_screen.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 '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 {
|
class FieldsScreen extends ConsumerWidget {
|
||||||
const FieldsScreen({super.key});
|
const FieldsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final activePlayers = ref.watch(activePlayersProvider);
|
// Hier lesen wir den aktuellen Shuffle-Stand aus
|
||||||
|
final shuffleResult = ref.watch(shuffleResultProvider);
|
||||||
// Shuffle für Demo (später echter Algorithmus)
|
final fields = shuffleResult.fields;
|
||||||
final shuffled = List<PlayerModel>.from(activePlayers)..shuffle();
|
final bench = shuffleResult.bench;
|
||||||
|
final message = shuffleResult.message;
|
||||||
// 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) : <PlayerModel>[];
|
|
||||||
|
|
||||||
// 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];
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
@ -35,9 +27,11 @@ class FieldsScreen extends ConsumerWidget {
|
|||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final fieldPlayers = fields[index];
|
final fieldPlayers = index < fields.length ? fields[index] : <PlayerModel>[];
|
||||||
|
|
||||||
final teamA = fieldPlayers.sublist(0, fieldPlayers.length.clamp(0, 6));
|
final teamA = fieldPlayers.isNotEmpty
|
||||||
|
? fieldPlayers.sublist(0, fieldPlayers.length.clamp(0, 6))
|
||||||
|
: <PlayerModel>[];
|
||||||
final teamB = fieldPlayers.length > 6
|
final teamB = fieldPlayers.length > 6
|
||||||
? fieldPlayers.sublist(6, fieldPlayers.length.clamp(6, 12))
|
? fieldPlayers.sublist(6, fieldPlayers.length.clamp(6, 12))
|
||||||
: <PlayerModel>[];
|
: <PlayerModel>[];
|
||||||
@ -46,11 +40,14 @@ class FieldsScreen extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Gewinner-Buttons
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -74,7 +71,7 @@ class FieldsScreen extends ConsumerWidget {
|
|||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Oben gewinnt! +1 Schleifchen für ${teamA.length} Spieler'),
|
content: Text('Oben gewinnt! +1 Schleifchen für ${teamA.length} Spieler'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(milliseconds: 800),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -95,14 +92,14 @@ class FieldsScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: teamB.isEmpty
|
onPressed: teamB.isEmpty
|
||||||
? null
|
? null
|
||||||
: () async{
|
: () async {
|
||||||
HapticFeedback.mediumImpact();
|
await HapticFeedback.mediumImpact();
|
||||||
ref.read(playerListProvider.notifier).awardWinToPlayers(teamB);
|
ref.read(playerListProvider.notifier).awardWinToPlayers(teamB);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Unten gewinnt! +1 Schleifchen für ${teamB.length} Spieler'),
|
content: Text('Unten gewinnt! +1 Schleifchen für ${teamB.length} Spieler'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(milliseconds: 800),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -131,28 +128,30 @@ class FieldsScreen extends ConsumerWidget {
|
|||||||
spacing: 12,
|
spacing: 12,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: bench
|
children: bench
|
||||||
.map((p) => Chip(
|
.map((p) => Chip(label: Text(p.name), backgroundColor: Colors.orange.shade300))
|
||||||
label: Text(p.name),
|
|
||||||
backgroundColor: Colors.orange.shade300,
|
|
||||||
))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Shuffle Button unten
|
// NEUER SHUFFLE-BUTTON – ruft den Controller auf
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.shuffle),
|
icon: const Icon(Icons.shuffle),
|
||||||
label: const Text('Neu mischen', style: TextStyle(fontSize: 18)),
|
label: Text(
|
||||||
onPressed: () async{
|
message.isEmpty ? 'Neu mischen' : message,
|
||||||
HapticFeedback.mediumImpact();
|
style: const TextStyle(fontSize: 18),
|
||||||
ref.invalidate(activePlayersProvider);
|
),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
style: ElevatedButton.styleFrom(
|
||||||
const SnackBar(content: Text('Neue Teams gemischt!'), duration: Duration(milliseconds: 100)),
|
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<PlayerModel> _slice(List<PlayerModel> list, int start, int end) {
|
|
||||||
final actualEnd = end.clamp(0, list.length);
|
|
||||||
if (start >= actualEnd) return [];
|
|
||||||
return list.sublist(start, actualEnd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user