From 93820341c2d09a2e5602e5609cf774347fb2d1b5 Mon Sep 17 00:00:00 2001 From: Brett Ryland Date: Mon, 5 Jun 2023 16:10:02 +0200 Subject: [PATCH] Switch the heat generation to use the same strategy as local tournaments so that heats always contain N or N-1 players. Fix a spelling typo. --- app/models/competition.rb | 19 +++++++++++++++++++ lib/armory.rb | 32 +++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/models/competition.rb b/app/models/competition.rb index 58528a2..fd4dd92 100644 --- a/app/models/competition.rb +++ b/app/models/competition.rb @@ -166,6 +166,25 @@ def players_per_heat(vessel_count) return possibles[mods.find_index(max_mod)] end + # An alternative to players_per_heat that spreads the deficit amongst the other heats (same as the local tournaments) + def players_per_heat_v2(vessel_count, limits) + possibles = (vessel_count > limits[1] && vessel_count < 2 * limits[0] - 1) ? Array(limits[1]/2..limits[1]) : Array(limits[0]..limits[1]) + vessel_count = vessels.includes(:player).where(players: { is_human: true }).count unless vessel_count + mods = possibles.map { |e| vessel_count % e } + zero_index = mods.reverse.find_index(0) + if !zero_index.nil? + # zero means we can have even heats + return possibles.reverse[zero_index], 0 + end + players_per_full_heat, heats_with_one_less = players_per_heat_better(vessel_count + 1, limits) + return players_per_full_heat, heats_with_one_less + 1 + end + + # Get the range of players per heat + def get_per_heat_limits() + return [6, max(8, max_players_per_heat)] rescue [6, 10] + end + def has_remaining_heats?(stage) heats.where(stage: stage, ended_at: nil).any? end diff --git a/lib/armory.rb b/lib/armory.rb index 17795a3..20476bc 100644 --- a/lib/armory.rb +++ b/lib/armory.rb @@ -7,7 +7,7 @@ def generate_heats(competition, stage, strategy, force=false) strategy.apply!(competition, stage) end - class GroupedStragegy + class GroupedStrategy def generate_with_groups(competition, stage, groups) groups.each.with_index do |g,k| heat = competition.heats.where(stage: stage, order: k).first_or_create @@ -16,31 +16,45 @@ def generate_with_groups(competition, stage, groups) end end end + + # Pass shuffled vessels for randomised, or ordered for ranked + def generate_groups(competition, limits, player_vessels) + vessel_count = player_vessels.length + players_per_full_heat, heats_with_one_less = competition.players_per_heat_v2(vessel_count, limits) + full_heats = (vessel_count / players_per_full_heat.to_f - heats_with_one_less).ceil + groups = full_heats > 0 ? + Array(0..full_heats - 1).map {|s| player_vessels.slice(s * players_per_full_heat, players_per_full_heat)}.concat( + Array(0..heats_with_one_less - 1).map {|s| player_vessels.slice(full_heats * players_per_full_heat + s * (players_per_full_heat - 1), players_per_full_heat - 1)}) : + player_vessels + return groups + end end - class RandomDistributionStrategy < GroupedStragegy + class RandomDistributionStrategy < GroupedStrategy def apply!(competition, stage) npc_vessels = competition.vessels.includes(:player).where(players: { is_human: false }) rescue [] player_vessels = competition.vessels.includes(:player).where(players: { is_human: true }) - players_per_heat = competition.players_per_heat(player_vessels.count) - puts "RandomDistribution for #{player_vessels.count} players in groups of #{players_per_heat}" - groups = player_vessels.shuffle.in_groups_of(players_per_heat, false).map { |g| g + npc_vessels } + limits = competition.get_per_heat_limits() + puts "RandomDistribution for #{player_vessels.count} players in groups of #{limits[0]}—#{limits[1]} with #{npc_vessels.length} npcs" + groups = generate_groups(competition, limits, player_vessels.shuffle) + groups = groups.map { |g| g + npc_vessels } generate_with_groups(competition, stage, groups) end end - class TournamentRankingStrategy < GroupedStragegy + class TournamentRankingStrategy < GroupedStrategy def apply!(competition, stage) ranked_vessels = competition.rankings.includes(:vessel).sort_by { |e| e.score }.map(&:vessel) npc_vessels = competition.vessels.includes(:player).where(players: { is_human: false }) player_vessels = ranked_vessels.filter { |v| v.player.is_human } - players_per_heat = competition.players_per_heat(player_vessels.count) - groups = player_vessels.in_groups_of(players_per_heat, false).map { |g| g + npc_vessels } + limits = competition.get_per_heat_limits() + groups = generate_groups(competition, limits, ranked_vessels) + groups = groups.map { |g| g + npc_vessels } generate_with_groups(competition, stage, groups) end end - class SinglePlayerStrategy < GroupedStragegy + class SinglePlayerStrategy < GroupedStrategy def apply!(competition, stage) npc_vessels = competition.vessels.includes(:player).where(players: { is_human: false }) player_vessels = competition.vessels.includes(:player).where(players: { is_human: true })