diff --git a/data/lib/api/match/match_model.dart b/data/lib/api/match/match_model.dart index 8d9ffe8d..b9e790c0 100644 --- a/data/lib/api/match/match_model.dart +++ b/data/lib/api/match/match_model.dart @@ -1,6 +1,7 @@ // ignore_for_file: non_constant_identifier_names import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../converter/timestamp_json_converter.dart'; @@ -210,6 +211,23 @@ extension DataMatchModel on MatchModel { revised_target == null && match_type == MatchType.limitedOvers; } + + double getRunRate(String teamId) { + if (!team_ids.contains(teamId)) return 0; + + final team = teams.firstWhereOrNull((team) => team.team_id == teamId); + if (team == null) return 0; + + final runs = team.run; + final overs = team.over; + + if (overs == 0) { + return 0.0; + } + + final runRate = runs / overs; + return double.parse(runRate.toStringAsFixed(2)); + } } enum WinnerByType { diff --git a/data/lib/api/tournament/tournament_model.dart b/data/lib/api/tournament/tournament_model.dart index 9e656a61..87473840 100644 --- a/data/lib/api/tournament/tournament_model.dart +++ b/data/lib/api/tournament/tournament_model.dart @@ -74,8 +74,7 @@ enum TournamentType { miniRobin(2, minTeamReq: 3), boxLeague(3, minTeamReq: 4), doubleOut(4, minTeamReq: 4), - superOver(5, minTeamReq: 2), - bestOf(6, minTeamReq: 2), + bestOfThree(6, minTeamReq: 2), custom(7, minTeamReq: 2); final int value; diff --git a/data/lib/api/tournament/tournament_model.g.dart b/data/lib/api/tournament/tournament_model.g.dart index a24fcf9b..df91f380 100644 --- a/data/lib/api/tournament/tournament_model.g.dart +++ b/data/lib/api/tournament/tournament_model.g.dart @@ -58,8 +58,7 @@ const _$TournamentTypeEnumMap = { TournamentType.miniRobin: 2, TournamentType.boxLeague: 3, TournamentType.doubleOut: 4, - TournamentType.superOver: 5, - TournamentType.bestOf: 6, + TournamentType.bestOfThree: 6, TournamentType.custom: 7, }; diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 069eb986..7b077075 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -301,16 +301,14 @@ "tournament_type_mini_robin": "Mini Robin", "tournament_type_box_league": "Box League", "tournament_type_double_out": "Double Out", - "tournament_type_super_over": "Super Over", - "tournament_type_best_of": "Best Of", + "tournament_type_best_of_three": "Best Of three", "tournament_type_custom": "Custom", "tournament_type_knock_out_description": "Teams face off in a single elimination, with the loser immediately knocked out.\nMinimum {count} teams required.", "tournament_type_mini_robin_description": "A smaller round-robin format where each team plays once against all others.\nMinimum {count} teams required.", "tournament_type_box_league_description": "Teams are divided into groups, with top teams advancing to the knockout stage.\nMinimum {count} teams required.", "tournament_type_double_out_description": "Teams get two chances before being knocked out, with a winners and losers bracket.\nMinimum {count} teams required.", - "tournament_type_super_over_description": "A knockout format with a super over to decide tied matches.\nMinimum {count} teams required.", - "tournament_type_best_of_description": "Teams play a series of matches, and the first to win the majority is the champion.\nMinimum {count} teams required.", + "tournament_type_best_of_three_description": "Teams play a series of matches, and the first to win the majority is the champion.\nMinimum {count} teams required.", "tournament_type_custom_description": "Fully flexible, create a structure that suits your needs.\nMinimum {count} teams required.", "add_team_screen_title": "Add Team", diff --git a/khelo/lib/domain/extensions/enum_extensions.dart b/khelo/lib/domain/extensions/enum_extensions.dart index 134282ff..e1a8d9a2 100644 --- a/khelo/lib/domain/extensions/enum_extensions.dart +++ b/khelo/lib/domain/extensions/enum_extensions.dart @@ -302,10 +302,8 @@ extension TournamentTypeString on TournamentType { return context.l10n.tournament_type_box_league; case TournamentType.doubleOut: return context.l10n.tournament_type_double_out; - case TournamentType.superOver: - return context.l10n.tournament_type_super_over; - case TournamentType.bestOf: - return context.l10n.tournament_type_best_of; + case TournamentType.bestOfThree: + return context.l10n.tournament_type_best_of_three; case TournamentType.custom: return context.l10n.tournament_type_custom; } @@ -321,10 +319,8 @@ extension TournamentTypeString on TournamentType { return context.l10n.tournament_type_box_league_description(minTeamReq); case TournamentType.doubleOut: return context.l10n.tournament_type_double_out_description(minTeamReq); - case TournamentType.superOver: - return context.l10n.tournament_type_super_over_description(minTeamReq); - case TournamentType.bestOf: - return context.l10n.tournament_type_best_of_description(minTeamReq); + case TournamentType.bestOfThree: + return context.l10n.tournament_type_best_of_three_description(minTeamReq); case TournamentType.custom: return context.l10n.tournament_type_custom_description(minTeamReq); } diff --git a/khelo/lib/ui/flow/tournament/match_selection/match_scheduler.dart b/khelo/lib/ui/flow/tournament/match_selection/match_scheduler.dart index 6b5702d6..a81541d8 100644 --- a/khelo/lib/ui/flow/tournament/match_selection/match_scheduler.dart +++ b/khelo/lib/ui/flow/tournament/match_selection/match_scheduler.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:math'; import 'package:collection/collection.dart'; import 'package:data/api/match/match_model.dart'; @@ -62,10 +61,8 @@ class MatchScheduler { return scheduleBoxLeagueMatches(); case TournamentType.doubleOut: return scheduleDoubleOutMatches(); - case TournamentType.superOver: - return scheduleSuperOverMatches(); - case TournamentType.bestOf: - return scheduleBestOfMatches(); + case TournamentType.bestOfThree: + return scheduleBestOfThreeMatches(); case TournamentType.custom: return scheduledMatches; } @@ -343,26 +340,49 @@ class MatchScheduler { return additionalScheduledMatches; } - GroupedMatchMap scheduleSuperOverMatches() { - return {}; - } + GroupedMatchMap scheduleBestOfThreeMatches() { + final GroupedMatchMap additionalScheduledMatches = + Map.from(scheduledMatches); + final List teamPool = List.of(teams); - GroupedMatchMap scheduleBestOfMatches() { - return {}; - } + var currentGroup = MatchGroup.round; + var currentRound = 1; - // Helper functions - int calculateMatchesPerTeam(int teamCount) { - if (teamCount <= 1) { - throw ArgumentError("Team count must be greater than 1."); - } + while (teamPool.length > 1) { + final group = + additionalScheduledMatches.putIfAbsent(currentGroup, () => {1: []}); + final matches = group[currentRound] ?? []; + var expectedQualifiers = 0; + + if (currentGroup == MatchGroup.round) { + expectedQualifiers = handleSingleBestOfThreePhase( + matches, teamPool, currentGroup, currentRound); + } else { + expectedQualifiers = handleSingleKnockoutPhase( + matches, + teamPool, + currentGroup, + currentRound, + ); + } - const double maxPercentage = 0.75; - int matchesPerTeam = - (teamCount * maxPercentage / log(teamCount + 1)).round(); - matchesPerTeam = max(1, matchesPerTeam); + group[currentRound] = matches; + + if (expectedQualifiers > 8) { + currentRound++; + } else if (expectedQualifiers > 4) { + currentRound = 1; + currentGroup = MatchGroup.quarterfinal; + } else if (expectedQualifiers > 2) { + currentRound = 1; + currentGroup = MatchGroup.semifinal; + } else if (expectedQualifiers == 2) { + currentRound = 1; + currentGroup = MatchGroup.finals; + } + } - return matchesPerTeam; + return additionalScheduledMatches; } List calculateTeamPoints( @@ -608,4 +628,139 @@ class MatchScheduler { } return teamToReturn; } + + int handleSingleKnockoutPhase( + List matches, + List teamPool, + MatchGroup group, + int number, + ) { + removeAlreadyScheduledTeams(matches, teamPool); + + final teamPairs = createKnockoutTeamPairs(teamPool); + addMatches(matches, teamPairs, group, number); + + teamPool.removeWhere((team) => teamPairs + .any((element) => element.length == 2 && element.contains(team))); + + addWinnerTeamsBackToTeam(matches, teamPool); + + return matches.length; + } + + int handleSingleBestOfThreePhase( + List matches, List teamPool, group, number) { + final existingPairs = getExistingTeamPairs(matches); + final remainedTeams = teamPool + .where((team) => !existingPairs.expand((pair) => pair).contains(team)) + .toList(); + + final newPairs = createKnockoutTeamPairs(remainedTeams); + final allPairs = [...existingPairs, ...newPairs]; + + final winners = + handleBestOfThreeMatchesAndItsWinner(matches, allPairs, group, number); + + teamPool.removeWhere((team) => allPairs.any((pair) => + pair.length == 2 && pair.contains(team) && !winners.contains(team.id))); + return allPairs.length; + } + + List> getExistingTeamPairs(List matches) { + final List> pairs = []; + for (var match in matches) { + final pair = match.teams.map((e) => e.team).toList(); + if (!pairs.map((e) => e.map((e) => e.id)).any((element) => + element.contains(pair.first.id) && element.contains(pair.last.id))) { + pairs.add(pair); + } + } + + return pairs; + } + + List handleBestOfThreeMatchesAndItsWinner( + List groupMatches, + List> pair, + MatchGroup group, + int number, + ) { + final List winnerTeams = []; + for (var p in pair) { + final matchesForPair = groupMatches + .where( + (element) => + element.team_ids.contains(p.first.id) && + element.team_ids.contains(p.last.id), + ) + .toList(); + if (matchesForPair.isEmpty) { + addMatches(groupMatches, [p], group, number); + addMatches(groupMatches, [p], group, number); + } else if (matchesForPair.length == 1) { + addMatches(groupMatches, [p], group, number); + } else { + final finishedMatches = + matchesForPair.where((element) => element.matchResult != null); + if (finishedMatches.length >= 2) { + final winner = getWinnerTeam( + matchesForPair.toList(), + p.first.id, + p.last.id, + ); + if (winner == null && matchesForPair.length == 2) { + addMatches(groupMatches, [p], group, number); + } else if (winner != null) { + winnerTeams.add(winner); + } + } + } + } + return winnerTeams; + } + + String? getWinnerTeam( + List matches, + String teamOne, + String teamTwo, + ) { + Map winner = {teamOne: 0, teamTwo: 0}; + for (var element in matches) { + final result = element.matchResult; + if (result == null) continue; + + if (result.winType != WinnerByType.tie && + winner.containsKey(result.teamId)) { + winner[result.teamId] = winner[result.teamId]! + 1; + } else if (result.winType == WinnerByType.tie) { + final runRateOne = element.getRunRate(teamOne); + final runRateTwo = element.getRunRate(teamTwo); + String? win; + if (runRateOne != runRateTwo) { + win = (runRateOne > runRateTwo ? teamOne : teamTwo); + } else { + final wicketsOne = element.teams + .firstWhere((team) => team.team.id == teamOne) + .wicket; + final wicketsTwo = element.teams + .firstWhere((team) => team.team.id == teamTwo) + .wicket; + win = (wicketsOne > wicketsTwo ? teamOne : teamTwo); + } + if (winner.containsKey(win)) { + winner[win] = winner[win]! + 1; + } + } + } + final maxWin = winner.values.max; + + final maxWinners = + winner.entries.where((element) => element.value == maxWin); + + if (maxWinners.length == 1) { + return maxWinners.first.key; + } else { + return null; + } + } }