Merge branch 'codex/TUR-45-third-place-match' into 'master'
TUR-45 add third-place playoff match Closes TUR-45 See merge request turniere/turniere-backend!47
This commit is contained in:
commit
201abe5145
|
|
@ -39,6 +39,13 @@ class Match < ApplicationRecord
|
||||||
finished? ? current_leading_team : nil
|
finished? ? current_leading_team : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def loser
|
||||||
|
winning_team = winner
|
||||||
|
return nil if winning_team.nil?
|
||||||
|
|
||||||
|
teams.find { |team| team != winning_team }
|
||||||
|
end
|
||||||
|
|
||||||
def group_match?
|
def group_match?
|
||||||
group.present?
|
group.present?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class PlayoffStageService
|
||||||
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
||||||
playoffs << initial_stage
|
playoffs << initial_stage
|
||||||
# empty stages are the stages, the tournament is filled with to have the matches ready for later
|
# 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.concat empty_stages
|
||||||
playoffs
|
playoffs
|
||||||
end
|
end
|
||||||
|
|
@ -25,10 +25,12 @@ class PlayoffStageService
|
||||||
#
|
#
|
||||||
# @param stage_count [Integer] number of stages to generate
|
# @param stage_count [Integer] number of stages to generate
|
||||||
# @return [Array] the generated stages
|
# @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 = []
|
empty_stages = []
|
||||||
stage_count.times do |i|
|
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
|
empty_stages << stage
|
||||||
end
|
end
|
||||||
# as we are generating the stages in the wrong order (starting with the lowest number of matches (which is
|
# 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 does not exist (there are no matches after the finale)
|
||||||
return [] if next_stage.nil?
|
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
|
# 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
|
# 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
|
# 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)
|
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)
|
||||||
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)
|
winners = get_winners_of(companion_match, current_match)
|
||||||
|
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
||||||
# 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -135,9 +118,43 @@ class PlayoffStageService
|
||||||
match_scores
|
match_scores
|
||||||
end
|
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)
|
def get_winners_of(companion_match, current_match)
|
||||||
matches = [current_match, companion_match].sort_by(&:position)
|
matches = [current_match, companion_match].sort_by(&:position)
|
||||||
matches.map(&:winner)
|
matches.map(&:winner)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ module TurniereE2E
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
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
|
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 = finish_final_stage_matches!(tournament)
|
||||||
finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match))
|
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:final_finish_attempted] = {
|
checkpoints[:final_finish_attempted] = {
|
||||||
finish_attempt: finish_attempt,
|
finish_attempt: finish_attempt,
|
||||||
|
|
@ -97,8 +96,7 @@ module TurniereE2E
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
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
|
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 = finish_final_stage_matches!(tournament)
|
||||||
finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match))
|
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:final_finish_attempted] = {
|
checkpoints[:final_finish_attempted] = {
|
||||||
finish_attempt: finish_attempt,
|
finish_attempt: finish_attempt,
|
||||||
|
|
@ -340,9 +338,24 @@ module TurniereE2E
|
||||||
tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } || raise('group stage not found')
|
tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } || raise('group stage not found')
|
||||||
end
|
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')
|
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
|
end
|
||||||
|
|
||||||
def playoff_matches(tournament)
|
def playoff_matches(tournament)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
||||||
expect(final_attempt[:status]).to eq(200)
|
expect(final_attempt[:status]).to eq(200)
|
||||||
expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries)
|
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
|
end
|
||||||
|
|
||||||
it 'drives the 3-group intermediate-round path over HTTP through a finished final' do
|
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)
|
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
||||||
expect(final_attempt[:status]).to eq(200)
|
expect(final_attempt[:status]).to eq(200)
|
||||||
expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries)
|
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
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -126,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
|
|
||||||
completed_stages = result.dig(:checkpoints, :completed, :stage_summaries)
|
completed_stages = result.dig(:checkpoints, :completed, :stage_summaries)
|
||||||
final_stage = completed_stages.find { |stage| stage[:level] == 0 }
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,13 @@ RSpec.describe PlayoffStageService do
|
||||||
expect(stage.level).to eq stage_level
|
expect(stage.level).to eq stage_level
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe 'number of teams isn\'t a power of two' do
|
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([])
|
expect(PlayoffStageService.populate_match_below(@final_match)).to eq([])
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
expect(sync_signature(follower)).to eq(sync_signature(source))
|
expect(sync_signature(follower)).to eq(sync_signature(source))
|
||||||
expect(follower.teams.count).to eq(128)
|
expect(follower.teams.count).to eq(128)
|
||||||
expect(follower.stages.where(level: -1).first.groups.count).to eq(32)
|
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.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(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))
|
expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue