diff --git a/Gemfile b/Gemfile index 3c2f1db..0fc4715 100644 --- a/Gemfile +++ b/Gemfile @@ -43,7 +43,6 @@ gem 'active_model_serializers' gem 'mailgun-ruby' group :test, optional: true do - gem 'coveralls', require: false gem 'factory_bot_rails' gem 'faker' gem 'rspec-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 0779004..13f0829 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,12 +91,6 @@ GEM case_transform (0.2) activesupport concurrent-ruby (1.2.3) - coveralls (0.8.23) - json (>= 1.8, < 3) - simplecov (~> 0.16.1) - term-ansicolor (~> 1.3) - thor (>= 0.19.4, < 2.0) - tins (~> 1.6) crass (1.0.6) date (3.3.4) devise (4.9.3) @@ -106,7 +100,6 @@ GEM responders warden (~> 1.2.3) diff-lcs (1.5.1) - docile (1.4.0) domain_name (0.6.20240107) e2mmap (0.1.0) erubi (1.12.0) @@ -277,11 +270,6 @@ GEM ruby-progressbar (1.13.0) shoulda-matchers (6.2.0) activesupport (>= 5.2.0) - simplecov (0.16.1) - docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) solargraph (0.50.0) backport (~> 1.2) benchmark @@ -305,14 +293,9 @@ GEM sqlite3 (1.7.3-aarch64-linux) sqlite3 (1.7.3-arm64-darwin) sqlite3 (1.7.3-x86_64-linux) - sync (0.5.0) - term-ansicolor (1.7.2) - tins (~> 1.0) thor (1.3.1) tilt (2.3.0) timeout (0.4.1) - tins (1.32.1) - sync tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) @@ -334,7 +317,6 @@ PLATFORMS DEPENDENCIES active_model_serializers bootsnap - coveralls devise devise_token_auth! factory_bot_rails diff --git a/README.md b/README.md index 9015644..5284cb3 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,4 @@ $ rails diagram:all_with_engines - WICHTIG UND EZ: gruppenphase in der gleichen gruppe sollten erst finale gegeneinander spielen (dazu nicht aus der nächsten gruppe sondern einmal advancing teams von vorne und einmal von hinten, oder offset von hälfte der weiterkommenden teams) - beim eintragen einer runde direkt den nächsten tisch anzeigen - spiel um platz 3 +- edgecase wenn mehr als die hälfte der teams weiterkommen bedenken bzw zumindest abfangen diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 2e33843..85d1eb3 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -116,7 +116,7 @@ class TournamentsController < ApplicationController end def organize_teams_in_groups(teams) - # each team gets put into a array of teams depending on the group specified in team[:group] + # each team gets put into an array of teams depending on the group specified in team[:group] teams.group_by { |team| team['group'] }.values.map do |group| group.map do |team| find_or_create_team(team) diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index 7b2ac94..c4b545c 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -102,39 +102,69 @@ class GroupStageService # @param group_stage GroupStage the group stage to get all advancing teams from # @return [Array] the teams advancing from that group stage def get_advancing_teams(group_stage) - advancing_teams = [] teams_per_group_ranked = group_stage.groups.map(&method(:teams_sorted_by_group_scores)) - # teams_per_group_ranked is a 2D array - # [ - # [ group_a_first, group_a_second, ... ], - # [ group_b_first, group_b_second, ... ], - # ... - # ] - advancing_teams_amount = group_stage.tournament.instant_finalists_amount + - group_stage.tournament.intermediate_round_participants_amount + advancing_teams_amount = calculate_advancing_teams_amount(group_stage) tournament_teams_amount = group_stage.tournament.teams.size - # special case for po2 teams in tournament and half of them advancing: - # we want to match first of first group with second of second group and so on - if Utils.po2?(tournament_teams_amount) and advancing_teams_amount * 2 == tournament_teams_amount - teams_per_group_ranked.each_with_index do |_group_teams, i| - first = teams_per_group_ranked[i % teams_per_group_ranked.size][0] - second = teams_per_group_ranked[(i + 1) % teams_per_group_ranked.size][1] - advancing_teams << first - advancing_teams << second - end - # default case + if special_case_for_po2?(tournament_teams_amount, advancing_teams_amount) + handle_special_case(teams_per_group_ranked) else - advancing_teams_amount.times do |i| - # we want to take the first team of the first group, then the first team of the second group, ... - advancing_teams << teams_per_group_ranked[i % group_stage.groups.size].shift - end + handle_default_case(teams_per_group_ranked, advancing_teams_amount, group_stage.groups.size) end - advancing_teams end private + # Calculates the total number of teams advancing to the playoff stage + # + # @param group_stage GroupStage the group stage to get the advancing teams amount from + # @return [Integer] the number of teams advancing from that group stage + def calculate_advancing_teams_amount(group_stage) + group_stage.tournament.instant_finalists_amount + + group_stage.tournament.intermediate_round_participants_amount + end + + # Checks if the special case for po2 teams in the tournament applies + # + # @param tournament_teams_amount [Integer] the total number of teams in the tournament + # @param advancing_teams_amount [Integer] the number of teams advancing to the playoff stage + # @return [Boolean] true if the special case applies, false otherwise + def special_case_for_po2?(tournament_teams_amount, advancing_teams_amount) + Utils.po2?(tournament_teams_amount) && advancing_teams_amount * 2 == tournament_teams_amount + end + + # Handles the special case for po2 teams in the tournament + # + # @param teams_per_group_ranked [Array] a 2D array of teams ranked by group scores + # @return [Array] the teams advancing from the group stage + def handle_special_case(teams_per_group_ranked) + # transpose the array to group first and second places together + # e.g. [[1, 2, 3], [4, 5, 6]] to [[1, 4], [2, 5], [3, 6]] + teams_per_group_ranked_transposed = teams_per_group_ranked.transpose + first_places = teams_per_group_ranked_transposed[0] + second_places = teams_per_group_ranked_transposed[1] + + second_places_new_order = Utils.split_and_rotate(second_places) + + # zip the first and second places together + # e.g. [1, 2, 3], [a, b, c] to [1, a, 2, b, 3, c] + first_places.zip(second_places_new_order).flatten + end + + # Handles the default case for advancing teams + # + # @param teams_per_group_ranked [Array] a 2D array of teams ranked by group scores + # @param advancing_teams_amount [Integer] the number of teams advancing to the playoff stage + # @param groups_size [Integer] the number of groups in the group stage + # @return [Array] the teams advancing from the group stage + def handle_default_case(teams_per_group_ranked, advancing_teams_amount, groups_size) + advancing_teams = [] + advancing_teams_amount.times do |i| + advancing_teams << teams_per_group_ranked[i % groups_size].shift + end + advancing_teams + end + def recalculate_position_of_group_scores!(group_scores) group_scores = group_scores.sort diff --git a/app/services/utils.rb b/app/services/utils.rb index 9a53daf..e2e9a3f 100644 --- a/app/services/utils.rb +++ b/app/services/utils.rb @@ -29,4 +29,17 @@ class Utils def self.po2?(number) number.to_s(2).count('1') == 1 end + + # split the array in half and place the second half at the beginning + # e.g. [1, 2, 3, 4, 5, 6] to [4, 5, 6, 1, 2, 3] + def self.split_and_rotate(array) + # handle the case where the array has an odd number of elements + middle_element = [] + if array.length.odd? + # pop the last element and place it in the middle + middle_element = [array.pop] + end + mid = array.length / 2 + array[mid..] + middle_element + array[0..(mid - 1)] + end end diff --git a/spec/factories/tournaments.rb b/spec/factories/tournaments.rb index 9a8a7d3..4e2ab21 100644 --- a/spec/factories/tournaments.rb +++ b/spec/factories/tournaments.rb @@ -7,13 +7,23 @@ FactoryBot.define do user transient do teams_count { 8 } + teams { nil } + playoff_teams_amount { 4 } + instant_finalists_amount { 4 } + intermediate_round_participants_amount { 0 } end after(:create) do |tournament, evaluator| - tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament) - tournament.playoff_teams_amount = (tournament.teams.size / 2) - tournament.instant_finalists_amount = (tournament.playoff_teams_amount / 2) - tournament.intermediate_round_participants_amount = ((tournament.playoff_teams_amount - - tournament.instant_finalists_amount) * 2) + if evaluator.teams.present? + tournament.teams = evaluator.teams + else + tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament) + end + tournament.playoff_teams_amount = evaluator.playoff_teams_amount + tournament.instant_finalists_amount = evaluator.instant_finalists_amount + tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount + if tournament.playoff_teams_amount != tournament.instant_finalists_amount + tournament.intermediate_round_participants_amount / 2 + raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2' + end tournament.save! end @@ -52,6 +62,15 @@ FactoryBot.define do end end + factory :prepared_group_stage_tournament do + transient do + group_stage { create(:group_stage) } + end + after(:create) do |tournament, evaluator| + tournament.stages << evaluator.group_stage + end + end + factory :dummy_stage_tournament do transient do stage_count { 3 } diff --git a/spec/services/group_stage_service_spec.rb b/spec/services/group_stage_service_spec.rb index 8073ef2..3b48d3f 100644 --- a/spec/services/group_stage_service_spec.rb +++ b/spec/services/group_stage_service_spec.rb @@ -190,4 +190,86 @@ RSpec.describe GroupStageService do end end end + + describe '#get_advancing_teams' do + context 'when special case for po2 applies' do + before do + teams = create_list(:team, 32) + + # put the teams in groups of four + groups = teams.each_slice(4).to_a + + # iterate over all groups and number the teams in their name + groups.each_with_index do |group, group_index| + group.each_with_index do |team, team_index| + team.name = "#{team.name} #{group_index} #{team_index}" + team.save! + end + end + + # Generate the group stage + @group_stage = GroupStageService.generate_group_stage(groups) + + @tournament = create(:prepared_group_stage_tournament, + group_stage: @group_stage, + teams: teams, + playoff_teams_amount: 16, + instant_finalists_amount: 16, + intermediate_round_participants_amount: 0) + # iterate over all groups and update the matches within to all be decided + @group_stage.groups.each do |group| + group.matches.each do |match| + match.match_scores.each do |ms| + # give the team 10 points minus the number in their name + # this results in the team 0 always winning and getting to place 1 in the group etc. + ms.points = 10 - ms.team.name.split(' ').last.to_i + ms.save! + end + match.state = 'finished' + match.save! + end + group_scores = GroupStageService.update_group_scores(group) + group_scores.each(&:save!) + + group.group_scores.each(&:reload) + end + end + + it 'returns the correct amount of teams' do + advancing_teams = GroupStageService.get_advancing_teams(@group_stage) + expect(advancing_teams.size).to be(16) + end + + it 'returns the correct teams in the correct order' do + advancing_teams = GroupStageService.get_advancing_teams(@group_stage) + advancing_teams.each_with_index do |team, i| + # if index is even, the team should be of a first place; end in a 0 + # if index is odd, the team should be of a second place; end in a 1 + team_quality = team.name.split(' ').last.to_i + expect(team_quality % 2).to be(i % 2) + end + end + + it 'spaces groups apart, so you meet your group only in finale' do + group_first_matchups_expected = { + 0 => 4, + 1 => 5, + 2 => 6, + 3 => 7, + 4 => 0, + 5 => 1, + 6 => 2, + 7 => 3 + } + advancing_teams = GroupStageService.get_advancing_teams(@group_stage) + advancing_teams.each_slice(2).to_a.each do |matchup| + # this is the team that landed a first place in the group + first_place_team = matchup[0].name.split(' ')[1].to_i + # this is the team that landed a second place in the group + second_place_team = matchup[1].name.split(' ')[1].to_i + expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team) + end + end + end + end end diff --git a/spec/services/utils_spec.rb b/spec/services/utils_spec.rb index f1c9ef5..ee84fd8 100644 --- a/spec/services/utils_spec.rb +++ b/spec/services/utils_spec.rb @@ -47,4 +47,20 @@ RSpec.describe Utils do expect(Utils.po2?(parameters[:test])).to eq(parameters[:result]) end end + + describe '#split_and_rotate' do + [ + { test: [1, 2, 3, 4, 5, 6], result: [4, 5, 6, 1, 2, 3] }, + { test: [1, 2, 3, 4, 5], result: [3, 4, 5, 1, 2] }, + { test: [1, 2, 3, 4], result: [3, 4, 1, 2] }, + { test: [1, 2, 3], result: [2, 3, 1] }, + { test: [1, 2], result: [2, 1] }, + { test: [1], result: [1] }, + { test: [], result: [] } + ].each do |parameters| + it "splits and rotates #{parameters[:test]} to #{parameters[:result]}" do + expect(Utils.split_and_rotate(parameters[:test])).to eq(parameters[:result]) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 74a58f0..f38f403 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'coveralls' -Coveralls.wear!('rails') - # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause