Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI support for more Single Elimination cuts. #394

Merged
merged 1 commit into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/controllers/players_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ def standings_data
registrations: [player: [:user, :corp_identity_ref, :runner_identity_ref, { registrations: [:stage] }]],
standing_rows: [player: [:user, :corp_identity_ref, :runner_identity_ref, { registrations: [:stage] }]]
)
double_elim = stages.select(&:double_elim?).first
elimination = stages.select(&:double_elim?).first
elimination = stages.select(&:single_elim?).first if elimination.nil?
render json: {
is_player_meeting: stages.all? { |stage| stage.rounds.empty? },
manual_seed: @tournament.manual_seed?,
Expand All @@ -140,16 +141,17 @@ def standings_data
format: stage.format,
rounds_complete: stage.rounds.select(&:completed?).count,
any_decks_viewable: stage.decks_visible_to(current_user) ||
(double_elim&.decks_visible_to(current_user) ? true : false),
(elimination&.decks_visible_to(current_user) ? true : false),
standings: render_standings_for_stage(stage)
}
end
}
end

def render_standings_for_stage(stage)
if stage.double_elim?
if stage.single_elim? || stage.double_elim?
# Compute standings on the fly during cut
Rails.logger.info 'Computing cut standings'
return compute_and_render_cut_standings stage
end
if stage.rounds.select(&:completed?).any?
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/rounds_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def pairings_data_round(stage, players, view_decks, round)
pairings << {
id:,
table_number:,
table_label: stage.double_elim? ? "Game #{table_number}" : "Table #{table_number}",
table_label: stage.double_elim? || stage.single_elim? ? "Game #{table_number}" : "Table #{table_number}",
policy: {
view_decks:
},
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/tournaments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ def cut
authorize @tournament

number = params[:number].to_i
format = params[:elimination_type] == 'single' ? :single_elim : :double_elim
return redirect_to standings_tournament_players_path(@tournament) unless [3, 4, 8, 16].include? number

@tournament.cut_to!(:double_elim, number)
@tournament.cut_to!(format, number)

redirect_to tournament_rounds_path(@tournament)
end
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/pairings/Pairing.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
let left_player = pairing.player1;
let right_player = pairing.player2;
console.log(`Format: ${stage.format}`);
if (pairing.player2.side == 'corp' && ['single_sided_swiss', 'double_elim'].includes(stage.format)) {
if (pairing.player2.side == 'corp' && ['single_sided_swiss', 'double_elim', 'single_elim'].includes(stage.format)) {
console.log(`Swapping players for round ${round.id}...`);
left_player = pairing.player2;
right_player = pairing.player1;
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/standings/Standings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

{#if data}
{#each data.stages as stage}
{#if stage.format === 'double_elim' }
{#if stage.format === 'single_elim' || stage.format === 'double_elim'}
<DoubleElimStandings stage={cutStage(stage)}/>
{:else}
<SwissStandings stage={swissStage(stage)} manual_seed="{data.manual_seed}"/>
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/pairings_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def presets(pairing)
end

# Single-sided elimination round
if pairing.stage.double_elim? && !pairing.side.nil?
if (pairing.stage.single_elim? || pairing.stage.double_elim?) && !pairing.side.nil?
if pairing.player1_is_corp?
return [
{ score1: 3, score2: 0, score1_corp: 3, score2_runner: 0, score1_runner: 0, score2_corp: 0, label: '3-0' },
Expand Down
2 changes: 1 addition & 1 deletion app/models/pairing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def side_for(player)
end

def table_label
stage.double_elim? ? "Game #{table_number}" : "Table #{table_number}"
stage.double_elim? || stage.single_elim? ? "Game #{table_number}" : "Table #{table_number}"
end

def fixed_table_number?
Expand Down
5 changes: 3 additions & 2 deletions app/models/stage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Stage < ApplicationRecord
enum :format, {
swiss: 0, # Double-Sided Swiss
double_elim: 1,
single_sided_swiss: 2
single_sided_swiss: 2,
single_elim: 3
}

def pair_new_round!
Expand All @@ -38,7 +39,7 @@ def seed(number)
end

def single_sided?
double_elim? || single_sided_swiss?
double_elim? || single_sided_swiss? || single_elim?
end

def default_round_minutes
Expand Down
9 changes: 7 additions & 2 deletions app/models/tournament.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Tournament < ApplicationRecord
has_many :stages, -> { order(:number) }, dependent: :destroy # rubocop:disable Rails/InverseOf
has_many :rounds

# TODO(plural): Rename double_elim to elimination
enum :stage, { swiss: 0, double_elim: 1 }

enum :cut_deck_visibility, { cut_decks_private: 0, cut_decks_open: 1, cut_decks_public: 2 }
Expand Down Expand Up @@ -67,7 +68,7 @@ def registration_unlocked?
end

def stage_decks_open?(stage)
if stage.double_elim?
if stage.double_elim? || stage.single_elim?
cut_decks_open?
elsif stage.swiss?
swiss_decks_open?
Expand All @@ -77,7 +78,7 @@ def stage_decks_open?(stage)
end

def stage_decks_public?(stage)
if stage.double_elim?
if stage.double_elim? || stage.single_elim?
cut_decks_public?
elsif stage.swiss?
swiss_decks_public?
Expand Down Expand Up @@ -174,6 +175,10 @@ def current_stage
stages.last
end

def single_elim_stage
stages.find_by format: :single_elim
end

def double_elim_stage
stages.find_by format: :double_elim
end
Expand Down
7 changes: 5 additions & 2 deletions app/services/bracket/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

module Bracket
class Factory
def self.bracket_for(num_players)
def self.bracket_for(num_players, single_elim: false)
raise 'bracket size not supported' unless [3, 4, 8, 16].include? num_players

"Bracket::Top#{num_players}".constantize
prefix = 'Top'
prefix = 'SingleElimTop' if single_elim || num_players == 3
Rails.logger.info "Bracket class is Bracket::#{prefix}#{num_players}"
"Bracket::#{prefix}#{num_players}".constantize
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Bracket
class Top3 < Base
class SingleElimTop3 < Base
game 1, seed(2), seed(3), round: 1

game 2, seed(1), winner(1), round: 2
Expand Down
8 changes: 5 additions & 3 deletions app/services/nrtm_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def swiss_pairing_data
end

def cut_stage
tournament.stages.find_by(format: :double_elim) || NilStage.new
tournament.stages.find_by(format: :single_elim) || tournament.stages.find_by(format: :double_elim) || NilStage.new
end

def cut_pairing_data
Expand Down Expand Up @@ -120,12 +120,14 @@ def single_sided_pairing_data(pairing)
}.merge(score_for_pairing(pairing, pairing.player2_side, pairing.score2, pairing.score1)),
intentionalDraw: pairing.intentional_draw.present?,
twoForOne: pairing.two_for_one.present?,
eliminationGame: pairing.stage.double_elim?
eliminationGame: pairing.stage.single_elim? || pairing.stage.double_elim?
}
end

def score_for_pairing(pairing, side, score, opp_score)
return { winner: (score > opp_score if score && opp_score) } if pairing.stage.double_elim?
if pairing.stage.single_elim? || pairing.stage.double_elim?
return { winner: (score > opp_score if score && opp_score) }
end

hash = { combinedScore: score }
hash.merge!({ runnerScore: nil, corpScore: score }) if side == :corp
Expand Down
2 changes: 1 addition & 1 deletion app/services/pairer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def pair!
private

def strategy
return PairingStrategies::Swiss unless %w[swiss double_elim single_sided_swiss].include? stage.format
return PairingStrategies::Swiss unless %w[swiss double_elim single_elim single_sided_swiss].include? stage.format

"PairingStrategies::#{stage.format.camelize}".constantize
end
Expand Down
22 changes: 22 additions & 0 deletions app/services/pairing_strategies/single_elim.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module PairingStrategies
class SingleElim < Base
def pair!
bracket.new(stage).pair(round.number).each do |pairing|
round.pairings.create(
player1: pairing[:player1],
player2: pairing[:player2],
table_number: pairing[:table_number],
side: SideDeterminer.determine_sides(pairing[:player1], pairing[:player2], stage)
)
end
end

private

def bracket
Bracket::Factory.bracket_for stage.players.count, single_elim: true
end
end
end
15 changes: 15 additions & 0 deletions app/services/standing_strategies/single_elim.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module StandingStrategies
class SingleElim < Base
def calculate!
bracket.new(stage).standings
end

private

def bracket
Bracket::Factory.bracket_for stage.players.count, single_elim: true
end
end
end
2 changes: 1 addition & 1 deletion app/services/standings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def top(number)
private

def strategy
return StandingStrategies::Swiss unless %w[swiss double_elim].include? stage.format
return StandingStrategies::Swiss unless %w[swiss double_elim single_elim].include? stage.format

"StandingStrategies::#{stage.format.camelize}".constantize
end
Expand Down
2 changes: 1 addition & 1 deletion app/views/rounds/_pairings.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
= fa_icon 'video-camera',
class: 'text-success streaming-tooltip ml-2',
title: 'May be included in video coverage'
- elsif round.stage.double_elim?
- elsif round.stage.single_elim? || round.stage.double_elim?
= fa_icon 'video-camera',
class: 'text-warning streaming-tooltip ml-2',
title: 'One or both players requested not to be included in video coverage,
Expand Down
22 changes: 16 additions & 6 deletions app/views/rounds/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,27 @@
=> fa_icon 'plus'
| Pair new round!
| All rounds must be flagged complete before you can add a new round
- unless @stages.last.double_elim?
- unless @stages.last.single_elim? || @stages.last.double_elim?
p
=> link_to cut_tournament_path(@tournament, number: 3), method: :post, class: 'btn btn-success' do
=> link_to cut_tournament_path(@tournament, number: 3, elimination_type: 'single'), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Cut to Top 3
| Single-Elimination Top 3
=> link_to cut_tournament_path(@tournament, number: 4, elimination_type: 'single'), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Single-Elimination Top 4
=> link_to cut_tournament_path(@tournament, number: 8, elimination_type: 'single'), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Single-Elimination Top 8
=> link_to cut_tournament_path(@tournament, number: 16, elimination_type: 'single'), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Single-Elimination Top 16
p
=> link_to cut_tournament_path(@tournament, number: 4), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Cut to Top 4
| Double-Elimination Top 4
=> link_to cut_tournament_path(@tournament, number: 8), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Cut to Top 8
| Double-Elimination Top 8
=> link_to cut_tournament_path(@tournament, number: 16), method: :post, class: 'btn btn-success' do
=> fa_icon 'scissors'
| Cut to Top 16
| Double-Elimination Top 16
18 changes: 15 additions & 3 deletions spec/feature/tournaments/cut_tournament_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,21 @@
end

it 'creates double elim stage' do
expect do
click_link 'Cut to Top 8'
end.to change(tournament.stages, :count).by(1)
expect(tournament.stages.size).to eq(1)

click_link 'Double-Elimination Top 8'

expect(tournament.stages.size).to eq(2)
expect(tournament.stages.last.format).to eq('double_elim')
end

it 'creates single elim stage' do
expect(tournament.stages.size).to eq(1)

click_link 'Single-Elimination Top 4'

expect(tournament.stages.size).to eq(2)
expect(tournament.stages.last.format).to eq('single_elim')
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

RSpec.describe Bracket::Top3 do
RSpec.describe Bracket::SingleElimTop3 do
let(:tournament) { create(:tournament) }
let(:stage) { tournament.stages.create(format: :double_elim) }
let(:bracket) { described_class.new(stage) }
Expand Down