Implement Adding Playoffs to a tournament

This commit is contained in:
Daniel Schädler 2018-11-29 11:10:15 +01:00
parent 7f243b06a2
commit 26bcc3dc88
8 changed files with 356 additions and 51 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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