From 26bcc3dc88bcdc2d492579b0ee25f15fcb8417ac Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 29 Nov 2018 11:10:15 +0100 Subject: [PATCH] Implement Adding Playoffs to a tournament --- app/interactors/add_playoffs_to_tournament.rb | 20 ++++ app/services/group_stage.rb | 18 ---- app/services/match_service.rb | 37 +++++++ app/services/matches.rb | 33 ------ app/services/playoff_stage_service.rb | 47 ++++++++ ..._playoffs_to_tournament_interactor_spec.rb | 72 +++++++++++++ spec/services/match_service_spec.rb | 102 ++++++++++++++++++ spec/services/playoff_stage_service_spec.rb | 78 ++++++++++++++ 8 files changed, 356 insertions(+), 51 deletions(-) create mode 100644 app/interactors/add_playoffs_to_tournament.rb delete mode 100644 app/services/group_stage.rb create mode 100644 app/services/match_service.rb delete mode 100644 app/services/matches.rb create mode 100644 app/services/playoff_stage_service.rb create mode 100644 spec/interactors/add_playoffs_to_tournament_interactor_spec.rb create mode 100644 spec/services/match_service_spec.rb create mode 100644 spec/services/playoff_stage_service_spec.rb diff --git a/app/interactors/add_playoffs_to_tournament.rb b/app/interactors/add_playoffs_to_tournament.rb new file mode 100644 index 0000000..fbe7f25 --- /dev/null +++ b/app/interactors/add_playoffs_to_tournament.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddPlayoffsToTournament + include Interactor + + def call + tournament = context.tournament + context.fail! if tournament.stages.size > 1 + if (playoff_stages = PlayoffStageService.generate_playoff_stages_from_tournament(tournament)) + if tournament.stages.empty? + tournament.stages = playoff_stages + else + tournament.stages.concat playoff_stages + end + context.tournament = tournament + else + context.fail! + end + end +end diff --git a/app/services/group_stage.rb b/app/services/group_stage.rb deleted file mode 100644 index e0b4d5d..0000000 --- a/app/services/group_stage.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Turniere - class GroupStage - def self.generate_playoffs(teams, _tournament) - stage_count = calculate_required_stage_count(teams.size) - generate_matches(teams) - end - - def self.calculate_required_stage_count(number_of_teams) - if number_of_teams.zero? || number_of_teams == 1 - 0 - else - Math.log(Turniere::Utils.previous_power_of_two(number_of_teams)) / Math.log(2) - end - end - end -end diff --git a/app/services/match_service.rb b/app/services/match_service.rb new file mode 100644 index 0000000..ca4bce9 --- /dev/null +++ b/app/services/match_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class MatchService + def self.generate_matches(teams) + if teams.size < 2 + # should be prevented by controller + return + end + + if Utils.po2?(teams.size) + normal_games = teams.size / 2 + needed_games = normal_games + else + normal_games = teams.size - Utils.previous_power_of_two(teams.size) + needed_games = Utils.previous_power_of_two(teams.size) + end + + matches = [] + while matches.size < normal_games + i = matches.size + match = Match.new state: :not_started, + position: i, + scores: [ + Score.create(team: teams[2 * i]), + Score.create(team: teams[(2 * i) + 1]) + ] + matches << match + end + + until matches.size >= needed_games + i = matches.size + match = Match.new state: :single_team, position: i, scores: [Score.create(team: teams[i])] + matches << match + end + matches + end +end diff --git a/app/services/matches.rb b/app/services/matches.rb deleted file mode 100644 index df444b9..0000000 --- a/app/services/matches.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Turniere - class Matches - def self.generate_matches(teams) - if teams.size == 1 - return #TODO error with only one team - end - needed_games = 0 - if (Turniere::Utils.po2?(teams.size()) - needed_games = teams.size() / 2 - else - needed_games = teams.size() - Turniere::Utils.previous_power_of_two(teams.size()) / 2 - end - - lastPos = 0 - matches = [] - i = 0 - - while i < needed_games - match = Match(teams[2 * i], teams[( 2 * i ) + 1], 0, 0, :not_startet, i, false) - matches.insert match - i++ - end - - lastPos = i + 1 - - while teams.size() != 0 - match = Match(teams[2 * i], teams[( 2 * i ) + 1], 0, 0, Match, i, false) - matches.insert match - end - return lastPos - end - end -end \ No newline at end of file diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb new file mode 100644 index 0000000..1251472 --- /dev/null +++ b/app/services/playoff_stage_service.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class PlayoffStageService + def self.generate_playoff_stages(teams) + playoffs = [] + stage_count = calculate_required_stage_count(teams.size) + initial_matches = MatchService.generate_matches(teams) + initial_stage = Stage.new level: stage_count - 1, matches: initial_matches + playoffs << initial_stage + empty_stages = generate_stages_with_empty_matches(stage_count - 1) + empty_stages.each do |stage| + playoffs << stage + end + playoffs + end + + def self.generate_playoff_stages_from_tournament(tournament) + generate_playoff_stages tournament.teams + end + + 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 + empty_stages.reverse! + end + + def self.generate_empty_matches(amount) + matches = [] + amount.times do |i| + match = Match.new state: :not_ready, position: i + matches << match + end + matches + end + + def self.calculate_required_stage_count(number_of_teams) + if number_of_teams.zero? || number_of_teams == 1 + 0 + else + stage_count = Math.log(Utils.previous_power_of_two(number_of_teams)) / Math.log(2) + stage_count.to_int + end + end +end diff --git a/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb new file mode 100644 index 0000000..3bb385d --- /dev/null +++ b/spec/interactors/add_playoffs_to_tournament_interactor_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +RSpec.describe AddPlayoffsToTournament do + let(:group_stage_tournament_context) do + AddPlayoffsToTournament.call(tournament: @group_stage_tournament) + end + + let(:playoff_stage_tournament_context) do + AddPlayoffsToTournament.call(tournament: @playoff_stage_tournament) + end + + let(:full_tournament_context) do + AddPlayoffsToTournament.call(tournament: @full_tournament) + end + + before do + @group_stage_tournament = create(:stage_tournament) + @playoff_stage_tournament = create(:tournament) + @full_tournament = create(:stage_tournament, stage_count: 5) + @stages = create_list(:stage, 5) + end + + context 'ez lyfe' do + before do + expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:generate_playoff_stages_from_tournament) + .and_return(@stages) + end + + context 'Playoff only tournament' do + it 'succeeds' do + expect(playoff_stage_tournament_context).to be_a_success + end + + it 'adds playoffs to the tournament' do + test = playoff_stage_tournament_context.tournament.stages + expect(test).to match_array(@stages) + end + end + + context 'GroupStage tournament' do + it 'succeeds' do + expect(group_stage_tournament_context).to be_a_success + end + + it 'adds playoffs to the tournament' do + test = group_stage_tournament_context.tournament.stages[1..-1] + expect(test).to match_array(@stages) + end + end + end + + context 'playoff generation fails' do + before do + expect(class_double('PlayoffStageService').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:generate_playoff_stages_from_tournament) + .and_return(nil) + end + + it 'fails' do + test = playoff_stage_tournament_context.failure? + expect(test).to eq(true) + end + end + + context 'Tournament where playoffs are already generated' do + it 'does not add playoff stages' do + test = full_tournament_context.failure? + expect(test).to eq(true) + end + end +end diff --git a/spec/services/match_service_spec.rb b/spec/services/match_service_spec.rb new file mode 100644 index 0000000..7706e1f --- /dev/null +++ b/spec/services/match_service_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +RSpec.describe MatchService do + describe '#generate_matches' do + [ + { team_size: 2 }, + { team_size: 4 }, + { team_size: 8 }, + { team_size: 16 }, + { team_size: 32 }, + { team_size: 64 } + ].each do |parameters| + result = parameters[:team_size] / 2 + it "generates #{result} matches from #{parameters[:team_size]} teams" do + teams = build_list(:team, parameters[:team_size], tournament: create(:tournament)) + generated_matches = MatchService.generate_matches teams + expect(generated_matches.size).to eq(result) + end + end + + [ + { team_size: 3, result: 2 }, + { team_size: 5, result: 4 }, + { team_size: 6, result: 4 }, + { team_size: 7, result: 4 }, + { team_size: 12, result: 8 }, + { team_size: 17, result: 16 }, + { team_size: 18, result: 16 }, + { team_size: 19, result: 16 }, + { team_size: 22, result: 16 }, + { team_size: 45, result: 32 }, + { team_size: 87, result: 64 }, + { team_size: 102, result: 64 }, + { team_size: 111, result: 64 }, + { team_size: 124, result: 64 }, + { team_size: 132, result: 128 }, + { team_size: 255, result: 128 } + ].each do |parameters| + it "generates #{parameters[:result]} matches from #{parameters[:team_size]} teams" do + teams = build_list(:team, parameters[:team_size], tournament: create(:tournament)) + generated_matches = MatchService.generate_matches teams + expect(generated_matches.size).to eq(parameters[:result]) + end + end + + [ + { team_size: 2 }, + { team_size: 4 }, + { team_size: 8 }, + { team_size: 16 }, + { team_size: 32 }, + { team_size: 64 }, + { team_size: 128 }, + { team_size: 256 } + + ].each do |parameters| + it "matches the right teams for powers of 2 (#{parameters[:team_size]})" do + teams = build_list(:team, parameters[:team_size], tournament: create(:tournament)) + generated_matches = MatchService.generate_matches teams + generated_matches.each_index do |index| + match = generated_matches[index] + first_team = match.scores.first.team.name + second_team = match.scores.second.team.name + expect(first_team).to eq(teams[2 * index].name) + expect(second_team).to eq(teams[2 * index + 1].name) + end + end + end + + # TODO: matches right teams for !powers of 2 + + [ + { team_size: 3, single_team_matches: 1 }, + { team_size: 5, single_team_matches: 3 }, + { team_size: 6, single_team_matches: 2 }, + { team_size: 17, single_team_matches: 15 }, + { team_size: 34, single_team_matches: 30 }, + { team_size: 65, single_team_matches: 63 }, + { team_size: 138, single_team_matches: 118 }, + { team_size: 276, single_team_matches: 236 } + + ].each do |parameters| + team_size = parameters[:team_size] + single_team_matches = parameters[:single_team_matches] + it "generates #{single_team_matches} empty matches for #{team_size} teams" do + teams = build_list(:team, team_size, tournament: create(:tournament)) + generated_matches = MatchService.generate_matches teams + filtered_matches = generated_matches.select(&:single_team?) + expected_single_team_matches_size = single_team_matches + expect(filtered_matches.size).to eq(expected_single_team_matches_size) + end + end + + it 'generates no matches for 0 teams' do + expect(MatchService.generate_matches([])). to eq(nil) + end + + it 'generates no matches for 1 team' do + expect(MatchService.generate_matches(build_list(:team, 1))). to eq(nil) + end + end +end diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb new file mode 100644 index 0000000..1fb8eb7 --- /dev/null +++ b/spec/services/playoff_stage_service_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.describe PlayoffStageService do + describe '#generate_empty_matches' do + [ + { amount: 1 }, + { amount: 3 }, + { amount: 4 }, + { amount: 7 }, + { amount: 23 }, + { amount: 33 }, + { amount: 82 }, + { amount: 359 } + ].each do |parameters| + it "generates #{parameters[:amount]} empty matches" do + amount = parameters[:amount] + generated_matches = PlayoffStageService.generate_empty_matches amount + generated_matches.each_index do |i| + expect(generated_matches[i].not_ready?).to eq(true) + expect(generated_matches[i].position).to eq(i) + end + expect(generated_matches.size).to eq(amount) + end + end + end + + describe '#generate_stages_with_empty_matches' do + [ + { stages: 1 }, + { stages: 2 }, + { stages: 3 }, + { stages: 4 }, + { stages: 5 }, + { stages: 6 }, + { stages: 7 }, + { stages: 8 }, + { stages: 9 }, + { stages: 10 } + ].each do |parameters| + it "generates #{parameters[:stages]} stages with matches provided by #generate_empty_matches" do + amount_of_empty_stages = parameters[:stages] + empty_stages = PlayoffStageService.generate_stages_with_empty_matches(amount_of_empty_stages) + expect(empty_stages.size).to eq(amount_of_empty_stages) + empty_stages.each_index do |i| + empty_stage = empty_stages[i] + expected_empty_stages_size = empty_stages.size - 1 - i + expect(empty_stage.level).to eq(expected_empty_stages_size) + expect(empty_stage.matches.size).to eq(2**expected_empty_stages_size) + end + end + end + end + + describe '#generate_playoffs' do + [ + { team_size: 4, expected_amount_of_playoff_stages: 2 }, + { team_size: 8, expected_amount_of_playoff_stages: 3 }, + { team_size: 16, expected_amount_of_playoff_stages: 4 }, + { team_size: 24, expected_amount_of_playoff_stages: 4 }, + { team_size: 32, expected_amount_of_playoff_stages: 5 }, + { team_size: 64, expected_amount_of_playoff_stages: 6 }, + { team_size: 111, expected_amount_of_playoff_stages: 6 } + ].each do |parameters| + 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) + stages = PlayoffStageService.generate_playoff_stages(teams) + expect(stages.size).to eq(expected_amount_of_playoff_stages) + stages.each_index do |i| + stage = stages[i] + stage_level = stages.size - i - 1 + expect(stage.level).to eq stage_level + end + end + end + end +end