diff --git a/app/models/match.rb b/app/models/match.rb index 21d0a6f..778fd4b 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -39,6 +39,16 @@ class Match < ApplicationRecord finished? ? current_leading_team : nil end + def loser + return nil unless finished? + return nil if single_team? + + winning_team = winner + return nil if winning_team.nil? + + teams.find { |team| team != winning_team } + end + def group_match? group.present? end diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 6cef460..20def6b 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -16,7 +16,7 @@ class PlayoffStageService initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? 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 = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3) playoffs.concat empty_stages playoffs end @@ -25,10 +25,12 @@ class PlayoffStageService # # @param stage_count [Integer] number of stages to generate # @return [Array] the generated stages - def generate_stages_with_empty_matches(stage_count) + def generate_stages_with_empty_matches(stage_count, include_third_place_match = false) empty_stages = [] stage_count.times do |i| - stage = Stage.new level: i, matches: generate_empty_matches(2**i) + match_count = 2**i + match_count += 1 if include_third_place_match && i.zero? + stage = Stage.new level: i, matches: generate_empty_matches(match_count) empty_stages << stage end # as we are generating the stages in the wrong order (starting with the lowest number of matches (which is @@ -76,33 +78,14 @@ class PlayoffStageService # 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) + companion_match = find_companion_match(current_match.position, current_stage) + return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage) 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.state = if match_below.match_scores.empty? || match_below.match_scores.size == 1 - :not_ready - elsif match_below.match_scores.size == 2 - :not_started - else - raise 'Unprocessable amount of match_scores found' - end - [match_below, match_scores].flatten + populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners) end private @@ -135,9 +118,43 @@ class PlayoffStageService match_scores end + def populate_match!(match:, teams:) + match_scores = match.match_scores.sort_by(&:id) + match_scores = assign_correct_match_scores!(match_scores, teams) + match_scores = match_scores.select { |ms| ms.team.present? } + match.match_scores = match_scores + match.state = if match.match_scores.empty? || match.match_scores.size == 1 + :not_ready + elsif match.match_scores.size == 2 + :not_started + else + raise 'Unprocessable amount of match_scores found' + end + [match, match_scores].flatten + end + def get_winners_of(companion_match, current_match) matches = [current_match, companion_match].sort_by(&:position) matches.map(&:winner) end + + def get_losers_of(companion_match, current_match) + matches = [current_match, companion_match].sort_by(&:position) + matches.map(&:loser) + end + + def semifinal_match?(current_stage, next_stage) + current_stage.level == 1 && next_stage.level.zero? && next_stage.matches.size > 1 + end + + def populate_final_stage_matches(current_match, companion_match, next_stage) + final_match = next_stage.matches.find { |match| match.position.zero? } + third_place_match = next_stage.matches.find { |match| match.position == 1 } + + [ + populate_match!(match: final_match, teams: get_winners_of(companion_match, current_match)), + populate_match!(match: third_place_match, teams: get_losers_of(companion_match, current_match)) + ].flatten + end end end diff --git a/e2e/lib/scenario_runner.rb b/e2e/lib/scenario_runner.rb index 5a2e6b5..d6d3b24 100644 --- a/e2e/lib/scenario_runner.rb +++ b/e2e/lib/scenario_runner.rb @@ -64,8 +64,7 @@ module TurniereE2E checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt - final_match = final_match_of!(tournament) - finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match)) + finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { finish_attempt: finish_attempt, @@ -97,8 +96,7 @@ module TurniereE2E checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt - final_match = final_match_of!(tournament) - finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match)) + finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { finish_attempt: finish_attempt, @@ -340,9 +338,24 @@ module TurniereE2E tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } || raise('group stage not found') end - def final_match_of!(tournament) + def final_stage_matches_of!(tournament) final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found') - attach_stage_level(final_stage).fetch(:matches).first + attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) } + end + + def final_match_of!(tournament) + final_stage_matches_of!(tournament).first + end + + def finish_final_stage_matches!(tournament) + final_stage_matches_of!(tournament).each do |match| + play_match!(match, winner_name: preferred_playoff_winner_name(match)) + end + + { + status: 200, + finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) } + } end def playoff_matches(tournament) diff --git a/spec/e2e/http/tournament_lifecycle_spec.rb b/spec/e2e/http/tournament_lifecycle_spec.rb index 04edcb1..36f8ee2 100644 --- a/spec/e2e/http/tournament_lifecycle_spec.rb +++ b/spec/e2e/http/tournament_lifecycle_spec.rb @@ -37,7 +37,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) expect(final_attempt[:status]).to eq(200) expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries) - .find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished']) + .find { |stage| stage[:level] == 0 }[:match_states]).to eq(%w[finished finished]) end it 'drives the 3-group intermediate-round path over HTTP through a finished final' do @@ -58,7 +58,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) expect(final_attempt[:status]).to eq(200) expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries) - .find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished']) + .find { |stage| stage[:level] == 0 }[:match_states]).to eq(%w[finished finished]) end { @@ -126,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do completed_stages = result.dig(:checkpoints, :completed, :stage_summaries) final_stage = completed_stages.find { |stage| stage[:level] == 0 } - expect(final_stage[:match_states]).to eq(['finished']) + expect(final_stage[:match_states]).to eq(team_count > 3 ? %w[finished finished] : ['finished']) end end end diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 721b7d1..351e5fa 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -75,6 +75,13 @@ RSpec.describe PlayoffStageService do expect(stage.level).to eq stage_level end end + + it "generates correct amount of final-stage matches for #{parameters[:team_size]} teams" do + teams = build_list(:team, parameters[:team_size]) + stages = PlayoffStageService.generate_playoff_stages(teams, false) + + expect(stages.find { |stage| stage.level.zero? }.matches.size).to eq(parameters[:team_size] > 3 ? 2 : 1) + end end describe 'number of teams isn\'t a power of two' do @@ -229,5 +236,42 @@ RSpec.describe PlayoffStageService do expect(PlayoffStageService.populate_match_below(@final_match)).to eq([]) end end + + context 'when semifinal finishes in tournament with third-place match' do + before do + @tournament = create(:stageless_tournament, teams_count: 4) + @tournament.stages = PlayoffStageService.generate_playoff_stages(@tournament.teams, false) + @tournament.save! + semifinal_stage = @tournament.stages.find { |stage| stage.level == 1 } + @semifinal_match = semifinal_stage.matches.find { |match| match.position.zero? } + @companion_match = semifinal_stage.matches.find { |match| match.position == 1 } + + [@semifinal_match, @companion_match].each do |match| + match.update!(state: :finished) + match.match_scores.sort_by(&:id).each_with_index do |match_score, index| + match_score.update!(points: index.zero? ? 2 : 1) + end + end + + @result = PlayoffStageService.populate_match_below(@semifinal_match) + @final_stage = @tournament.stages.find { |stage| stage.level.zero? } + @final_match = @final_stage.matches.find { |match| match.position.zero? } + @third_place_match = @final_stage.matches.find { |match| match.position == 1 } + end + + it 'fills final with winners' do + expect(@final_match.teams).to match_array([@semifinal_match.winner, @companion_match.winner]) + expect(@final_match.state).to eq('not_started') + end + + it 'fills third-place match with losers' do + expect(@third_place_match.teams).to match_array([@semifinal_match.loser, @companion_match.loser]) + expect(@third_place_match.state).to eq('not_started') + end + + it 'returns both changed matches' do + expect(@result).to include(@final_match, @third_place_match) + end + end end end diff --git a/spec/services/tournament_snapshot_importer_spec.rb b/spec/services/tournament_snapshot_importer_spec.rb index 4788941..0206814 100644 --- a/spec/services/tournament_snapshot_importer_spec.rb +++ b/spec/services/tournament_snapshot_importer_spec.rb @@ -56,7 +56,7 @@ RSpec.describe TournamentSnapshotImporter do expect(sync_signature(follower)).to eq(sync_signature(source)) expect(follower.teams.count).to eq(128) expect(follower.stages.where(level: -1).first.groups.count).to eq(32) - expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(63) + expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(64) expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count) expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id)) expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))