Merge branch 'first_vs_second_with_offset' into 'master'

First vs second with offset

See merge request turniere/turniere-backend!29
This commit is contained in:
Daniel Schädler 2025-03-14 12:35:17 +00:00
commit b1cc300d98
10 changed files with 191 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
handle_default_case(teams_per_group_ranked, advancing_teams_amount, group_stage.groups.size)
end
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

View File

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

View File

@ -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|
if evaluator.teams.present?
tournament.teams = evaluator.teams
else
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)
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 }

View File

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

View File

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

View File

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