From 1a515603b08ec5e7e4e36ef82c532f4c67648025 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Fri, 10 Feb 2023 17:34:53 +0100 Subject: [PATCH] Add Group Bracket Generator --- .../screens/event/components/fight_list.dart | 10 +- .../event/components/loaded_event.dart | 5 +- .../generator/generators/group_generator.dart | 431 ++++++++++++++---- lib/src/screens/home/home.dart | 1 - lib/src/util/constants.dart | 2 +- lib/src/util/functions.dart | 26 ++ 6 files changed, 370 insertions(+), 105 deletions(-) create mode 100644 lib/src/util/functions.dart diff --git a/lib/src/screens/event/components/fight_list.dart b/lib/src/screens/event/components/fight_list.dart index a10adaf..83745bb 100644 --- a/lib/src/screens/event/components/fight_list.dart +++ b/lib/src/screens/event/components/fight_list.dart @@ -22,6 +22,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:grouped_list/grouped_list.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:steamwar_multitool/src/components/date_time_editor.dart'; @@ -204,7 +205,7 @@ class EventFightList extends HookConsumerWidget { update(); } }, - icon: const Icon(Icons.person)), + icon: const Icon(Icons.gavel)), ), Tooltip( message: "Change Group", @@ -302,6 +303,13 @@ class EventFightList extends HookConsumerWidget { icon: const Icon(Icons.add), ), const SizedBox(width: 8), + FilledButton.icon( + onPressed: () { + context.go("/event/${event.id}/generator"); + }, + label: const Text("Generate Fight"), + icon: const Icon(Icons.shuffle)), + const SizedBox(width: 8), PopupMenuButton( itemBuilder: (context) { return const [ diff --git a/lib/src/screens/event/components/loaded_event.dart b/lib/src/screens/event/components/loaded_event.dart index 4e2f4cc..abace7a 100644 --- a/lib/src/screens/event/components/loaded_event.dart +++ b/lib/src/screens/event/components/loaded_event.dart @@ -118,7 +118,7 @@ class LoadedEventScreen extends HookConsumerWidget { : null, icon: const Icon(Icons.save)), ], - leading: IconButton( + leading: BackButton( onPressed: () async { var canPop = false; if (changed.value) { @@ -147,7 +147,6 @@ class LoadedEventScreen extends HookConsumerWidget { } if (canPop) context.go("/"); }, - icon: const Icon(Icons.arrow_back), ), ), body: Padding( @@ -283,7 +282,7 @@ class LoadedEventScreen extends HookConsumerWidget { side: const BorderSide(color: Colors.transparent), label: Text(team.name, style: TextStyle( - color: team.color.computeLuminance() > 0.5 + color: team.color.computeLuminance() > 0.4 ? Colors.black : Colors.white)), backgroundColor: team.color, diff --git a/lib/src/screens/generator/generators/group_generator.dart b/lib/src/screens/generator/generators/group_generator.dart index f4ced38..f46b2b6 100644 --- a/lib/src/screens/generator/generators/group_generator.dart +++ b/lib/src/screens/generator/generators/group_generator.dart @@ -17,10 +17,18 @@ * along with this program. If not, see . */ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:steamwar_multitool/src/components/components.dart'; +import 'package:steamwar_multitool/src/delegates/delegates.dart'; +import 'package:steamwar_multitool/src/provider/data.dart'; +import 'package:steamwar_multitool/src/provider/events.dart'; import 'package:steamwar_multitool/src/types/types.dart'; +import 'package:steamwar_multitool/src/util/constants.dart'; class GroupBracketGenerator extends HookConsumerWidget { final EventExtended event; @@ -47,49 +55,233 @@ class GroupBracketGenerator extends HookConsumerWidget { } }, [groups.value]); + Team getTeam(int id) => + event.teams.firstWhere((element) => element.id == id); + + final startTime = useState(event.event.start); + final roundTime = useState(const Duration(minutes: 30)); + final startDelay = useState(const Duration(seconds: 30)); + + final gamemode = useState(null); + final map = useState(null); + final canProceed = useMemoized( () => groups.value.every((element) => element.length >= 2) && - groups.value.reduce((value, element) => value + element).length == - event.teams.length, - [groups.value, notInGroupTeams]); + groups.value.isNotEmpty && + gamemode.value != null && + map.value != null, + [groups.value, notInGroupTeams, gamemode.value, map.value]); + + final fights = useMemoized(() { + List>>> groupFights = []; + final random = Random(); + for (final group in groups.value) { + int rounds = group.length - 1; + List>> groupFight = []; + for (int i = 0; i < rounds; i++) { + final availableTeams = group.toList(); + if (group.length % 2 == 1) availableTeams.removeAt(i); + final List> roundFights = []; + while (availableTeams.isNotEmpty) { + final team1 = availableTeams.removeAt(0); + final team2 = availableTeams.removeAt(i % availableTeams.length); + final fight = [team1, team2]; + fight.shuffle(random); + roundFights.add(fight); + } + groupFight.add(roundFights); + } + groupFights.add(groupFight); + } + return groupFights; + }, [groups.value]); return Scaffold( appBar: AppBar( - title: Text('Group Bracket Generator'), - ), - floatingActionButton: FloatingActionButton( - onPressed: !canProceed - ? null - : () { - groups.value.add([]); - }, - child: Icon(Icons.navigate_next), - ), - body: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.start, - alignment: WrapAlignment.start, - children: [ - for (final team in notInGroupTeams) - Padding( - padding: const EdgeInsets.all(8.0), - child: Draggable( - data: team, - feedback: Material( - color: Colors.transparent, child: _TeamChip(team)), - child: _TeamChip(team), - ), - ), - ], + title: const Text('Group Bracket Generator'), + actions: [ + IconButton( + icon: const Icon(Icons.sync), + tooltip: "Auto Generate", + onPressed: () async { + final size = await showDialog( + context: context, + builder: (context) { + return HookBuilder(builder: (context) { + final groupsCount = useState(event.teams.length ~/ 2); + return AlertDialog( + title: const Text("Groups"), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Groups: ${groupsCount.value}"), + Slider( + value: groupsCount.value.toDouble(), + min: 1, + max: event.teams.length / 2, + divisions: (event.teams.length ~/ 2) - 1, + label: groupsCount.value.toString(), + onChanged: (value) { + groupsCount.value = value.toInt(); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(0), + child: const Text("Cancel"), + ), + FilledButton( + onPressed: () => + Navigator.of(context).pop(groupsCount.value), + child: const Text("Ok"), + ), + ], + ); + }); + }); + if (size == 0) return; + final random = Random(); + final teams = event.teams.toList(); + teams.shuffle(random); + final groupsNew = List.generate(size, (index) => []); + int k = 0; + for (int i = 0; i < teams.length; i++) { + groupsNew[k].add(teams[i].id); + k++; + if (k >= size) k = 0; + } + groups.value = groupsNew; + }, ), - Wrap( - crossAxisAlignment: WrapCrossAlignment.start, - alignment: WrapAlignment.start, - children: [ - for (final group in groups.value) + ], + leading: BackButton( + onPressed: () => context.go('/event/${event.event.id}'), + ), + ), + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.check), + onPressed: canProceed + ? () async { + final repo = await ref.read(eventRepositoryProvider.future); + for (final group in fights) { + for (final round in group) { + for (final fight in round) { + final blue = getTeam(fight[0]); + final red = getTeam(fight[1]); + final start = startTime.value.add( + roundTime.value * group.indexOf(round) + + startDelay.value * + (round.indexOf(fight) + + (fights.indexOf(group) * round.length)), + ); + await repo.createFight( + event.event.id, + start, + gamemode.value!, + map.value!, + blue, + red, + 0, + "Gruppe ${fights.indexOf(group) + 1}"); + } + } + } + context.go("/events/${event.event.id}"); + } + : null, + label: const Text("Generate")), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.start, + alignment: WrapAlignment.start, + children: [ + for (final team in notInGroupTeams) + Padding( + padding: const EdgeInsets.all(8.0), + child: Draggable( + data: team, + feedback: Material( + color: Colors.transparent, child: _TeamChip(team)), + child: _TeamChip(team), + ), + ), + ], + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.start, + alignment: WrapAlignment.start, + children: [ + for (final group in groups.value) + Padding( + padding: const EdgeInsets.all(8.0), + child: DragTarget( + builder: (context, candidateData, rejectedData) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 200, + maxWidth: 200, + minHeight: 200, + ), + child: Card( + child: Column( + children: [ + Text( + 'Group ${groups.value.indexOf(group) + 1}'), + Wrap( + children: [ + for (final teamId in group) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + child: _TeamChip( + event.teams.firstWhere((element) => + element.id == teamId), + ), + onTap: () { + groups.value = [ + for (final addGroup + in groups.value) + if (addGroup.contains(teamId)) + addGroup + .where((element) => + element != teamId) + .toList() + else + addGroup + ]; + }, + ), + ), + ], + ), + ], + ), + ), + ); + }, + onWillAccept: (data) => true, + onAccept: (data) { + groups.value = [ + for (final addGroup in groups.value) + if (addGroup.contains(data.id)) + addGroup + .where((element) => element != data.id) + .toList() + else if (addGroup == group) + [...addGroup, data.id] + else + addGroup + ]; + }, + ), + ), Padding( padding: const EdgeInsets.all(8.0), child: DragTarget( @@ -99,34 +291,8 @@ class GroupBracketGenerator extends HookConsumerWidget { height: 200, child: Card( child: Column( - children: [ - Text('Group ${groups.value.indexOf(group) + 1}'), - Wrap( - children: [ - for (final teamId in group) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - child: _TeamChip( - event.teams.firstWhere((element) => - element.id == teamId), - ), - onTap: () { - groups.value = [ - for (final addGroup in groups.value) - if (addGroup.contains(teamId)) - addGroup - .where((element) => - element != teamId) - .toList() - else - addGroup - ]; - }, - ), - ), - ], - ), + children: const [ + Text('New Group'), ], ), ), @@ -135,47 +301,114 @@ class GroupBracketGenerator extends HookConsumerWidget { onWillAccept: (data) => true, onAccept: (data) { groups.value = [ - for (final addGroup in groups.value) - if (addGroup.contains(data.id)) - addGroup - .where((element) => element != data.id) - .toList() - else if (addGroup == group) - [...addGroup, data.id] - else - addGroup + ...groups.value, + [data.id], ]; }, ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: DragTarget( - builder: (context, candidateData, rejectedData) { - return SizedBox( - width: 200, - height: 200, - child: Card( - child: Column( - children: const [ - Text('New Group'), + ], + ), + Divider(), + Column( + children: [ + Row( + children: [ + const Text('Start Time:'), + DateTimeEditor((p0) { + startTime.value = p0; + }, startTime.value, true), + ], + ), + Text('Round Time: ${roundTime.value.inMinutes} min'), + Slider( + value: roundTime.value.inMinutes.toDouble(), + onChanged: (t) => + roundTime.value = Duration(minutes: t.round()), + min: 5, + max: 60, + divisions: 60 - 5, + label: '${roundTime.value.inMinutes} min'), + Text('Start Delay: ${startDelay.value.inSeconds} sec'), + Slider( + value: startDelay.value.inSeconds.toDouble(), + onChanged: (t) => + startDelay.value = Duration(seconds: t.round()), + min: 0, + max: 30, + divisions: 30, + label: '${startDelay.value.inSeconds} sec'), + Row( + children: [ + const Text('Gamemode:'), + TextButton( + onPressed: () async { + final newmode = await showSearch( + context: context, + delegate: GamemodeSearchDelegate( + await ref.read(fightServersProvider.future))); + gamemode.value = newmode; + }, + child: Text(gamemode.value ?? 'None')), + ], + ), + Row( + children: [ + const Text('Map:'), + TextButton( + onPressed: gamemode.value != null + ? () async { + final newmap = await showSearch( + context: context, + delegate: MapSearchDelegate(await ref + .read(mapsProvider.future) + .then((value) => + value[gamemode.value]!))); + map.value = newmap; + } + : null, + child: Text(map.value ?? 'None')), + ], + ), + const Divider(), + for (final group in fights) + Column( + children: [ + Text('Group ${fights.indexOf(group) + 1}', + style: Theme.of(context).textTheme.headlineMedium), + for (final round in group) + Column( + children: [ + Text('Round ${group.indexOf(round) + 1}', + style: + Theme.of(context).textTheme.headlineSmall), + const Divider(), + for (final fight in round) + ListTile( + leading: Chip( + label: Text( + kDateFormat.format( + startTime.value.add( + roundTime.value * group.indexOf(round) + + startDelay.value * + (round.indexOf(fight) + + (fights.indexOf(group) * + round.length)), + ), + ), + ), + ), + title: Text( + '${getTeam(fight[0]).name} vs ${getTeam(fight[1]).name}'), + ), ], ), - ), - ); - }, - onWillAccept: (data) => true, - onAccept: (data) { - groups.value = [ - ...groups.value, - [data.id], - ]; - }, - ), - ), - ], - ), - ], + ], + ) + ], + ), + ], + ), ), ); } diff --git a/lib/src/screens/home/home.dart b/lib/src/screens/home/home.dart index 7902dd2..daa42a8 100644 --- a/lib/src/screens/home/home.dart +++ b/lib/src/screens/home/home.dart @@ -24,7 +24,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:steamwar_multitool/src/provider/mods.dart'; import 'package:steamwar_multitool/src/provider/provider.dart'; import 'package:steamwar_multitool/src/screens/home/lists/events_list.dart'; diff --git a/lib/src/util/constants.dart b/lib/src/util/constants.dart index b5a408c..fefbba0 100644 --- a/lib/src/util/constants.dart +++ b/lib/src/util/constants.dart @@ -19,4 +19,4 @@ import 'package:intl/intl.dart'; -final kDateFormat = DateFormat("dd.MM.yyyy HH:mm"); +final kDateFormat = DateFormat("dd.MM.yyyy HH:mm:ss"); diff --git a/lib/src/util/functions.dart b/lib/src/util/functions.dart new file mode 100644 index 0000000..7f6c0ba --- /dev/null +++ b/lib/src/util/functions.dart @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +int wrapAround(int value, int max) { + if (value < max) { + return value; + } else { + return value % max; + } +}