Skip to content

Commit

Permalink
Best of match scheduling (#136)
Browse files Browse the repository at this point in the history
* BestOf match scheduling

* minor changes

* Refactor bestOf logic

* Minor changes

* Minor changes

* Update best of logic

* Minor change

* Minor changes

* add hybrid and full bestOf approach

* remove unused code

---------

Co-authored-by: sidhdhi canopas <[email protected]>
  • Loading branch information
cp-mayank and cp-sidhdhi-p authored Nov 21, 2024
1 parent f33b400 commit 2cf8890
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 37 deletions.
18 changes: 18 additions & 0 deletions data/lib/api/match/match_model.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions data/lib/api/tournament/tournament_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions data/lib/api/tournament/tournament_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions khelo/assets/locales/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 4 additions & 8 deletions khelo/lib/domain/extensions/enum_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down
197 changes: 176 additions & 21 deletions khelo/lib/ui/flow/tournament/match_selection/match_scheduler.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:collection';
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:data/api/match/match_model.dart';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -343,26 +340,49 @@ class MatchScheduler {
return additionalScheduledMatches;
}

GroupedMatchMap scheduleSuperOverMatches() {
return {};
}
GroupedMatchMap scheduleBestOfThreeMatches() {
final GroupedMatchMap additionalScheduledMatches =
Map.from(scheduledMatches);
final List<TeamModel> 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<TeamPoints> calculateTeamPoints(
Expand Down Expand Up @@ -608,4 +628,139 @@ class MatchScheduler {
}
return teamToReturn;
}

int handleSingleKnockoutPhase(
List<MatchModel> matches,
List<TeamModel> 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<MatchModel> matches, List<TeamModel> 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<List<TeamModel>> getExistingTeamPairs(List<MatchModel> matches) {
final List<List<TeamModel>> 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<String> handleBestOfThreeMatchesAndItsWinner(
List<MatchModel> groupMatches,
List<List<TeamModel>> pair,
MatchGroup group,
int number,
) {
final List<String> 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<MatchModel> matches,
String teamOne,
String teamTwo,
) {
Map<String, int> 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;
}
}
}

0 comments on commit 2cf8890

Please sign in to comment.