public static void SaveRounds(IDbConnection conn, GroupCoords coords, CalendarResult result) { var transaction = conn.BeginTransaction(); try { var dbGroup = conn.Get <StageGroup>(coords.IdGroup); if (dbGroup == null) { throw new Exception("Error.NotFound"); } if ((dbGroup.Flags & (int)StageGroupFlags.HasGeneratedCalendar) > 0) { throw new Exception("Tournament.AlreadyHasCalendar"); } foreach (var day in result.Days) { InsertDay(conn, transaction, day); } dbGroup.Flags = dbGroup.Flags | (int)StageGroupFlags.HasGeneratedCalendar; conn.Update(dbGroup, transaction); transaction.Commit(); } catch (System.Exception) { transaction.Rollback(); throw; } }
public static void AssignSpecificPreferencesToSlots(CalendarResult result, List <Match> matchesInRound, IList <CalendarSlot> slots, IEnumerable <TeamLocationPreference> prefs, string roundName) { // Locate slots for those prefs, remove slots and assign them to matches foreach (var pref in prefs) { // Locate slot for that preference var slot = GetSlotForPreference(pref, slots); if (slot == null) { result.AddMessage(OutputLineType.Warning, "{1}: El horario preferido del equipo {0} no está en los rangos de horas especificados, o el campo preferido no está en la lista de seleccionados. Ignorando la preferencia del equipo.", pref.IdTeam, roundName); continue; } // Assign the slot to the match (find the match for this team) and remove the slot in slots. var match = GetMatchForHomeTeamId(matchesInRound, pref.IdTeam); if (match == null) { result.AddMessage(OutputLineType.Error, "{1}: Hay una preferencia para el equipo {0} pero no está en la lista de equipos. Inconsistencia interna.", pref.IdTeam, roundName); } else { // Assign slot to match and remove from the list of available slots slot.AssignToMatch(match); slots.Remove(slot); } } }
public static CalendarResult Calculate(CalendarGenInput input, IList <Field> fields, string locale, TeamLocationPreferenceProviderDelegate preferenceProvider, IDbConnection c) { Assert.IsNotNull(input); Assert.IsNotNull(input.TeamIds); Assert.IsNotNull(input.WeekdaySlots); Assert.IsNotNull(input.FieldIds); var result = new CalendarResult(); var teams = input.TeamIds; bool noTeamsDefined = teams.Length == 0; int numberOfTeams = 0; if (noTeamsDefined) // No teams defined yet. { long idGroup = input.Group.IdGroup; numberOfTeams = c.QueryFirst <int>($"SELECT numteams FROM stageGroups WHERE id = {idGroup};"); } else { numberOfTeams = teams.Length; } if (!IsPowerOfTwo(numberOfTeams)) { throw new PlannerException("Error.NotPowerOfTwo"); } // Should also check that teams are actually valid (!= -1) var matchRounds = CreateRounds(teams, input.Group, numberOfTeams); string roundNameCallback(int index) => GetRoundName(index, matchRounds.Count, locale); var numSlotsNeededPerRound = numberOfTeams / 2; //var numFields = input.FieldIds.Length; //var availableTimeSlots = PlannerScheduler.GetTimeSlots(input.WeekdaySlots, input.GameDuration); //var availableSlots = availableTimeSlots * numFields; //if (availableSlots < numSlotsNeededPerRound) throw new PlannerException("Error.Calendar.NotEnoughHours"); var teamPrefs = noTeamsDefined ? null : preferenceProvider?.Invoke(input.TeamIds); PlannerScheduler.SpreadMatchesInCalendar( result, matchRounds, input.WeekdaySlots, input.StartDate, input.FieldIds, numSlotsNeededPerRound, input.GameDuration, false, input.Group, input.ForbiddenDays, teamPrefs, roundNameCallback); return(result); }
public static PlayDay CreateAndFillRound(CalendarResult result, List <Match> matchesInRound, IList <CalendarSlot> slots, GroupCoords coords, IEnumerable <TeamLocationPreference> fieldPreferences, string roundName) { // Round Id -1: when saving to the database, will have to set proper ID here and to the matches. var day = new PlayDay { Id = -1, Matches = new List <Match>() }; // Spread fieldPreferences in the slots (consuming them). Then fill the rest of the matches. AssignPreferencesToSlots(result, matchesInRound, slots, fieldPreferences, roundName); foreach (var match in matchesInRound) { match.IdTournament = coords.IdTournament; match.IdStage = coords.IdStage; match.IdGroup = coords.IdGroup; match.Status = (int)MatchStatus.Created; match.IdDay = day.Id; match.Status = (int)MatchStatus.Created; if (IsFillerMatch(match)) { // Add match, but do not consume slot match.Status = (int)MatchStatus.Skip; day.Matches.Add(match); continue; } // If the match doesn't already have time if (match.StartTime == default(DateTime)) { if (slots.Count == 0) { throw new Exception("Error.Calendar.NotEnoughHours"); } // Assign first available slot and remove from the list. var slot = slots[0]; slot.AssignToMatch(match); slots.RemoveAt(0); } day.Matches.Add(match); } day.SetDatesFromDatesList(); return(day); }
private static DateTime?GetSlotForTime(CalendarResult result, IList <DateTime> slots, int gameDuration, DateTime time, long idTeam) { foreach (var slot in slots) { var slotStartTime = GetTime(slot); var slotEndTime = slotStartTime.AddMinutes(gameDuration); if (slotStartTime >= time && time >= slotEndTime) { return(slot); } } result.Messages.Add(new OutputLine(OutputLineType.Warning, Localization.Get("El equipo {0} prefiere la hora {1} que no está en el rango de horas asignado. Ignorando la preferencia.", null, idTeam, time))); return(null); }
public static void AssignPreferencesToSlots(CalendarResult result, List <Match> matchesInRound, IList <CalendarSlot> slots, IEnumerable <TeamLocationPreference> teamPreferences, string roundName) { if (teamPreferences == null) { return; } var localTeamIds = GetLocalTeams(matchesInRound); // * Assign first LOCAL teams with field AND time preference var prefs = GetPreferencesForLocalTeams(GetFieldAndTimePreferences(teamPreferences), localTeamIds); AssignSpecificPreferencesToSlots(result, matchesInRound, slots, prefs, roundName); // * Now assign LOCAL teams with field preference to any available hour (time preference only is not allowed) prefs = GetPreferencesForLocalTeams(GetFieldPreferences(teamPreferences), localTeamIds); AssignSpecificPreferencesToSlots(result, matchesInRound, slots, prefs, roundName); }
public static void SpreadMatchesInCalendar( CalendarResult result, List <List <Match> > matchRounds, DailySlot[][] weekdaySlots, DateTime startDate, IList <long> fieldIds, int numSlots, int gameDuration, bool wantsRandom, GroupCoords coords, DateTime[] forbiddenDays, IEnumerable <TeamLocationPreference> fieldPreferences, Func <int, string> roundNameCallback) { var days = new List <PlayDay>(); //var fieldIds = GetFieldIds(fields); var roundNumber = 1; // First, spread the rounds in days in the calendar foreach (var matchRound in matchRounds) { startDate = GetUnforbiddenStartDate(startDate, weekdaySlots, forbiddenDays); var slots = GetRoundSlots(startDate, weekdaySlots, numSlots, gameDuration, fieldIds); var roundName = roundNameCallback(roundNumber); var round = CreateAndFillRound(result, matchRound, slots, coords, fieldPreferences, roundName); round.IdTournament = coords.IdTournament; round.IdStage = coords.IdStage; round.IdGroup = coords.IdGroup; round.Name = roundName; round.SequenceOrder = roundNumber; days.Add(round); startDate = startDate.AddDays(7); roundNumber++; } result.Days = days; }
public IActionResult GenerateTournamentCalendar([FromBody] CalendarGenInput input) { return(DbOperation(c => { if (input == null) { throw new NoDataException(); } if (!IsOrganizationAdmin()) { throw new UnauthorizedAccessException(); } CalendarResult result = null; var teams = GetTeams(c, null, input.TeamIds); switch ((CalendarType)input.Type) { case CalendarType.League: result = LeaguePlanner.Calculate(input, null, GetUserLocale(), teamIds => GetTeamPreferences(c, null, teamIds), idTeam => GetTeamName(teams, idTeam)); break; case CalendarType.Knockout: result = KnockoutPlanner.Calculate(input, null, GetUserLocale(), teamIds => GetTeamPreferences(c, null, teamIds), c); break; default: break; } if (!input.IsPreview) { CalendarStorer.SaveRounds(c, input.Group, result); } return result; })); }
/* * LEAGUE * * input * - list of teams (ids) * - start date * - weekdays available * - with range of hours * - days forbidden * - list of play fields * - with their availability * - game duration (minutes) * - is preview? * * output: * - a collection of days, each containing: * - date * - list of matches, each containing: * - teams * - hour * - play field * - status: scheduled * * Notes: * - First create a list of matches per day, then spread it in the available slots. * - From the number of teams, the algorithm can predict how many slots are needed in each day * (it's the same as the number of teams). This should be used in the UI to set up the * calendar parameters. * - To create the list of matches per day, start with the list of teams. * - In the UI, allow the organizer to order them any way the want (display a message for this). * - Then do every possible combination. If odd number of teams, one will be left out each round, in order. * - To spread the matches in slots, randomize the list and assign to slots. * - Have to consider the "home" setting of each team. Only one team should play at home, but then, * what if two have then same home? no matter what, they will have to play, and their home won't * change. * * Should this be implemented in the server instead? Having this not validated on the client implies an * organizer can mess the calendar. Is it possible that it can ultimately generate payments to users? * In the server this could be implemented as a simple API call that calculates the preview. An argument * can decide whether the generated result is just a preview or has to be commited to the DB. * Since the game field availability is a lot of data that has to be queried to the database, it's * benefitial to do it on the server side. * * Test environment for this. */ /// <summary> /// /// </summary> /// <param name="input"></param> /// <param name="fields">List of fields, including matches already scheduled to consider in the plan</param> /// <returns></returns> public static CalendarResult Calculate(CalendarGenInput input, IList <Field> fields, string locale, TeamLocationPreferenceProviderDelegate preferenceProvider, TeamNameProvider nameProvider) { //-First create a list of matches per round, then spread it in the available slots. // - From the number of teams, the algorithm can predict how many slots are needed in each round // (it's the same as the number of teams). This should be used in the UI to set up the // calendar parameters. // -To create the list of matches per round, start with the list of teams. // - In the UI, allow the organizer to order them any way the want(display a message for this). // - Then apply the round robin algorithm, preserving the first entry in the list. // - To spread the matches in slots, randomize each round list and assign to slots in sequence. // - Have to consider the "home" setting of each team. Only one team should play at home, but then, // what if two have the same home? no matter what, they will have to play, and their home won't // change. Assert.IsNotNull(input); Assert.IsNotNull(input.TeamIds); Assert.IsNotNull(input.WeekdaySlots); Assert.IsNotNull(input.FieldIds); var result = new CalendarResult(); if (input.TeamIds.Length <= 2) { throw new PlannerException("Error.Calendar.NotEnoughTeams"); } var teams = input.TeamIds; var oddNumTeams = teams.Length % 2 == 1; if (oddNumTeams) { // Add a "free day" team (id = -1) Array.Resize(ref teams, teams.Length + 1); teams[teams.Length - 1] = -1; } var numSlotsNeededPerRound = teams.Length / 2; //var numFields = input.FieldIds.Length; //var availableTimeSlots = PlannerScheduler.GetTimeSlots(input.WeekdaySlots, input.GameDuration); //var availableSlots = availableTimeSlots * numFields; //if (availableSlots < numSlotsNeededPerRound) throw new PlannerException("Error.Calendar.NotEnoughHours"); var matchRounds = CreateRoundRobinMatches(teams, input.NumRounds); string roundNameCallback(int index) => Localization.Get("Jornada {0}", locale, index); var locationPreferences = preferenceProvider?.Invoke(input.TeamIds); // Now assign matches to days. Consider field availability. PlannerScheduler.SpreadMatchesInCalendar( result, matchRounds, input.WeekdaySlots, input.StartDate, input.FieldIds, numSlotsNeededPerRound, input.GameDuration, input.RandomizeFields, input.Group, input.ForbiddenDays, locationPreferences, roundNameCallback); return(result); }