diff --git a/app/interactors/add_group_stage_to_tournament.rb b/app/interactors/add_group_stage_to_tournament.rb index 23e81de..e9866c2 100644 --- a/app/interactors/add_group_stage_to_tournament.rb +++ b/app/interactors/add_group_stage_to_tournament.rb @@ -13,7 +13,7 @@ class AddGroupStageToTournament tournament.instant_finalists_amount, tournament.intermediate_round_participants_amount = TournamentService.calculate_default_amount_of_teams_advancing(tournament.playoff_teams_amount, group_stage.groups.size) - context.object_to_save = tournament + (context.object_to_save ||= []) << tournament rescue StandardError context.fail! end diff --git a/app/interactors/add_playoffs_to_tournament.rb b/app/interactors/add_playoffs_to_tournament.rb index 9efc2ec..ce530cb 100644 --- a/app/interactors/add_playoffs_to_tournament.rb +++ b/app/interactors/add_playoffs_to_tournament.rb @@ -12,7 +12,8 @@ class AddPlayoffsToTournament else tournament.stages.concat playoff_stages end - context.object_to_save = tournament + context.intermediate_stage = tournament.stages.find(&:intermediate_stage?) + (context.object_to_save ||= []) << tournament else context.fail! end diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb new file mode 100644 index 0000000..1eb1bd3 --- /dev/null +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AdvanceTeamsInIntermediateStage + include Interactor + + def call + intermediate_stage = context.intermediate_stage + return if intermediate_stage.nil? + + intermediate_stage.matches.select { |m| m.state == 'single_team' } + .each do |match| + context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? + end + (context.object_to_save ||= []) << intermediate_stage + end +end diff --git a/app/interactors/populate_match_below.rb b/app/interactors/populate_match_below.rb index 96a58c3..e224639 100644 --- a/app/interactors/populate_match_below.rb +++ b/app/interactors/populate_match_below.rb @@ -7,7 +7,7 @@ class PopulateMatchBelow match = context.match begin objects_to_save = PlayoffStageService.populate_match_below(match) - context.object_to_save = objects_to_save + (context.object_to_save ||= []) << objects_to_save rescue StandardError context.fail! end diff --git a/app/interactors/update_groups_group_scores.rb b/app/interactors/update_groups_group_scores.rb index 3ea49e2..fbb05fc 100644 --- a/app/interactors/update_groups_group_scores.rb +++ b/app/interactors/update_groups_group_scores.rb @@ -4,7 +4,7 @@ class UpdateGroupsGroupScores include Interactor def call - context.object_to_save = GroupStageService.update_group_scores(context.group) + (context.object_to_save ||= []) << GroupStageService.update_group_scores(context.group) rescue StandardError context.fail! end diff --git a/app/models/match.rb b/app/models/match.rb index 7891276..6e15b10 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -28,6 +28,8 @@ class Match < ApplicationRecord end def winner + return teams.first if single_team? + finished? ? current_leading_team : nil end diff --git a/app/models/stage.rb b/app/models/stage.rb index 1f36b21..f29d661 100644 --- a/app/models/stage.rb +++ b/app/models/stage.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Stage < ApplicationRecord - enum state: %i[playoff_stage in_progress finished] + enum state: %i[playoff_stage intermediate_stage in_progress finished] belongs_to :tournament has_many :matches, dependent: :destroy diff --git a/app/organizers/add_playoffs_to_tournament_and_save.rb b/app/organizers/add_playoffs_to_tournament_and_save.rb index 766add6..e2e5c49 100644 --- a/app/organizers/add_playoffs_to_tournament_and_save.rb +++ b/app/organizers/add_playoffs_to_tournament_and_save.rb @@ -3,5 +3,5 @@ class AddPlayoffsToTournamentAndSave include Interactor::Organizer - organize AddPlayoffsToTournament, SaveApplicationRecordObject + organize AddPlayoffsToTournament, AdvanceTeamsInIntermediateStage, SaveApplicationRecordObject end diff --git a/app/services/match_service.rb b/app/services/match_service.rb index adf2995..9d3cb50 100644 --- a/app/services/match_service.rb +++ b/app/services/match_service.rb @@ -45,12 +45,14 @@ class MatchService end # the start point is to compensate for all the teams that are already within a "normal" match - startpoint = matches.size + i = team_offset = matches.size until matches.size >= needed_games # while we do not have enough matches in general we need to fill the array with "single team" matches - i = matches.size + startpoint - match = Match.new state: :single_team, position: i, match_scores: [MatchScore.create(team: teams[i])] + match = Match.new state: :single_team, + position: i, + match_scores: [MatchScore.create(team: teams[i + team_offset])] matches << match + i += 1 end matches end diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 38ccabc..3ee2e1f 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -12,12 +12,11 @@ class PlayoffStageService # 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 + 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.each do |stage| - playoffs << stage - end + playoffs.concat empty_stages playoffs end diff --git a/spec/factories/matches.rb b/spec/factories/matches.rb index 5c8a42e..cd0543f 100644 --- a/spec/factories/matches.rb +++ b/spec/factories/matches.rb @@ -14,6 +14,13 @@ FactoryBot.define do state { :in_progress } end + factory :single_team_match do + state { :single_team } + after(:create) do |match| + match.match_scores = [create(:match_score, points: 0)] + end + end + factory :empty_prepared_playoff_match do state { :not_ready } end diff --git a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb new file mode 100644 index 0000000..976863b --- /dev/null +++ b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.describe AdvanceTeamsInIntermediateStage do + shared_examples_for 'succeeding context' do + it 'succeeds' do + expect(context).to be_a_success + end + end + + shared_examples_for 'failing context' do + it 'fails' do + expect(context).to be_a_failure + end + end + + context 'intermediate_stage is nil' do + let(:context) do + AdvanceTeamsInIntermediateStage.call(intermediate_stage: nil) + end + + it_behaves_like 'succeeding context' + + it 'doesn\'t call PopulateMatchBelow' do + expect(PopulateMatchBelowAndSave).not_to receive(:call) + context + end + end + + context 'intermediate_stage is a realistic stage' do + let(:context) do + AdvanceTeamsInIntermediateStage.call(intermediate_stage: create(:playoff_stage, match_type: :single_team_match)) + end + + context 'PopulateMatchBelow succeeds' do + before do + expect(class_double('PopulateMatchBelowAndSave').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:call).exactly(4).times.and_return(double(:context, success?: true)) + end + + it_behaves_like 'succeeding context' + end + + context 'PopulateMatchBelow fails' do + before do + expect(class_double('PopulateMatchBelowAndSave').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:call).and_return(double(:context, success?: false)) + end + + it_behaves_like 'failing context' + end + end +end diff --git a/spec/interactors/populate_match_below_interactor_spec.rb b/spec/interactors/populate_match_below_interactor_spec.rb index 22ef725..2201c91 100644 --- a/spec/interactors/populate_match_below_interactor_spec.rb +++ b/spec/interactors/populate_match_below_interactor_spec.rb @@ -21,7 +21,7 @@ RSpec.describe PopulateMatchBelow, type: :interactor do end it 'provides the objects to save' do - expect(context.object_to_save).to match_array(@objects_to_save) + expect(context.object_to_save.flatten).to match_array(@objects_to_save.flatten) end end diff --git a/spec/interactors/update_groups_group_scores_interactor_spec.rb b/spec/interactors/update_groups_group_scores_interactor_spec.rb index e664ffc..bc9a3d9 100644 --- a/spec/interactors/update_groups_group_scores_interactor_spec.rb +++ b/spec/interactors/update_groups_group_scores_interactor_spec.rb @@ -22,7 +22,7 @@ RSpec.describe UpdateGroupsGroupScores, type: :interactor do end it 'provides the objects to save' do - expect(context.object_to_save).to eq(@group_scores) + expect(context.object_to_save.flatten).to eq(@group_scores) end end diff --git a/spec/services/match_service_spec.rb b/spec/services/match_service_spec.rb index 21ada52..726fc55 100644 --- a/spec/services/match_service_spec.rb +++ b/spec/services/match_service_spec.rb @@ -117,5 +117,16 @@ RSpec.describe MatchService do it 'raises an exception for for 0 teams' do expect { MatchService.generate_matches([]) }. to raise_error 'Cannot generate Matches without teams' end + + it 'generates matches with consecutive positions' do + MatchService.generate_matches(create_list(:team, 7)).sort_by(&:position).each_with_index do |match, i| + expect(match.position).to eq(i) + end + end + + it 'places all given teams into the matches exactly once' do + teams = create_list(:team, 11) + expect(MatchService.generate_matches(teams).map(&:teams).flatten).to match_array(teams) + end end end diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 91e3149..97fdfb7 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -51,7 +51,7 @@ RSpec.describe PlayoffStageService do end end - describe 'generates playoff stages for' do + describe '#generate_playoff_stages' do [ { team_size: 1, expected_amount_of_playoff_stages: 1 }, { team_size: 2, expected_amount_of_playoff_stages: 1 }, @@ -66,7 +66,7 @@ RSpec.describe PlayoffStageService do { team_size: 64, expected_amount_of_playoff_stages: 6 }, { team_size: 111, expected_amount_of_playoff_stages: 7 } ].each do |parameters| - it "#{parameters[:team_size]} teams" do + it "generates playoff stages for #{parameters[:team_size]} teams" do amount_of_teams = parameters[:team_size] expected_amount_of_playoff_stages = parameters[:expected_amount_of_playoff_stages] teams = build_list(:team, amount_of_teams) @@ -79,6 +79,38 @@ RSpec.describe PlayoffStageService do end end end + + describe 'number of teams isn\'t a power of two' do + let(:generated_stages) do + PlayoffStageService.generate_playoff_stages(create_list(:team, 12)) + end + + let(:intermediate_stage) do + generated_stages.max_by(&:level) + end + + it 'generates an intermediate stage at the top level' do + expect(intermediate_stage.state).to eq('intermediate_stage') + end + + it 'generates normal playoff_stage state stages elsewhere' do + (generated_stages - [intermediate_stage]).each do |stage| + expect(stage.state).to eq('playoff_stage') + end + end + end + + describe 'number of teams is a power of two' do + let(:generated_stages) do + PlayoffStageService.generate_playoff_stages(create_list(:team, 16)) + end + + it 'generates only normal playoff_stage state stages' do + generated_stages.each do |stage| + expect(stage.state).to eq('playoff_stage') + end + end + end end describe '#populate_match_below' do