diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 60d236b..080df96 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -1,109 +1,109 @@ # frozen_string_literal: true class PlayoffStageService - # Generates the playoff stage given the tournament - # - # @param teams [Array] The teams to generate the playoff stages with - # @return [Array] the generated playoff stages - def self.generate_playoff_stages(teams) - playoffs = [] - stage_count = calculate_required_stage_count(teams.size) - # initial_matches are the matches in the first stage; this is the only stage filled with teams from the start on - initial_matches = MatchService.generate_matches(teams) - initial_stage = Stage.new level: stage_count - 1, matches: initial_matches - playoffs << initial_stage - # empty stages are the stages, the tournament is filled with to have the matches ready for later - empty_stages = generate_stages_with_empty_matches(stage_count - 1) - empty_stages.each do |stage| - playoffs << stage - end - playoffs - end - - # Generates the playoff stage given the tournament - # - # @param tournament [Tournament] The tournament to generate the playoff stages from - # @return [Array] the generated playoff stages - def self.generate_playoff_stages_from_tournament(tournament) - generate_playoff_stages tournament.teams - end - - # Generates given number of empty stages - # - # @param stage_count [Integer] number of stages to generate - # @return [Array] the generated stages - def self.generate_stages_with_empty_matches(stage_count) - empty_stages = [] - stage_count.times do |i| - stage = Stage.new level: i, matches: generate_empty_matches(2**i) - empty_stages << stage - end - # as we are generating the stages in the wrong order (starting with the lowest number of matches (which is - # the final stage)) they need to be reversed - empty_stages.reverse! - end - - # Generates a number of empty matches to fill later stages - # - # @param amount [Integer] the amount of matches to generate - # @return [Array] the generated matches - def self.generate_empty_matches(amount) - matches = [] - amount.times do |i| - match = Match.new state: :not_ready, position: i - matches << match - end - matches - end - - # Calculates how many stages are required for given number of teams - # - # @param number_of_teams [Integer] the teams number of teams to calculate amount of stages - # @return [Integer] amount of required stages - def self.calculate_required_stage_count(number_of_teams) - if number_of_teams == 1 - 1 - else - # black voodoo magic - stage_count = Math.log(Utils.next_power_of_two(number_of_teams)) / Math.log(2) - stage_count -= 1 if Utils.po2?(number_of_teams) - stage_count.to_int - end - end - - # Populates the match below given match with the winners of the matches above - # - # @param current_match [Match] The Match which finished, the match below it gets populated - # @return [Array] the objects that changed and need to be saved - def self.populate_match_below(current_match) - current_stage = current_match.stage - next_stage = current_stage.tournament.stages.find { |s| s.level == current_stage.level - 1 } - # return if next stage does not exist (there are no matches after the finale) - return if next_stage.nil? - - current_position = current_match.position - - # a "companion" match is the one that with the selected match makes up the two matches of which the winners advance - # into the match below - # depending on the position of the match, the companion match is either on the left or right of it - companion_match = find_companion_match(current_position, current_stage) - - match_below = next_stage.matches.find { |m| m.position == current_position / 2 } - match_scores = match_below.match_scores.sort_by(&:id) - - winners = get_winners_of(companion_match, current_match) - - # depending on the amount of match_scores already present we need to do different things - match_scores = assign_correct_match_scores!(match_scores, winners) - - # If a match is not decided yet, it will return nil as winner. - # This is not allowed in Database. The following code filters out MatchScores that contain nil as team. - match_scores = match_scores.select { |ms| ms.team.present? } - match_below.match_scores = match_scores - [match_below, match_scores].flatten - end - class << self + # Generates the playoff stage given the tournament + # + # @param teams [Array] The teams to generate the playoff stages with + # @return [Array] the generated playoff stages + def generate_playoff_stages(teams) + playoffs = [] + stage_count = calculate_required_stage_count(teams.size) + # initial_matches are the matches in the first stage; this is the only stage filled with teams from the start on + initial_matches = MatchService.generate_matches(teams) + initial_stage = Stage.new level: stage_count - 1, matches: initial_matches + playoffs << initial_stage + # empty stages are the stages, the tournament is filled with to have the matches ready for later + empty_stages = generate_stages_with_empty_matches(stage_count - 1) + empty_stages.each do |stage| + playoffs << stage + end + playoffs + end + + # Generates the playoff stage given the tournament + # + # @param tournament [Tournament] The tournament to generate the playoff stages from + # @return [Array] the generated playoff stages + def generate_playoff_stages_from_tournament(tournament) + generate_playoff_stages tournament.teams + end + + # Generates given number of empty stages + # + # @param stage_count [Integer] number of stages to generate + # @return [Array] the generated stages + def generate_stages_with_empty_matches(stage_count) + empty_stages = [] + stage_count.times do |i| + stage = Stage.new level: i, matches: generate_empty_matches(2**i) + empty_stages << stage + end + # as we are generating the stages in the wrong order (starting with the lowest number of matches (which is + # the final stage)) they need to be reversed + empty_stages.reverse! + end + + # Generates a number of empty matches to fill later stages + # + # @param amount [Integer] the amount of matches to generate + # @return [Array] the generated matches + def generate_empty_matches(amount) + matches = [] + amount.times do |i| + match = Match.new state: :not_ready, position: i + matches << match + end + matches + end + + # Calculates how many stages are required for given number of teams + # + # @param number_of_teams [Integer] the teams number of teams to calculate amount of stages + # @return [Integer] amount of required stages + def calculate_required_stage_count(number_of_teams) + if number_of_teams == 1 + 1 + else + # black voodoo magic + stage_count = Math.log(Utils.next_power_of_two(number_of_teams)) / Math.log(2) + stage_count -= 1 if Utils.po2?(number_of_teams) + stage_count.to_int + end + end + + # Populates the match below given match with the winners of the matches above + # + # @param current_match [Match] The Match which finished, the match below it gets populated + # @return [Array] the objects that changed and need to be saved + def populate_match_below(current_match) + current_stage = current_match.stage + next_stage = current_stage.tournament.stages.find { |s| s.level == current_stage.level - 1 } + # return if next stage does not exist (there are no matches after the finale) + return if next_stage.nil? + + current_position = current_match.position + + # a "companion" match is the one that with the selected match makes up the two matches + # of which the winners advance into the match below + # depending on the position of the match, the companion match is either on the left or right of it + companion_match = find_companion_match(current_position, current_stage) + + match_below = next_stage.matches.find { |m| m.position == current_position / 2 } + match_scores = match_below.match_scores.sort_by(&:id) + + winners = get_winners_of(companion_match, current_match) + + # depending on the amount of match_scores already present we need to do different things + match_scores = assign_correct_match_scores!(match_scores, winners) + + # If a match is not decided yet, it will return nil as winner. + # This is not allowed in Database. The following code filters out MatchScores that contain nil as team. + match_scores = match_scores.select { |ms| ms.team.present? } + match_below.match_scores = match_scores + [match_below, match_scores].flatten + end + private def find_companion_match(current_position, current_stage)